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