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

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