@formspec/ts-plugin 0.1.0-alpha.21 → 0.1.0-alpha.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,36 +1,31 @@
1
+ // src/index.ts
2
+ import {
3
+ FORMSPEC_ANALYSIS_PROTOCOL_VERSION as FORMSPEC_ANALYSIS_PROTOCOL_VERSION4,
4
+ FORMSPEC_ANALYSIS_SCHEMA_VERSION as FORMSPEC_ANALYSIS_SCHEMA_VERSION2
5
+ } from "@formspec/analysis/protocol";
6
+
1
7
  // src/service.ts
2
8
  import fs from "fs/promises";
3
9
  import net from "net";
4
10
  import "typescript";
5
11
  import {
6
- FORMSPEC_ANALYSIS_PROTOCOL_VERSION as FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
7
- computeFormSpecTextHash,
12
+ FORMSPEC_ANALYSIS_PROTOCOL_VERSION as FORMSPEC_ANALYSIS_PROTOCOL_VERSION3,
8
13
  isFormSpecSemanticQuery
9
14
  } from "@formspec/analysis/protocol";
10
- import {
11
- buildFormSpecAnalysisFileSnapshot,
12
- createFormSpecPerformanceRecorder,
13
- findDeclarationForCommentOffset,
14
- getCommentHoverInfoAtOffset,
15
- getSemanticCommentCompletionContextAtOffset,
16
- getFormSpecPerformanceNow,
17
- getSubjectType,
18
- optionalMeasure,
19
- resolveDeclarationPlacement,
20
- serializeCompletionContext,
21
- serializeHoverInfo
22
- } from "@formspec/analysis/internal";
15
+ import "@formspec/analysis/internal";
23
16
 
24
17
  // src/workspace.ts
25
18
  import os from "os";
26
19
  import path from "path";
