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