27
20
  import {
28
21
  FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
29
- FORMSPEC_ANALYSIS_SCHEMA_VERSION,
22
+ FORMSPEC_ANALYSIS_SCHEMA_VERSION
23
+ } from "@formspec/analysis/protocol";
24
+ import {
30
25
  getFormSpecManifestPath,
31
26
  getFormSpecWorkspaceId,
32
27
  getFormSpecWorkspaceRuntimeDirectory
33
- } from "@formspec/analysis/protocol";
28
+ } from "@formspec/analysis/internal";
34
29
  function getFormSpecWorkspaceRuntimePaths(workspaceRoot, platform = process.platform, userScope = getFormSpecUserScope()) {
35
30
  const workspaceId = getFormSpecWorkspaceId(workspaceRoot);
36
31
  const runtimeDirectory = getFormSpecWorkspaceRuntimeDirectory(workspaceRoot);
@@ -79,112 +74,154 @@ function sanitizeScopeSegment(value) {
79
74
  return sanitized.length > 0 ? sanitized : "formspec";
80
75
  }
81
76
 
77
+ // src/semantic-service.ts
78
+ import "typescript";
79
+ import {
80
+ FORMSPEC_ANALYSIS_PROTOCOL_VERSION as FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
81
+ computeFormSpecTextHash
82
+ } from "@formspec/analysis/protocol";
83
+ import {
84
+ buildFormSpecAnalysisFileSnapshot,
85
+ createFormSpecPerformanceRecorder,
86
+ findDeclarationForCommentOffset,
87
+ getCommentHoverInfoAtOffset,
88
+ getSemanticCommentCompletionContextAtOffset,
89
+ getFormSpecPerformanceNow,
90
+ getSubjectType,
91
+ optionalMeasure,
92
+ resolveDeclarationPlacement,
93
+ serializeCompletionContext,
94
+ serializeHoverInfo
95
+ } from "@formspec/analysis/internal";
96
+
82
97
  // src/constants.ts
83
98
  var FORM_SPEC_PLUGIN_MAX_SOCKET_PAYLOAD_BYTES = 256 * 1024;
84
99
  var FORM_SPEC_PLUGIN_SOCKET_IDLE_TIMEOUT_MS = 3e4;
85
100
  var FORM_SPEC_PLUGIN_DEFAULT_PERFORMANCE_LOG_THRESHOLD_MS = 50;
86
101
  var FORM_SPEC_PLUGIN_DEFAULT_SNAPSHOT_DEBOUNCE_MS = 250;
87
- var FORM_SPEC_PLUGIN_PERFORMANCE_EVENT = {
88
- handleQuery: "plugin.handleQuery"
89
- };
90
102
 
91
- // src/service.ts
92
- var FormSpecPluginService = class {
103
+ // src/perf-utils.ts
104
+ function formatPerformanceEvent(event) {
105
+ const detailEntries = Object.entries(event.detail ?? {}).map(([key, value]) => `${key}=${String(value)}`).join(" ");
106
+ return `${event.durationMs.toFixed(1)}ms ${event.name}${detailEntries === "" ? "" : ` ${detailEntries}`}`;
107
+ }
108
+
109
+ // src/semantic-service.ts
110
+ var STATS_ONLY_EVENT_NAMES = /* @__PURE__ */ new Set([
111
+ "analysis.syntheticCheckBatch.cacheHit",
112
+ "analysis.narrowSyntheticCheckBatch.cacheHit",
113
+ "analysis.syntheticCheckBatch.cacheMiss",
114
+ "analysis.narrowSyntheticCheckBatch.cacheMiss",
115
+ "analysis.syntheticCheckBatch.createProgram",
116
+ "analysis.narrowSyntheticCheckBatch.createProgram"
117
+ ]);
118
+ var StatsOnlyPerformanceRecorder = class {
119
+ mutableEvents = [];
120
+ get events() {
121
+ return this.mutableEvents;
122
+ }
123
+ measure(name, detail, callback) {
124
+ const result = callback();
125
+ if (STATS_ONLY_EVENT_NAMES.has(name)) {
126
+ this.mutableEvents.push({
127
+ name,
128
+ durationMs: 0,
129
+ ...detail === void 0 ? {} : { detail }
130
+ });
131
+ }
132
+ return result;
133
+ }
134
+ record(event) {
135
+ if (STATS_ONLY_EVENT_NAMES.has(event.name)) {
136
+ this.mutableEvents.push(event);
137
+ }
138
+ }
139
+ };
140
+ var FormSpecSemanticService = class {
93
141
  constructor(options) {
94
142
  this.options = options;
95
- this.runtimePaths = getFormSpecWorkspaceRuntimePaths(options.workspaceRoot);
96
- this.manifest = createFormSpecAnalysisManifest(
97
- options.workspaceRoot,
98
- options.typescriptVersion,
99
- Date.now()
100
- );
101
143
  }
102
- manifest;
103
- runtimePaths;
104
144
  snapshotCache = /* @__PURE__ */ new Map();
105
145
  refreshTimers = /* @__PURE__ */ new Map();
106
- server = null;
107
- getManifest() {
108
- return this.manifest;
146
+ stats = {
147
+ queryTotals: {
148
+ completion: 0,
149
+ hover: 0,
150
+ diagnostics: 0,
151
+ fileSnapshot: 0
152
+ },
153
+ queryPathTotals: {
154
+ diagnostics: { cold: 0, warm: 0 },
155
+ fileSnapshot: { cold: 0, warm: 0 }
156
+ },
157
+ fileSnapshotCacheHits: 0,
158
+ fileSnapshotCacheMisses: 0,
159
+ syntheticBatchCacheHits: 0,
160
+ syntheticBatchCacheMisses: 0,
161
+ syntheticCompileCount: 0,
162
+ syntheticCompileApplications: 0
163
+ };
164
+ /** Resolves semantic completion context for a comment cursor position. */
165
+ getCompletionContext(filePath, offset) {
166
+ this.stats.queryTotals.completion += 1;
167
+ return this.runMeasured(
168
+ "semantic.getCompletionContext",
169
+ { filePath, offset },
170
+ (performance2) => this.withCommentQueryContext(filePath, offset, performance2, (context) => ({
171
+ protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
172
+ sourceHash: context.sourceHash,
173
+ context: serializeCompletionContext(
174
+ getSemanticCommentCompletionContextAtOffset(context.sourceFile.text, offset, {
175
+ checker: context.checker,
176
+ ...context.placement === null ? {} : { placement: context.placement },
177
+ ...context.subjectType === void 0 ? {} : { subjectType: context.subjectType }
178
+ })
179
+ )
180
+ }))
181
+ );
109
182
  }
110
- async start() {
111
- if (this.server !== null) {
112
- return;
113
- }
114
- await fs.mkdir(this.runtimePaths.runtimeDirectory, { recursive: true });
115
- if (this.runtimePaths.endpoint.kind === "unix-socket") {
116
- await fs.rm(this.runtimePaths.endpoint.address, { force: true });
117
- }
118
- this.server = net.createServer((socket) => {
119
- let buffer = "";
120
- socket.setEncoding("utf8");
121
- socket.setTimeout(FORM_SPEC_PLUGIN_SOCKET_IDLE_TIMEOUT_MS, () => {
122
- this.options.logger?.info(
123
- `[FormSpec] Closing idle semantic query socket for ${this.runtimePaths.workspaceRoot}`
124
- );
125
- socket.destroy();
126
- });
127
- socket.on("data", (chunk) => {
128
- buffer += String(chunk);
129
- if (buffer.length > FORM_SPEC_PLUGIN_MAX_SOCKET_PAYLOAD_BYTES) {
130
- socket.end(
131
- `${JSON.stringify({
132
- protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
133
- kind: "error",
134
- error: `FormSpec semantic query exceeded ${String(FORM_SPEC_PLUGIN_MAX_SOCKET_PAYLOAD_BYTES)} bytes`
135
- })}
136
- `
137
- );
138
- return;
139
- }
140
- const newlineIndex = buffer.indexOf("\n");
141
- if (newlineIndex < 0) {
142
- return;
143
- }
144
- const payload = buffer.slice(0, newlineIndex);
145
- const remaining = buffer.slice(newlineIndex + 1);
146
- if (remaining.trim().length > 0) {
147
- this.options.logger?.info(
148
- `[FormSpec] Ignoring extra semantic query payload data for ${this.runtimePaths.workspaceRoot}`
149
- );
150
- }
151
- buffer = remaining;
152
- this.respondToSocket(socket, payload);
153
- });
154
- });
155
- await new Promise((resolve, reject) => {
156
- const handleError = (error) => {
157
- reject(error);
183
+ /** Resolves semantic hover payload for a comment cursor position. */
184
+ getHover(filePath, offset) {
185
+ this.stats.queryTotals.hover += 1;
186
+ return this.runMeasured(
187
+ "semantic.getHover",
188
+ { filePath, offset },
189
+ (performance2) => this.withCommentQueryContext(filePath, offset, performance2, (context) => ({
190
+ protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
191
+ sourceHash: context.sourceHash,
192
+ hover: serializeHoverInfo(
193
+ getCommentHoverInfoAtOffset(context.sourceFile.text, offset, {
194
+ checker: context.checker,
195
+ ...context.placement === null ? {} : { placement: context.placement },
196
+ ...context.subjectType === void 0 ? {} : { subjectType: context.subjectType }
197
+ })
198
+ )
199
+ }))
200
+ );
201
+ }
202
+ /** Returns canonical FormSpec diagnostics for a file in the current host program. */
203
+ getDiagnostics(filePath) {
204
+ this.stats.queryTotals.diagnostics += 1;
205
+ return this.runMeasured("semantic.getDiagnostics", { filePath }, (performance2) => {
206
+ const { snapshot, cacheState } = this.getFileSnapshotWithCacheState(filePath, performance2);
207
+ this.recordQueryPath("diagnostics", cacheState);
208
+ return {
209
+ protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
210
+ sourceHash: snapshot.sourceHash,
211
+ diagnostics: snapshot.diagnostics
158
212
  };
159
- this.server?.once("error", handleError);
160
- this.server?.listen(this.runtimePaths.endpoint.address, () => {
161
- this.server?.off("error", handleError);
162
- resolve();
163
- });
164
213
  });
165
- await this.writeManifest();
166
214
  }
167
- async stop() {
168
- for (const timer of this.refreshTimers.values()) {
169
- clearTimeout(timer);
170
- }
171
- this.refreshTimers.clear();
172
- this.snapshotCache.clear();
173
- const server = this.server;
174
- this.server = null;
175
- if (server?.listening === true) {
176
- await new Promise((resolve, reject) => {
177
- server.close((error) => {
178
- if (error === void 0) {
179
- resolve();
180
- return;
181
- }
182
- reject(error);
183
- });
184
- });
185
- }
186
- await this.cleanupRuntimeArtifacts();
215
+ /** Returns the full serialized semantic snapshot for a file. */
216
+ getFileSnapshot(filePath) {
217
+ this.stats.queryTotals.fileSnapshot += 1;
218
+ return this.runMeasured("semantic.getFileSnapshot", { filePath }, (performance2) => {
219
+ const { snapshot, cacheState } = this.getFileSnapshotWithCacheState(filePath, performance2);
220
+ this.recordQueryPath("fileSnapshot", cacheState);
221
+ return snapshot;
222
+ });
187
223
  }
224
+ /** Schedules a debounced background refresh for the file snapshot cache. */
188
225
  scheduleSnapshotRefresh(filePath) {
189
226
  const existing = this.refreshTimers.get(filePath);
190
227
  if (existing !== void 0) {
@@ -192,7 +229,7 @@ var FormSpecPluginService = class {
192
229
  }
193
230
  const timer = setTimeout(() => {
194
231
  try {
195
- this.getFileSnapshot(filePath, void 0);
232
+ this.getFileSnapshot(filePath);
196
233
  } catch (error) {
197
234
  this.options.logger?.info(
198
235
  `[FormSpec] Failed to refresh semantic snapshot for ${filePath}: ${String(error)}`
@@ -200,76 +237,58 @@ var FormSpecPluginService = class {
200
237
  }
201
238
  this.refreshTimers.delete(filePath);
202
239
  }, this.options.snapshotDebounceMs ?? FORM_SPEC_PLUGIN_DEFAULT_SNAPSHOT_DEBOUNCE_MS);
240
+ timer.unref();
203
241
  this.refreshTimers.set(filePath, timer);
204
242
  }
205
- handleQuery(query) {
206
- const performance = this.options.enablePerformanceLogging === true ? createFormSpecPerformanceRecorder() : void 0;
207
- const response = optionalMeasure(
208
- performance,
209
- FORM_SPEC_PLUGIN_PERFORMANCE_EVENT.handleQuery,
210
- {
211
- kind: query.kind,
212
- ...query.kind === "health" ? {} : { filePath: query.filePath }
213
- },
214
- () => this.executeQuery(query, performance)
215
- );
216
- if (performance !== void 0) {
217
- this.logPerformanceEvents(performance.events);
218
- }
219
- return response;
220
- }
221
- respondToSocket(socket, payload) {
222
- try {
223
- const query = JSON.parse(payload);
224
- if (!isFormSpecSemanticQuery(query)) {
225
- throw new Error("Invalid FormSpec semantic query payload");
226
- }
227
- const response = this.handleQuery(query);
228
- socket.end(`${JSON.stringify(response)}
229
- `);
230
- } catch (error) {
231
- socket.end(
232
- `${JSON.stringify({
233
- protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
234
- kind: "error",
235
- error: error instanceof Error ? error.message : String(error)
236
- })}
237
- `
238
- );
243
+ /** Clears pending timers and cached semantic snapshots. */
244
+ dispose() {
245
+ for (const timer of this.refreshTimers.values()) {
246
+ clearTimeout(timer);
239
247
  }
248
+ this.refreshTimers.clear();
249
+ this.snapshotCache.clear();
240
250
  }
241
- async writeManifest() {
242
- const tempManifestPath = `${this.runtimePaths.manifestPath}.tmp`;
243
- await fs.writeFile(tempManifestPath, `${JSON.stringify(this.manifest, null, 2)}
244
- `, "utf8");
245
- await fs.rename(tempManifestPath, this.runtimePaths.manifestPath);
251
+ /** Returns a copy of the current performance and cache counters. */
252
+ getStats() {
253
+ return {
254
+ queryTotals: { ...this.stats.queryTotals },
255
+ queryPathTotals: {
256
+ diagnostics: { ...this.stats.queryPathTotals.diagnostics },
257
+ fileSnapshot: { ...this.stats.queryPathTotals.fileSnapshot }
258
+ },
259
+ fileSnapshotCacheHits: this.stats.fileSnapshotCacheHits,
260
+ fileSnapshotCacheMisses: this.stats.fileSnapshotCacheMisses,
261
+ syntheticBatchCacheHits: this.stats.syntheticBatchCacheHits,
262
+ syntheticBatchCacheMisses: this.stats.syntheticBatchCacheMisses,
263
+ syntheticCompileCount: this.stats.syntheticCompileCount,
264
+ syntheticCompileApplications: this.stats.syntheticCompileApplications
265
+ };
246
266
  }
247
- async cleanupRuntimeArtifacts() {
248
- await fs.rm(this.runtimePaths.manifestPath, { force: true });
249
- if (this.runtimePaths.endpoint.kind === "unix-socket") {
250
- await fs.rm(this.runtimePaths.endpoint.address, { force: true });
267
+ runMeasured(name, detail, fn) {
268
+ const performance2 = this.options.enablePerformanceLogging === true ? createFormSpecPerformanceRecorder() : new StatsOnlyPerformanceRecorder();
269
+ const result = optionalMeasure(performance2, name, detail, () => fn(performance2));
270
+ this.updateStatsFromPerformanceEvents(performance2.events);
271
+ if (this.options.enablePerformanceLogging === true) {
272
+ this.logPerformanceEvents(name, performance2.events);
251
273
  }
274
+ return result;
252
275
  }
253
- withCommentQueryContext(filePath, offset, handler, performance) {
276
+ withCommentQueryContext(filePath, offset, performance2, handler) {
254
277
  return optionalMeasure(
255
- performance,
256
- "plugin.resolveCommentQueryContext",
278
+ performance2,
279
+ "semantic.resolveCommentQueryContext",
257
280
  {
258
281
  filePath,
259
282
  offset
260
283
  },
261
284
  () => {
262
- const environment = this.getSourceEnvironment(filePath, performance);
285
+ const environment = this.getSourceEnvironment(filePath, performance2);
263
286
  if (environment === null) {
264
- return {
265
- protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
266
- kind: "error",
267
- error: `Unable to resolve TypeScript source file for ${filePath}`
268
- };
287
+ return null;
269
288
  }
270
289
  const declaration = optionalMeasure(
271
- performance,
272
- "plugin.findDeclarationForCommentOffset",
290
+ performance2,
291
+ "semantic.findDeclarationForCommentOffset",
273
292
  {
274
293
  filePath,
275
294
  offset
@@ -277,14 +296,14 @@ var FormSpecPluginService = class {
277
296
  () => findDeclarationForCommentOffset(environment.sourceFile, offset)
278
297
  );
279
298
  const placement = declaration === null ? null : optionalMeasure(
280
- performance,
281
- "plugin.resolveDeclarationPlacement",
299
+ performance2,
300
+ "semantic.resolveDeclarationPlacement",
282
301
  void 0,
283
302
  () => resolveDeclarationPlacement(declaration)
284
303
  );
285
304
  const subjectType = declaration === null ? void 0 : optionalMeasure(
286
- performance,
287
- "plugin.getSubjectType",
305
+ performance2,
306
+ "semantic.getSubjectType",
288
307
  void 0,
289
308
  () => getSubjectType(declaration, environment.checker)
290
309
  );
@@ -297,11 +316,12 @@ var FormSpecPluginService = class {
297
316
  }
298
317
  );
299
318
  }
300
- getFileSnapshot(filePath, performance) {
319
+ getFileSnapshotWithCacheState(filePath, performance2) {
301
320
  const startedAt = getFormSpecPerformanceNow();
302
- const environment = this.getSourceEnvironment(filePath, performance);
321
+ const environment = this.getSourceEnvironment(filePath, performance2);
303
322
  if (environment === null) {
304
- const missingSourceSnapshot = {
323
+ this.stats.fileSnapshotCacheMisses += 1;
324
+ const snapshot2 = {
305
325
  filePath,
306
326
  sourceHash: "",
307
327
  generatedAt: this.getNow().toISOString(),
@@ -309,131 +329,83 @@ var FormSpecPluginService = class {
309
329
  diagnostics: [
310
330
  {
311
331
  code: "MISSING_SOURCE_FILE",
332
+ category: "infrastructure",
312
333
  message: `Unable to resolve TypeScript source file for ${filePath}`,
313
334
  range: { start: 0, end: 0 },
314
- severity: "warning"
335
+ severity: "warning",
336
+ relatedLocations: [],
337
+ data: {
338
+ filePath
339
+ }
315
340
  }
316
341
  ]
317
342
  };
318
- performance?.record({
319
- name: "plugin.getFileSnapshot",
343
+ performance2.record({
344
+ name: "semantic.getFileSnapshot.result",
320
345
  durationMs: getFormSpecPerformanceNow() - startedAt,
321
346
  detail: {
322
347
  filePath,
323
348
  cache: "missing-source"
324
349
  }
325
350
  });
326
- return missingSourceSnapshot;
351
+ return {
352
+ snapshot: snapshot2,
353
+ cacheState: "missing-source"
354
+ };
327
355
  }
328
356
  const cached = this.snapshotCache.get(filePath);
329
357
  if (cached?.sourceHash === environment.sourceHash) {
330
- performance?.record({
331
- name: "plugin.getFileSnapshot",
358
+ this.stats.fileSnapshotCacheHits += 1;
359
+ performance2.record({
360
+ name: "semantic.getFileSnapshot.result",
332
361
  durationMs: getFormSpecPerformanceNow() - startedAt,
333
362
  detail: {
334
363
  filePath,
335
364
  cache: "hit"
336
365
  }
337
366
  });
338
- return cached.snapshot;
367
+ return {
368
+ snapshot: cached.snapshot,
369
+ cacheState: "hit"
370
+ };
339
371
  }
372
+ this.stats.fileSnapshotCacheMisses += 1;
340
373
  const snapshot = buildFormSpecAnalysisFileSnapshot(environment.sourceFile, {
341
374
  checker: environment.checker,
342
375
  now: () => this.getNow(),
343
- ...performance === void 0 ? {} : { performance }
376
+ performance: performance2
344
377
  });
345
378
  this.snapshotCache.set(filePath, {
346
379
  sourceHash: environment.sourceHash,
347
380
  snapshot
348
381
  });
349
- performance?.record({
350
- name: "plugin.getFileSnapshot",
382
+ performance2.record({
383
+ name: "semantic.getFileSnapshot.result",
351
384
  durationMs: getFormSpecPerformanceNow() - startedAt,
352
385
  detail: {
353
386
  filePath,
354
387
  cache: "miss"
355
388
  }
356
389
  });
357
- return snapshot;
390
+ return {
391
+ snapshot,
392
+ cacheState: "miss"
393
+ };
358
394
  }
359
395
  getNow() {
360
396
  return this.options.now?.() ?? /* @__PURE__ */ new Date();
361
397
  }
362
- executeQuery(query, performance) {
363
- switch (query.kind) {
364
- case "health":
365
- return {
366
- protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
367
- kind: "health",
368
- manifest: this.manifest
369
- };
370
- case "completion":
371
- return this.withCommentQueryContext(
372
- query.filePath,
373
- query.offset,
374
- (context) => ({
375
- protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
376
- kind: "completion",
377
- sourceHash: context.sourceHash,
378
- context: serializeCompletionContext(
379
- getSemanticCommentCompletionContextAtOffset(context.sourceFile.text, query.offset, {
380
- checker: context.checker,
381
- ...context.placement === null ? {} : { placement: context.placement },
382
- ...context.subjectType === void 0 ? {} : { subjectType: context.subjectType }
383
- })
384
- )
385
- }),
386
- performance
387
- );
388
- case "hover":
389
- return this.withCommentQueryContext(
390
- query.filePath,
391
- query.offset,
392
- (context) => ({
393
- protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
394
- kind: "hover",
395
- sourceHash: context.sourceHash,
396
- hover: serializeHoverInfo(
397
- getCommentHoverInfoAtOffset(context.sourceFile.text, query.offset, {
398
- checker: context.checker,
399
- ...context.placement === null ? {} : { placement: context.placement },
400
- ...context.subjectType === void 0 ? {} : { subjectType: context.subjectType }
401
- })
402
- )
403
- }),
404
- performance
405
- );
406
- case "diagnostics": {
407
- const snapshot = this.getFileSnapshot(query.filePath, performance);
408
- return {
409
- protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
410
- kind: "diagnostics",
411
- sourceHash: snapshot.sourceHash,
412
- diagnostics: snapshot.diagnostics
413
- };
414
- }
415
- case "file-snapshot":
416
- return {
417
- protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
418
- kind: "file-snapshot",
419
- snapshot: this.getFileSnapshot(query.filePath, performance)
420
- };
421
- default: {
422
- throw new Error(`Unhandled semantic query: ${JSON.stringify(query)}`);
423
- }
424
- }
425
- }
426
- getSourceEnvironment(filePath, performance) {
398
+ getSourceEnvironment(filePath, performance2) {
427
399
  return optionalMeasure(
428
- performance,
429
- "plugin.getSourceEnvironment",
400
+ performance2,
401
+ "semantic.getSourceEnvironment",
430
402
  {
431
403
  filePath
432
404
  },
433
405
  () => {
434
406
  const program = optionalMeasure(
435
- performance,
436
- "plugin.sourceEnvironment.getProgram",
407
+ performance2,
408
+ "semantic.sourceEnvironment.getProgram",
437
409
  void 0,
438
410
  () => this.options.getProgram()
439
411
  );
@@ -441,8 +413,8 @@ var FormSpecPluginService = class {
441
413
  return null;
442
414
  }
443
415
  const sourceFile = optionalMeasure(
444
- performance,
445
- "plugin.sourceEnvironment.getSourceFile",
416
+ performance2,
417
+ "semantic.sourceEnvironment.getSourceFile",
446
418
  void 0,
447
419
  () => program.getSourceFile(filePath)
448
420
  );
@@ -450,14 +422,14 @@ var FormSpecPluginService = class {
450
422
  return null;
451
423
  }
452
424
  const checker = optionalMeasure(
453
- performance,
454
- "plugin.sourceEnvironment.getTypeChecker",
425
+ performance2,
426
+ "semantic.sourceEnvironment.getTypeChecker",
455
427
  void 0,
456
428
  () => program.getTypeChecker()
457
429
  );
458
430
  const sourceHash = optionalMeasure(
459
- performance,
460
- "plugin.sourceEnvironment.computeTextHash",
431
+ performance2,
432
+ "semantic.sourceEnvironment.computeTextHash",
461
433
  void 0,
462
434
  () => computeFormSpecTextHash(sourceFile.text)
463
435
  );
@@ -469,19 +441,38 @@ var FormSpecPluginService = class {
469
441
  }
470
442
  );
471
443
  }
472
- logPerformanceEvents(events) {
473
- const logger = this.options.logger;
474
- if (logger === void 0 || events.length === 0) {
444
+ recordQueryPath(kind, cacheState) {
445
+ if (cacheState === "hit") {
446
+ this.stats.queryPathTotals[kind].warm += 1;
475
447
  return;
476
448
  }
477
- let rootEvent;
478
- for (let index = events.length - 1; index >= 0; index -= 1) {
479
- const candidate = events[index];
480
- if (candidate?.name === FORM_SPEC_PLUGIN_PERFORMANCE_EVENT.handleQuery) {
481
- rootEvent = candidate;
482
- break;
449
+ this.stats.queryPathTotals[kind].cold += 1;
450
+ }
451
+ updateStatsFromPerformanceEvents(events) {
452
+ for (const event of events) {
453
+ if (event.name === "analysis.syntheticCheckBatch.cacheHit" || event.name === "analysis.narrowSyntheticCheckBatch.cacheHit") {
454
+ this.stats.syntheticBatchCacheHits += 1;
455
+ continue;
456
+ }
457
+ if (event.name === "analysis.syntheticCheckBatch.cacheMiss" || event.name === "analysis.narrowSyntheticCheckBatch.cacheMiss") {
458
+ this.stats.syntheticBatchCacheMisses += 1;
459
+ const applicationCount = event.detail?.["applicationCount"];
460
+ if (typeof applicationCount === "number") {
461
+ this.stats.syntheticCompileApplications += applicationCount;
462
+ }
463
+ continue;
464
+ }
465
+ if (event.name === "analysis.syntheticCheckBatch.createProgram" || event.name === "analysis.narrowSyntheticCheckBatch.createProgram") {
466
+ this.stats.syntheticCompileCount += 1;
483
467
  }
484
468
  }
469
+ }
470
+ logPerformanceEvents(rootEventName, events) {
471
+ const logger = this.options.logger;
472
+ if (logger === void 0 || events.length === 0) {
473
+ return;
474
+ }
475
+ const rootEvent = [...events].reverse().find((event) => event.name === rootEventName);
485
476
  if (rootEvent === void 0) {
486
477
  return;
487
478
  }
@@ -489,18 +480,234 @@ var FormSpecPluginService = class {
489
480
  if (rootEvent.durationMs < thresholdMs) {
490
481
  return;
491
482
  }
492
- const sortedHotspots = [...events].filter((event) => event.name !== FORM_SPEC_PLUGIN_PERFORMANCE_EVENT.handleQuery).sort((left, right) => right.durationMs - left.durationMs).slice(0, 8);
483
+ const sortedHotspots = [...events].filter((event) => event.name !== rootEventName).sort((left, right) => right.durationMs - left.durationMs).slice(0, 8);
493
484
  const lines = [
494
- `[FormSpec][perf] ${rootEvent.name} ${formatPerformanceEvent(rootEvent)}`,
485
+ `[FormSpec][perf] ${formatPerformanceEvent(rootEvent)}`,
495
486
  ...sortedHotspots.map((event) => ` ${formatPerformanceEvent(event)}`)
496
487
  ];
497
488
  logger.info(lines.join("\n"));
498
489
  }
499
490
  };
500
- function formatPerformanceEvent(event) {
501
- const detailEntries = Object.entries(event.detail ?? {}).map(([key, value]) => `${key}=${String(value)}`).join(" ");
502
- return `${event.durationMs.toFixed(1)}ms ${event.name}${detailEntries === "" ? "" : ` ${detailEntries}`}`;
503
- }
491
+
492
+ // src/service.ts
493
+ var FormSpecPluginService = class {
494
+ constructor(options) {
495
+ this.options = options;
496
+ this.semanticService = new FormSpecSemanticService(options);
497
+ this.runtimePaths = getFormSpecWorkspaceRuntimePaths(options.workspaceRoot);
498
+ this.manifest = createFormSpecAnalysisManifest(
499
+ options.workspaceRoot,
500
+ options.typescriptVersion,
501
+ Date.now()
502
+ );
503
+ }
504
+ manifest;
505
+ runtimePaths;
506
+ semanticService;
507
+ server = null;
508
+ getManifest() {
509
+ return this.manifest;
510
+ }
511
+ /**
512
+ * Returns the underlying semantic service used by this reference wrapper.
513
+ *
514
+ * @public
515
+ */
516
+ getSemanticService() {
517
+ return this.semanticService;
518
+ }
519
+ async start() {
520
+ if (this.server !== null) {
521
+ return;
522
+ }
523
+ await fs.mkdir(this.runtimePaths.runtimeDirectory, { recursive: true });
524
+ if (this.runtimePaths.endpoint.kind === "unix-socket") {
525
+ await fs.rm(this.runtimePaths.endpoint.address, { force: true });
526
+ }
527
+ this.server = net.createServer((socket) => {
528
+ let buffer = "";
529
+ socket.setEncoding("utf8");
530
+ socket.setTimeout(FORM_SPEC_PLUGIN_SOCKET_IDLE_TIMEOUT_MS, () => {
531
+ this.options.logger?.info(
532
+ `[FormSpec] Closing idle semantic query socket for ${this.runtimePaths.workspaceRoot}`
533
+ );
534
+ socket.destroy();
535
+ });
536
+ socket.on("data", (chunk) => {
537
+ buffer += String(chunk);
538
+ if (buffer.length > FORM_SPEC_PLUGIN_MAX_SOCKET_PAYLOAD_BYTES) {
539
+ socket.end(
540
+ `${JSON.stringify({
541
+ protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION3,
542
+ kind: "error",
543
+ error: `FormSpec semantic query exceeded ${String(FORM_SPEC_PLUGIN_MAX_SOCKET_PAYLOAD_BYTES)} bytes`
544
+ })}
545
+ `
546
+ );
547
+ return;
548
+ }
549
+ const newlineIndex = buffer.indexOf("\n");
550
+ if (newlineIndex < 0) {
551
+ return;
552
+ }
553
+ const payload = buffer.slice(0, newlineIndex);
554
+ const remaining = buffer.slice(newlineIndex + 1);
555
+ if (remaining.trim().length > 0) {
556
+ this.options.logger?.info(
557
+ `[FormSpec] Ignoring extra semantic query payload data for ${this.runtimePaths.workspaceRoot}`
558
+ );
559
+ }
560
+ buffer = remaining;
561
+ this.respondToSocket(socket, payload);
562
+ });
563
+ });
564
+ await new Promise((resolve, reject) => {
565
+ const handleError = (error) => {
566
+ reject(error);
567
+ };
568
+ this.server?.once("error", handleError);
569
+ this.server?.listen(this.runtimePaths.endpoint.address, () => {
570
+ this.server?.off("error", handleError);
571
+ resolve();
572
+ });
573
+ });
574
+ await this.writeManifest();
575
+ }
576
+ async stop() {
577
+ this.semanticService.dispose();
578
+ const server = this.server;
579
+ this.server = null;
580
+ if (server?.listening === true) {
581
+ await new Promise((resolve, reject) => {
582
+ server.close((error) => {
583
+ if (error === void 0) {
584
+ resolve();
585
+ return;
586
+ }
587
+ reject(error);
588
+ });
589
+ });
590
+ }
591
+ await this.cleanupRuntimeArtifacts();
592
+ }
593
+ scheduleSnapshotRefresh(filePath) {
594
+ this.semanticService.scheduleSnapshotRefresh(filePath);
595
+ }
596
+ handleQuery(query) {
597
+ if (this.options.enablePerformanceLogging === true) {
598
+ const startedAt = performance.now();
599
+ const response = this.executeQuery(query);
600
+ this.logQueryDuration(query, performance.now() - startedAt);
601
+ return response;
602
+ }
603
+ return this.executeQuery(query);
604
+ }
605
+ executeQuery(query) {
606
+ switch (query.kind) {
607
+ case "health":
608
+ return {
609
+ protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION3,
610
+ kind: "health",
611
+ manifest: this.manifest
612
+ };
613
+ case "completion": {
614
+ const result = this.semanticService.getCompletionContext(query.filePath, query.offset);
615
+ if (result === null) {
616
+ return {
617
+ protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION3,
618
+ kind: "error",
619
+ error: `Unable to resolve TypeScript source file for ${query.filePath}`
620
+ };
621
+ }
622
+ return {
623
+ ...result,
624
+ kind: "completion"
625
+ };
626
+ }
627
+ case "hover": {
628
+ const result = this.semanticService.getHover(query.filePath, query.offset);
629
+ if (result === null) {
630
+ return {
631
+ protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION3,
632
+ kind: "error",
633
+ error: `Unable to resolve TypeScript source file for ${query.filePath}`
634
+ };
635
+ }
636
+ return {
637
+ ...result,
638
+ kind: "hover"
639
+ };
640
+ }
641
+ case "diagnostics": {
642
+ const result = this.semanticService.getDiagnostics(query.filePath);
643
+ return {
644
+ ...result,
645
+ kind: "diagnostics"
646
+ };
647
+ }
648
+ case "file-snapshot":
649
+ return {
650
+ protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION3,
651
+ kind: "file-snapshot",
652
+ snapshot: this.semanticService.getFileSnapshot(query.filePath)
653
+ };
654
+ default: {
655
+ throw new Error(`Unhandled semantic query: ${JSON.stringify(query)}`);
656
+ }
657
+ }
658
+ }
659
+ respondToSocket(socket, payload) {
660
+ try {
661
+ const query = JSON.parse(payload);
662
+ if (!isFormSpecSemanticQuery(query)) {
663
+ throw new Error("Invalid FormSpec semantic query payload");
664
+ }
665
+ const response = this.handleQuery(query);
666
+ socket.end(`${JSON.stringify(response)}
667
+ `);
668
+ } catch (error) {
669
+ socket.end(
670
+ `${JSON.stringify({
671
+ protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION3,
672
+ kind: "error",
673
+ error: error instanceof Error ? error.message : String(error)
674
+ })}
675
+ `
676
+ );
677
+ }
678
+ }
679
+ async writeManifest() {
680
+ const tempManifestPath = `${this.runtimePaths.manifestPath}.tmp`;
681
+ await fs.writeFile(tempManifestPath, `${JSON.stringify(this.manifest, null, 2)}
682
+ `, "utf8");
683
+ await fs.rename(tempManifestPath, this.runtimePaths.manifestPath);
684
+ }
685
+ async cleanupRuntimeArtifacts() {
686
+ await fs.rm(this.runtimePaths.manifestPath, { force: true });
687
+ if (this.runtimePaths.endpoint.kind === "unix-socket") {
688
+ await fs.rm(this.runtimePaths.endpoint.address, { force: true });
689
+ }
690
+ }
691
+ logQueryDuration(query, durationMs) {
692
+ const logger = this.options.logger;
693
+ if (logger === void 0) {
694
+ return;
695
+ }
696
+ const thresholdMs = this.options.performanceLogThresholdMs ?? FORM_SPEC_PLUGIN_DEFAULT_PERFORMANCE_LOG_THRESHOLD_MS;
697
+ if (durationMs < thresholdMs) {
698
+ return;
699
+ }
700
+ const event = {
701
+ name: "plugin.handleQuery",
702
+ durationMs,
703
+ detail: {
704
+ kind: query.kind,
705
+ ...query.kind === "health" ? {} : { filePath: query.filePath }
706
+ }
707
+ };
708
+ logger.info(`[FormSpec][perf] ${formatPerformanceEvent(event)}`);
709
+ }
710
+ };
504
711
  function createLanguageServiceProxy(languageService, semanticService) {
505
712
  const wrapWithSnapshotRefresh = (fn) => {
506
713
  return (fileName, ...args) => {
@@ -609,11 +816,16 @@ function init(modules) {
609
816
  return {
610
817
  create(info) {
611
818
  const service = getOrCreateService(info, typescriptVersion);
612
- return createLanguageServiceProxy(info.languageService, service);
819
+ return createLanguageServiceProxy(info.languageService, service.getSemanticService());
613
820
  }
614
821
  };
615
822
  }
616
823
  export {
824
+ FORMSPEC_ANALYSIS_PROTOCOL_VERSION4 as FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
825
+ FORMSPEC_ANALYSIS_SCHEMA_VERSION2 as FORMSPEC_ANALYSIS_SCHEMA_VERSION,
826
+ FormSpecPluginService,
827
+ FormSpecSemanticService,
828
+ createLanguageServiceProxy,
617
829
  init
618
830
  };
619
831
  //# sourceMappingURL=index.js.map