@formspec/ts-plugin 0.1.0-alpha.20
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/README.md +3 -0
- package/dist/__tests__/service.test.d.ts +2 -0
- package/dist/__tests__/service.test.d.ts.map +1 -0
- package/dist/__tests__/workspace.test.d.ts +2 -0
- package/dist/__tests__/workspace.test.d.ts.map +1 -0
- package/dist/index.cjs +453 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +434 -0
- package/dist/index.js.map +1 -0
- package/dist/service.d.ts +36 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/ts-plugin.d.ts +12 -0
- package/dist/workspace.d.ts +11 -0
- package/dist/workspace.d.ts.map +1 -0
- package/index.cjs +1 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/service.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/workspace.test.ts"],"names":[],"mappings":""}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
init: () => init
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(index_exports);
|
|
36
|
+
|
|
37
|
+
// src/service.ts
|
|
38
|
+
var import_promises = __toESM(require("fs/promises"), 1);
|
|
39
|
+
var import_node_net = __toESM(require("net"), 1);
|
|
40
|
+
var ts = require("typescript");
|
|
41
|
+
var import_analysis2 = require("@formspec/analysis");
|
|
42
|
+
|
|
43
|
+
// src/workspace.ts
|
|
44
|
+
var import_node_os = __toESM(require("os"), 1);
|
|
45
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
46
|
+
var import_analysis = require("@formspec/analysis");
|
|
47
|
+
function getFormSpecWorkspaceRuntimePaths(workspaceRoot, platform = process.platform, userScope = getFormSpecUserScope()) {
|
|
48
|
+
const workspaceId = (0, import_analysis.getFormSpecWorkspaceId)(workspaceRoot);
|
|
49
|
+
const runtimeDirectory = (0, import_analysis.getFormSpecWorkspaceRuntimeDirectory)(workspaceRoot);
|
|
50
|
+
const sanitizedUserScope = sanitizeScopeSegment(userScope);
|
|
51
|
+
const endpoint = platform === "win32" ? {
|
|
52
|
+
kind: "windows-pipe",
|
|
53
|
+
address: `\\\\.\\pipe\\formspec-${sanitizedUserScope}-${workspaceId}`
|
|
54
|
+
} : {
|
|
55
|
+
kind: "unix-socket",
|
|
56
|
+
address: import_node_path.default.join(import_node_os.default.tmpdir(), `formspec-${sanitizedUserScope}-${workspaceId}.sock`)
|
|
57
|
+
};
|
|
58
|
+
return {
|
|
59
|
+
workspaceRoot,
|
|
60
|
+
workspaceId,
|
|
61
|
+
runtimeDirectory,
|
|
62
|
+
manifestPath: (0, import_analysis.getFormSpecManifestPath)(workspaceRoot),
|
|
63
|
+
endpoint
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function createFormSpecAnalysisManifest(workspaceRoot, typescriptVersion, generation, extensionFingerprint = "builtin") {
|
|
67
|
+
const paths = getFormSpecWorkspaceRuntimePaths(workspaceRoot);
|
|
68
|
+
return {
|
|
69
|
+
protocolVersion: import_analysis.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
70
|
+
analysisSchemaVersion: import_analysis.FORMSPEC_ANALYSIS_SCHEMA_VERSION,
|
|
71
|
+
workspaceRoot,
|
|
72
|
+
workspaceId: paths.workspaceId,
|
|
73
|
+
endpoint: paths.endpoint,
|
|
74
|
+
typescriptVersion,
|
|
75
|
+
extensionFingerprint,
|
|
76
|
+
generation,
|
|
77
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function getFormSpecUserScope() {
|
|
81
|
+
try {
|
|
82
|
+
return sanitizeScopeSegment(import_node_os.default.userInfo().username);
|
|
83
|
+
} catch {
|
|
84
|
+
return sanitizeScopeSegment(
|
|
85
|
+
process.env["USER"] ?? process.env["USERNAME"] ?? process.env["LOGNAME"] ?? "formspec"
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function sanitizeScopeSegment(value) {
|
|
90
|
+
const trimmed = value.trim().toLowerCase();
|
|
91
|
+
const sanitized = trimmed.replace(/[^a-z0-9_-]+/gu, "-").replace(/-+/gu, "-");
|
|
92
|
+
return sanitized.length > 0 ? sanitized : "formspec";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/service.ts
|
|
96
|
+
var FormSpecPluginService = class {
|
|
97
|
+
constructor(options) {
|
|
98
|
+
this.options = options;
|
|
99
|
+
this.runtimePaths = getFormSpecWorkspaceRuntimePaths(options.workspaceRoot);
|
|
100
|
+
this.manifest = createFormSpecAnalysisManifest(
|
|
101
|
+
options.workspaceRoot,
|
|
102
|
+
options.typescriptVersion,
|
|
103
|
+
Date.now()
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
manifest;
|
|
107
|
+
runtimePaths;
|
|
108
|
+
snapshotCache = /* @__PURE__ */ new Map();
|
|
109
|
+
refreshTimers = /* @__PURE__ */ new Map();
|
|
110
|
+
server = null;
|
|
111
|
+
getManifest() {
|
|
112
|
+
return this.manifest;
|
|
113
|
+
}
|
|
114
|
+
async start() {
|
|
115
|
+
if (this.server !== null) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
await import_promises.default.mkdir(this.runtimePaths.runtimeDirectory, { recursive: true });
|
|
119
|
+
if (this.runtimePaths.endpoint.kind === "unix-socket") {
|
|
120
|
+
await import_promises.default.rm(this.runtimePaths.endpoint.address, { force: true });
|
|
121
|
+
}
|
|
122
|
+
this.server = import_node_net.default.createServer((socket) => {
|
|
123
|
+
let buffer = "";
|
|
124
|
+
socket.setEncoding("utf8");
|
|
125
|
+
socket.on("data", (chunk) => {
|
|
126
|
+
buffer += String(chunk);
|
|
127
|
+
const newlineIndex = buffer.indexOf("\n");
|
|
128
|
+
if (newlineIndex < 0) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const payload = buffer.slice(0, newlineIndex);
|
|
132
|
+
const remaining = buffer.slice(newlineIndex + 1);
|
|
133
|
+
if (remaining.trim().length > 0) {
|
|
134
|
+
this.options.logger?.info(
|
|
135
|
+
`[FormSpec] Ignoring extra semantic query payload data for ${this.runtimePaths.workspaceRoot}`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
buffer = remaining;
|
|
139
|
+
this.respondToSocket(socket, payload);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
await new Promise((resolve, reject) => {
|
|
143
|
+
const handleError = (error) => {
|
|
144
|
+
reject(error);
|
|
145
|
+
};
|
|
146
|
+
this.server?.once("error", handleError);
|
|
147
|
+
this.server?.listen(this.runtimePaths.endpoint.address, () => {
|
|
148
|
+
this.server?.off("error", handleError);
|
|
149
|
+
resolve();
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
await this.writeManifest();
|
|
153
|
+
}
|
|
154
|
+
async stop() {
|
|
155
|
+
for (const timer of this.refreshTimers.values()) {
|
|
156
|
+
clearTimeout(timer);
|
|
157
|
+
}
|
|
158
|
+
this.refreshTimers.clear();
|
|
159
|
+
this.snapshotCache.clear();
|
|
160
|
+
const server = this.server;
|
|
161
|
+
this.server = null;
|
|
162
|
+
if (server?.listening === true) {
|
|
163
|
+
await new Promise((resolve, reject) => {
|
|
164
|
+
server.close((error) => {
|
|
165
|
+
if (error === void 0) {
|
|
166
|
+
resolve();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
reject(error);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
await this.cleanupRuntimeArtifacts();
|
|
174
|
+
}
|
|
175
|
+
scheduleSnapshotRefresh(filePath) {
|
|
176
|
+
const existing = this.refreshTimers.get(filePath);
|
|
177
|
+
if (existing !== void 0) {
|
|
178
|
+
clearTimeout(existing);
|
|
179
|
+
}
|
|
180
|
+
const timer = setTimeout(() => {
|
|
181
|
+
try {
|
|
182
|
+
this.getFileSnapshot(filePath);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
this.options.logger?.info(
|
|
185
|
+
`[FormSpec] Failed to refresh semantic snapshot for ${filePath}: ${String(error)}`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
this.refreshTimers.delete(filePath);
|
|
189
|
+
}, this.options.snapshotDebounceMs ?? 250);
|
|
190
|
+
this.refreshTimers.set(filePath, timer);
|
|
191
|
+
}
|
|
192
|
+
handleQuery(query) {
|
|
193
|
+
switch (query.kind) {
|
|
194
|
+
case "health":
|
|
195
|
+
return {
|
|
196
|
+
protocolVersion: import_analysis2.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
197
|
+
kind: "health",
|
|
198
|
+
manifest: this.manifest
|
|
199
|
+
};
|
|
200
|
+
case "completion": {
|
|
201
|
+
const environment = this.getSourceEnvironment(query.filePath);
|
|
202
|
+
if (environment === null) {
|
|
203
|
+
return {
|
|
204
|
+
protocolVersion: import_analysis2.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
205
|
+
kind: "error",
|
|
206
|
+
error: `Unable to resolve TypeScript source file for ${query.filePath}`
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
const declaration = (0, import_analysis2.findDeclarationForCommentOffset)(environment.sourceFile, query.offset);
|
|
210
|
+
const placement = declaration === null ? null : (0, import_analysis2.resolveDeclarationPlacement)(declaration);
|
|
211
|
+
const subjectType = declaration === null ? void 0 : (0, import_analysis2.getSubjectType)(declaration, environment.checker);
|
|
212
|
+
const context = (0, import_analysis2.getSemanticCommentCompletionContextAtOffset)(
|
|
213
|
+
environment.sourceFile.text,
|
|
214
|
+
query.offset,
|
|
215
|
+
{
|
|
216
|
+
checker: environment.checker,
|
|
217
|
+
...placement === null ? {} : { placement },
|
|
218
|
+
...subjectType === void 0 ? {} : { subjectType }
|
|
219
|
+
}
|
|
220
|
+
);
|
|
221
|
+
return {
|
|
222
|
+
protocolVersion: import_analysis2.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
223
|
+
kind: "completion",
|
|
224
|
+
sourceHash: (0, import_analysis2.computeFormSpecTextHash)(environment.sourceFile.text),
|
|
225
|
+
context: (0, import_analysis2.serializeCompletionContext)(context)
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
case "hover": {
|
|
229
|
+
const environment = this.getSourceEnvironment(query.filePath);
|
|
230
|
+
if (environment === null) {
|
|
231
|
+
return {
|
|
232
|
+
protocolVersion: import_analysis2.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
233
|
+
kind: "error",
|
|
234
|
+
error: `Unable to resolve TypeScript source file for ${query.filePath}`
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
const declaration = (0, import_analysis2.findDeclarationForCommentOffset)(environment.sourceFile, query.offset);
|
|
238
|
+
const placement = declaration === null ? null : (0, import_analysis2.resolveDeclarationPlacement)(declaration);
|
|
239
|
+
const subjectType = declaration === null ? void 0 : (0, import_analysis2.getSubjectType)(declaration, environment.checker);
|
|
240
|
+
const hover = (0, import_analysis2.getCommentHoverInfoAtOffset)(environment.sourceFile.text, query.offset, {
|
|
241
|
+
checker: environment.checker,
|
|
242
|
+
...placement === null ? {} : { placement },
|
|
243
|
+
...subjectType === void 0 ? {} : { subjectType }
|
|
244
|
+
});
|
|
245
|
+
return {
|
|
246
|
+
protocolVersion: import_analysis2.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
247
|
+
kind: "hover",
|
|
248
|
+
sourceHash: (0, import_analysis2.computeFormSpecTextHash)(environment.sourceFile.text),
|
|
249
|
+
hover: (0, import_analysis2.serializeHoverInfo)(hover)
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
case "diagnostics": {
|
|
253
|
+
const snapshot = this.getFileSnapshot(query.filePath);
|
|
254
|
+
return {
|
|
255
|
+
protocolVersion: import_analysis2.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
256
|
+
kind: "diagnostics",
|
|
257
|
+
sourceHash: snapshot.sourceHash,
|
|
258
|
+
diagnostics: snapshot.diagnostics
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
case "file-snapshot":
|
|
262
|
+
return {
|
|
263
|
+
protocolVersion: import_analysis2.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
264
|
+
kind: "file-snapshot",
|
|
265
|
+
snapshot: this.getFileSnapshot(query.filePath)
|
|
266
|
+
};
|
|
267
|
+
default: {
|
|
268
|
+
throw new Error(`Unhandled semantic query: ${JSON.stringify(query)}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
respondToSocket(socket, payload) {
|
|
273
|
+
try {
|
|
274
|
+
const query = JSON.parse(payload);
|
|
275
|
+
if (!(0, import_analysis2.isFormSpecSemanticQuery)(query)) {
|
|
276
|
+
throw new Error("Invalid FormSpec semantic query payload");
|
|
277
|
+
}
|
|
278
|
+
const response = this.handleQuery(query);
|
|
279
|
+
socket.end(`${JSON.stringify(response)}
|
|
280
|
+
`);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
socket.end(
|
|
283
|
+
`${JSON.stringify({
|
|
284
|
+
protocolVersion: import_analysis2.FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
285
|
+
kind: "error",
|
|
286
|
+
error: error instanceof Error ? error.message : String(error)
|
|
287
|
+
})}
|
|
288
|
+
`
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
async writeManifest() {
|
|
293
|
+
const tempManifestPath = `${this.runtimePaths.manifestPath}.tmp`;
|
|
294
|
+
await import_promises.default.writeFile(tempManifestPath, `${JSON.stringify(this.manifest, null, 2)}
|
|
295
|
+
`, "utf8");
|
|
296
|
+
await import_promises.default.rename(tempManifestPath, this.runtimePaths.manifestPath);
|
|
297
|
+
}
|
|
298
|
+
async cleanupRuntimeArtifacts() {
|
|
299
|
+
await import_promises.default.rm(this.runtimePaths.manifestPath, { force: true });
|
|
300
|
+
if (this.runtimePaths.endpoint.kind === "unix-socket") {
|
|
301
|
+
await import_promises.default.rm(this.runtimePaths.endpoint.address, { force: true });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
getSourceEnvironment(filePath) {
|
|
305
|
+
const program = this.options.getProgram();
|
|
306
|
+
if (program === void 0) {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
const sourceFile = program.getSourceFile(filePath);
|
|
310
|
+
if (sourceFile === void 0) {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
sourceFile,
|
|
315
|
+
checker: program.getTypeChecker()
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
getFileSnapshot(filePath) {
|
|
319
|
+
const environment = this.getSourceEnvironment(filePath);
|
|
320
|
+
if (environment === null) {
|
|
321
|
+
return {
|
|
322
|
+
filePath,
|
|
323
|
+
sourceHash: "",
|
|
324
|
+
generatedAt: this.getNow().toISOString(),
|
|
325
|
+
comments: [],
|
|
326
|
+
diagnostics: [
|
|
327
|
+
{
|
|
328
|
+
code: "MISSING_SOURCE_FILE",
|
|
329
|
+
message: `Unable to resolve TypeScript source file for ${filePath}`,
|
|
330
|
+
range: { start: 0, end: 0 },
|
|
331
|
+
severity: "warning"
|
|
332
|
+
}
|
|
333
|
+
]
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
const sourceHash = (0, import_analysis2.computeFormSpecTextHash)(environment.sourceFile.text);
|
|
337
|
+
const cached = this.snapshotCache.get(filePath);
|
|
338
|
+
if (cached?.sourceHash === sourceHash) {
|
|
339
|
+
return cached.snapshot;
|
|
340
|
+
}
|
|
341
|
+
const snapshot = (0, import_analysis2.buildFormSpecAnalysisFileSnapshot)(environment.sourceFile, {
|
|
342
|
+
checker: environment.checker
|
|
343
|
+
});
|
|
344
|
+
this.snapshotCache.set(filePath, {
|
|
345
|
+
sourceHash,
|
|
346
|
+
snapshot
|
|
347
|
+
});
|
|
348
|
+
return snapshot;
|
|
349
|
+
}
|
|
350
|
+
getNow() {
|
|
351
|
+
return this.options.now?.() ?? /* @__PURE__ */ new Date();
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
function createLanguageServiceProxy(languageService, semanticService) {
|
|
355
|
+
const wrapWithSnapshotRefresh = (fn) => {
|
|
356
|
+
return (fileName, ...args) => {
|
|
357
|
+
semanticService.scheduleSnapshotRefresh(fileName);
|
|
358
|
+
return fn(fileName, ...args);
|
|
359
|
+
};
|
|
360
|
+
};
|
|
361
|
+
const getSemanticDiagnostics = wrapWithSnapshotRefresh(
|
|
362
|
+
(fileName) => languageService.getSemanticDiagnostics(fileName)
|
|
363
|
+
);
|
|
364
|
+
const getCompletionsAtPosition = wrapWithSnapshotRefresh(
|
|
365
|
+
(fileName, position, options) => languageService.getCompletionsAtPosition(fileName, position, options)
|
|
366
|
+
);
|
|
367
|
+
const getQuickInfoAtPosition = wrapWithSnapshotRefresh(
|
|
368
|
+
(fileName, position) => languageService.getQuickInfoAtPosition(fileName, position)
|
|
369
|
+
);
|
|
370
|
+
return new Proxy(languageService, {
|
|
371
|
+
get(target, property, receiver) {
|
|
372
|
+
switch (property) {
|
|
373
|
+
case "getSemanticDiagnostics":
|
|
374
|
+
return getSemanticDiagnostics;
|
|
375
|
+
case "getCompletionsAtPosition":
|
|
376
|
+
return getCompletionsAtPosition;
|
|
377
|
+
case "getQuickInfoAtPosition":
|
|
378
|
+
return getQuickInfoAtPosition;
|
|
379
|
+
default:
|
|
380
|
+
return Reflect.get(target, property, receiver);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// src/index.ts
|
|
387
|
+
var services = /* @__PURE__ */ new Map();
|
|
388
|
+
function formatPluginError(error) {
|
|
389
|
+
return error instanceof Error ? error.stack ?? error.message : String(error);
|
|
390
|
+
}
|
|
391
|
+
function getOrCreateService(info, typescriptVersion) {
|
|
392
|
+
const workspaceRoot = info.project.getCurrentDirectory();
|
|
393
|
+
const existing = services.get(workspaceRoot);
|
|
394
|
+
if (existing !== void 0) {
|
|
395
|
+
existing.referenceCount += 1;
|
|
396
|
+
attachProjectCloseHandler(info, workspaceRoot, existing);
|
|
397
|
+
return existing.service;
|
|
398
|
+
}
|
|
399
|
+
const service = new FormSpecPluginService({
|
|
400
|
+
workspaceRoot,
|
|
401
|
+
typescriptVersion,
|
|
402
|
+
getProgram: () => info.languageService.getProgram(),
|
|
403
|
+
logger: info.project.projectService.logger
|
|
404
|
+
});
|
|
405
|
+
const serviceEntry = {
|
|
406
|
+
service,
|
|
407
|
+
referenceCount: 1
|
|
408
|
+
};
|
|
409
|
+
attachProjectCloseHandler(info, workspaceRoot, serviceEntry);
|
|
410
|
+
service.start().catch((error) => {
|
|
411
|
+
info.project.projectService.logger.info(
|
|
412
|
+
`[FormSpec] Plugin service failed to start for ${workspaceRoot}: ${formatPluginError(error)}`
|
|
413
|
+
);
|
|
414
|
+
services.delete(workspaceRoot);
|
|
415
|
+
});
|
|
416
|
+
services.set(workspaceRoot, serviceEntry);
|
|
417
|
+
return service;
|
|
418
|
+
}
|
|
419
|
+
function attachProjectCloseHandler(info, workspaceRoot, serviceEntry) {
|
|
420
|
+
const originalClose = info.project.close.bind(info.project);
|
|
421
|
+
let closed = false;
|
|
422
|
+
info.project.close = () => {
|
|
423
|
+
if (closed) {
|
|
424
|
+
originalClose();
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
closed = true;
|
|
428
|
+
serviceEntry.referenceCount -= 1;
|
|
429
|
+
if (serviceEntry.referenceCount <= 0) {
|
|
430
|
+
services.delete(workspaceRoot);
|
|
431
|
+
void serviceEntry.service.stop().catch((error) => {
|
|
432
|
+
info.project.projectService.logger.info(
|
|
433
|
+
`[FormSpec] Failed to stop plugin service for ${workspaceRoot}: ${formatPluginError(error)}`
|
|
434
|
+
);
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
originalClose();
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
function init(modules) {
|
|
441
|
+
const typescriptVersion = modules.typescript.version;
|
|
442
|
+
return {
|
|
443
|
+
create(info) {
|
|
444
|
+
const service = getOrCreateService(info, typescriptVersion);
|
|
445
|
+
return createLanguageServiceProxy(info.languageService, service);
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
450
|
+
0 && (module.exports = {
|
|
451
|
+
init
|
|
452
|
+
});
|
|
453
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/service.ts","../src/workspace.ts"],"sourcesContent":["import type * as tsServer from \"typescript/lib/tsserverlibrary.js\";\nimport { createLanguageServiceProxy, FormSpecPluginService } from \"./service.js\";\n\ninterface ServiceEntry {\n readonly service: FormSpecPluginService;\n referenceCount: number;\n}\n\nconst services = new Map<string, ServiceEntry>();\n\nfunction formatPluginError(error: unknown): string {\n return error instanceof Error ? (error.stack ?? error.message) : String(error);\n}\n\nfunction getOrCreateService(\n info: tsServer.server.PluginCreateInfo,\n typescriptVersion: string\n): FormSpecPluginService {\n const workspaceRoot = info.project.getCurrentDirectory();\n const existing = services.get(workspaceRoot);\n if (existing !== undefined) {\n existing.referenceCount += 1;\n attachProjectCloseHandler(info, workspaceRoot, existing);\n return existing.service;\n }\n\n const service = new FormSpecPluginService({\n workspaceRoot,\n typescriptVersion,\n getProgram: () => info.languageService.getProgram(),\n logger: info.project.projectService.logger,\n });\n\n const serviceEntry: ServiceEntry = {\n service,\n referenceCount: 1,\n };\n attachProjectCloseHandler(info, workspaceRoot, serviceEntry);\n\n service.start().catch((error: unknown) => {\n info.project.projectService.logger.info(\n `[FormSpec] Plugin service failed to start for ${workspaceRoot}: ${formatPluginError(error)}`\n );\n services.delete(workspaceRoot);\n });\n services.set(workspaceRoot, serviceEntry);\n return service;\n}\n\nfunction attachProjectCloseHandler(\n info: tsServer.server.PluginCreateInfo,\n workspaceRoot: string,\n serviceEntry: ServiceEntry\n): void {\n const originalClose = info.project.close.bind(info.project);\n let closed = false;\n\n info.project.close = () => {\n if (closed) {\n originalClose();\n return;\n }\n\n closed = true;\n serviceEntry.referenceCount -= 1;\n if (serviceEntry.referenceCount <= 0) {\n services.delete(workspaceRoot);\n void serviceEntry.service.stop().catch((error: unknown) => {\n info.project.projectService.logger.info(\n `[FormSpec] Failed to stop plugin service for ${workspaceRoot}: ${formatPluginError(error)}`\n );\n });\n }\n originalClose();\n };\n}\n\n/**\n * Initializes the FormSpec TypeScript language service plugin.\n *\n * @public\n */\nexport function init(modules: {\n readonly typescript: typeof tsServer;\n}): tsServer.server.PluginModule {\n const typescriptVersion = modules.typescript.version;\n return {\n create(info) {\n const service = getOrCreateService(info, typescriptVersion);\n return createLanguageServiceProxy(info.languageService, service);\n },\n };\n}\n","import fs from \"node:fs/promises\";\nimport net from \"node:net\";\nimport * as ts from \"typescript\";\nimport {\n FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n buildFormSpecAnalysisFileSnapshot,\n computeFormSpecTextHash,\n findDeclarationForCommentOffset,\n getSubjectType,\n getCommentHoverInfoAtOffset,\n getSemanticCommentCompletionContextAtOffset,\n isFormSpecSemanticQuery,\n resolveDeclarationPlacement,\n serializeCompletionContext,\n serializeHoverInfo,\n type BuildFormSpecAnalysisFileSnapshotOptions,\n type FormSpecAnalysisFileSnapshot,\n type FormSpecAnalysisManifest,\n type FormSpecSemanticQuery,\n type FormSpecSemanticResponse,\n} from \"@formspec/analysis\";\nimport {\n createFormSpecAnalysisManifest,\n getFormSpecWorkspaceRuntimePaths,\n type FormSpecWorkspaceRuntimePaths,\n} from \"./workspace.js\";\n\ninterface LoggerLike {\n info(message: string): void;\n}\n\nexport interface FormSpecPluginServiceOptions {\n readonly workspaceRoot: string;\n readonly typescriptVersion: string;\n readonly getProgram: () => ts.Program | undefined;\n readonly logger?: LoggerLike;\n readonly snapshotDebounceMs?: number;\n readonly now?: () => Date;\n}\n\ninterface CachedFileSnapshot {\n readonly sourceHash: string;\n readonly snapshot: FormSpecAnalysisFileSnapshot;\n}\n\nexport class FormSpecPluginService {\n private readonly manifest: FormSpecAnalysisManifest;\n private readonly runtimePaths: FormSpecWorkspaceRuntimePaths;\n private readonly snapshotCache = new Map<string, CachedFileSnapshot>();\n private readonly refreshTimers = new Map<string, NodeJS.Timeout>();\n private server: net.Server | null = null;\n\n public constructor(private readonly options: FormSpecPluginServiceOptions) {\n this.runtimePaths = getFormSpecWorkspaceRuntimePaths(options.workspaceRoot);\n this.manifest = createFormSpecAnalysisManifest(\n options.workspaceRoot,\n options.typescriptVersion,\n Date.now()\n );\n }\n\n public getManifest(): FormSpecAnalysisManifest {\n return this.manifest;\n }\n\n public async start(): Promise<void> {\n if (this.server !== null) {\n return;\n }\n\n await fs.mkdir(this.runtimePaths.runtimeDirectory, { recursive: true });\n if (this.runtimePaths.endpoint.kind === \"unix-socket\") {\n await fs.rm(this.runtimePaths.endpoint.address, { force: true });\n }\n\n this.server = net.createServer((socket) => {\n let buffer = \"\";\n socket.setEncoding(\"utf8\");\n socket.on(\"data\", (chunk) => {\n buffer += String(chunk);\n const newlineIndex = buffer.indexOf(\"\\n\");\n if (newlineIndex < 0) {\n return;\n }\n\n const payload = buffer.slice(0, newlineIndex);\n const remaining = buffer.slice(newlineIndex + 1);\n if (remaining.trim().length > 0) {\n this.options.logger?.info(\n `[FormSpec] Ignoring extra semantic query payload data for ${this.runtimePaths.workspaceRoot}`\n );\n }\n buffer = remaining;\n // The FormSpec IPC transport is intentionally one-request-per-connection.\n this.respondToSocket(socket, payload);\n });\n });\n\n await new Promise<void>((resolve, reject) => {\n const handleError = (error: Error) => {\n reject(error);\n };\n this.server?.once(\"error\", handleError);\n this.server?.listen(this.runtimePaths.endpoint.address, () => {\n this.server?.off(\"error\", handleError);\n resolve();\n });\n });\n\n await this.writeManifest();\n }\n\n public async stop(): Promise<void> {\n for (const timer of this.refreshTimers.values()) {\n clearTimeout(timer);\n }\n this.refreshTimers.clear();\n this.snapshotCache.clear();\n\n const server = this.server;\n this.server = null;\n if (server?.listening === true) {\n await new Promise<void>((resolve, reject) => {\n server.close((error) => {\n if (error === undefined) {\n resolve();\n return;\n }\n reject(error);\n });\n });\n }\n\n await this.cleanupRuntimeArtifacts();\n }\n\n public scheduleSnapshotRefresh(filePath: string): void {\n const existing = this.refreshTimers.get(filePath);\n if (existing !== undefined) {\n clearTimeout(existing);\n }\n\n const timer = setTimeout(() => {\n try {\n this.getFileSnapshot(filePath);\n } catch (error: unknown) {\n this.options.logger?.info(\n `[FormSpec] Failed to refresh semantic snapshot for ${filePath}: ${String(error)}`\n );\n }\n this.refreshTimers.delete(filePath);\n }, this.options.snapshotDebounceMs ?? 250);\n\n this.refreshTimers.set(filePath, timer);\n }\n\n public handleQuery(query: FormSpecSemanticQuery): FormSpecSemanticResponse {\n switch (query.kind) {\n case \"health\":\n return {\n protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n kind: \"health\",\n manifest: this.manifest,\n };\n case \"completion\": {\n const environment = this.getSourceEnvironment(query.filePath);\n if (environment === null) {\n return {\n protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n kind: \"error\",\n error: `Unable to resolve TypeScript source file for ${query.filePath}`,\n };\n }\n\n const declaration = findDeclarationForCommentOffset(environment.sourceFile, query.offset);\n const placement = declaration === null ? null : resolveDeclarationPlacement(declaration);\n const subjectType =\n declaration === null ? undefined : getSubjectType(declaration, environment.checker);\n const context = getSemanticCommentCompletionContextAtOffset(\n environment.sourceFile.text,\n query.offset,\n {\n checker: environment.checker,\n ...(placement === null ? {} : { placement }),\n ...(subjectType === undefined ? {} : { subjectType }),\n }\n );\n\n return {\n protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n kind: \"completion\",\n sourceHash: computeFormSpecTextHash(environment.sourceFile.text),\n context: serializeCompletionContext(context),\n };\n }\n case \"hover\": {\n const environment = this.getSourceEnvironment(query.filePath);\n if (environment === null) {\n return {\n protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n kind: \"error\",\n error: `Unable to resolve TypeScript source file for ${query.filePath}`,\n };\n }\n\n const declaration = findDeclarationForCommentOffset(environment.sourceFile, query.offset);\n const placement = declaration === null ? null : resolveDeclarationPlacement(declaration);\n const subjectType =\n declaration === null ? undefined : getSubjectType(declaration, environment.checker);\n const hover = getCommentHoverInfoAtOffset(environment.sourceFile.text, query.offset, {\n checker: environment.checker,\n ...(placement === null ? {} : { placement }),\n ...(subjectType === undefined ? {} : { subjectType }),\n });\n\n return {\n protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n kind: \"hover\",\n sourceHash: computeFormSpecTextHash(environment.sourceFile.text),\n hover: serializeHoverInfo(hover),\n };\n }\n case \"diagnostics\": {\n const snapshot = this.getFileSnapshot(query.filePath);\n return {\n protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n kind: \"diagnostics\",\n sourceHash: snapshot.sourceHash,\n diagnostics: snapshot.diagnostics,\n };\n }\n case \"file-snapshot\":\n return {\n protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n kind: \"file-snapshot\",\n snapshot: this.getFileSnapshot(query.filePath),\n };\n default: {\n throw new Error(`Unhandled semantic query: ${JSON.stringify(query)}`);\n }\n }\n }\n\n private respondToSocket(socket: net.Socket, payload: string): void {\n try {\n const query = JSON.parse(payload) as unknown;\n if (!isFormSpecSemanticQuery(query)) {\n throw new Error(\"Invalid FormSpec semantic query payload\");\n }\n const response = this.handleQuery(query);\n socket.end(`${JSON.stringify(response)}\\n`);\n } catch (error) {\n socket.end(\n `${JSON.stringify({\n protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n kind: \"error\",\n error: error instanceof Error ? error.message : String(error),\n } satisfies FormSpecSemanticResponse)}\\n`\n );\n }\n }\n\n private async writeManifest(): Promise<void> {\n const tempManifestPath = `${this.runtimePaths.manifestPath}.tmp`;\n await fs.writeFile(tempManifestPath, `${JSON.stringify(this.manifest, null, 2)}\\n`, \"utf8\");\n await fs.rename(tempManifestPath, this.runtimePaths.manifestPath);\n }\n\n private async cleanupRuntimeArtifacts(): Promise<void> {\n await fs.rm(this.runtimePaths.manifestPath, { force: true });\n if (this.runtimePaths.endpoint.kind === \"unix-socket\") {\n await fs.rm(this.runtimePaths.endpoint.address, { force: true });\n }\n }\n\n private getSourceEnvironment(filePath: string): {\n readonly sourceFile: ts.SourceFile;\n readonly checker: ts.TypeChecker;\n } | null {\n const program = this.options.getProgram();\n if (program === undefined) {\n return null;\n }\n\n const sourceFile = program.getSourceFile(filePath);\n if (sourceFile === undefined) {\n return null;\n }\n\n return {\n sourceFile,\n checker: program.getTypeChecker(),\n };\n }\n\n private getFileSnapshot(filePath: string): FormSpecAnalysisFileSnapshot {\n const environment = this.getSourceEnvironment(filePath);\n if (environment === null) {\n return {\n filePath,\n sourceHash: \"\",\n generatedAt: this.getNow().toISOString(),\n comments: [],\n diagnostics: [\n {\n code: \"MISSING_SOURCE_FILE\",\n message: `Unable to resolve TypeScript source file for ${filePath}`,\n range: { start: 0, end: 0 },\n severity: \"warning\",\n },\n ],\n };\n }\n\n const sourceHash = computeFormSpecTextHash(environment.sourceFile.text);\n const cached = this.snapshotCache.get(filePath);\n if (cached?.sourceHash === sourceHash) {\n return cached.snapshot;\n }\n\n const snapshot = buildFormSpecAnalysisFileSnapshot(environment.sourceFile, {\n checker: environment.checker,\n } satisfies BuildFormSpecAnalysisFileSnapshotOptions);\n this.snapshotCache.set(filePath, {\n sourceHash,\n snapshot,\n });\n return snapshot;\n }\n\n private getNow(): Date {\n return this.options.now?.() ?? new Date();\n }\n}\n\nexport function createLanguageServiceProxy(\n languageService: ts.LanguageService,\n semanticService: FormSpecPluginService\n): ts.LanguageService {\n const wrapWithSnapshotRefresh = <Args extends readonly unknown[], Result>(\n fn: (fileName: string, ...args: Args) => Result\n ) => {\n return (fileName: string, ...args: Args): Result => {\n semanticService.scheduleSnapshotRefresh(fileName);\n return fn(fileName, ...args);\n };\n };\n\n // The plugin keeps semantic snapshots fresh for the lightweight LSP. The\n // underlying tsserver results still come from the original language service.\n const getSemanticDiagnostics = wrapWithSnapshotRefresh((fileName) =>\n languageService.getSemanticDiagnostics(fileName)\n );\n\n const getCompletionsAtPosition = wrapWithSnapshotRefresh(\n (fileName: string, position: number, options: ts.GetCompletionsAtPositionOptions | undefined) =>\n languageService.getCompletionsAtPosition(fileName, position, options)\n );\n\n const getQuickInfoAtPosition = wrapWithSnapshotRefresh((fileName, position: number) =>\n languageService.getQuickInfoAtPosition(fileName, position)\n );\n\n return new Proxy(languageService, {\n get(target, property, receiver) {\n switch (property) {\n case \"getSemanticDiagnostics\":\n return getSemanticDiagnostics;\n case \"getCompletionsAtPosition\":\n return getCompletionsAtPosition;\n case \"getQuickInfoAtPosition\":\n return getQuickInfoAtPosition;\n default:\n return Reflect.get(target, property, receiver) as unknown;\n }\n },\n });\n}\n","import os from \"node:os\";\nimport path from \"node:path\";\nimport {\n FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n FORMSPEC_ANALYSIS_SCHEMA_VERSION,\n getFormSpecManifestPath,\n getFormSpecWorkspaceId,\n getFormSpecWorkspaceRuntimeDirectory,\n type FormSpecAnalysisManifest,\n type FormSpecIpcEndpoint,\n} from \"@formspec/analysis\";\n\nexport interface FormSpecWorkspaceRuntimePaths {\n readonly workspaceRoot: string;\n readonly workspaceId: string;\n readonly runtimeDirectory: string;\n readonly manifestPath: string;\n readonly endpoint: FormSpecIpcEndpoint;\n}\n\nexport function getFormSpecWorkspaceRuntimePaths(\n workspaceRoot: string,\n platform = process.platform,\n userScope = getFormSpecUserScope()\n): FormSpecWorkspaceRuntimePaths {\n const workspaceId = getFormSpecWorkspaceId(workspaceRoot);\n const runtimeDirectory = getFormSpecWorkspaceRuntimeDirectory(workspaceRoot);\n const sanitizedUserScope = sanitizeScopeSegment(userScope);\n const endpoint: FormSpecIpcEndpoint =\n platform === \"win32\"\n ? {\n kind: \"windows-pipe\",\n address: `\\\\\\\\.\\\\pipe\\\\formspec-${sanitizedUserScope}-${workspaceId}`,\n }\n : {\n kind: \"unix-socket\",\n address: path.join(os.tmpdir(), `formspec-${sanitizedUserScope}-${workspaceId}.sock`),\n };\n\n return {\n workspaceRoot,\n workspaceId,\n runtimeDirectory,\n manifestPath: getFormSpecManifestPath(workspaceRoot),\n endpoint,\n };\n}\n\nexport function createFormSpecAnalysisManifest(\n workspaceRoot: string,\n typescriptVersion: string,\n generation: number,\n extensionFingerprint = \"builtin\"\n): FormSpecAnalysisManifest {\n const paths = getFormSpecWorkspaceRuntimePaths(workspaceRoot);\n return {\n protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n analysisSchemaVersion: FORMSPEC_ANALYSIS_SCHEMA_VERSION,\n workspaceRoot,\n workspaceId: paths.workspaceId,\n endpoint: paths.endpoint,\n typescriptVersion,\n extensionFingerprint,\n generation,\n updatedAt: new Date().toISOString(),\n };\n}\n\nfunction getFormSpecUserScope(): string {\n try {\n return sanitizeScopeSegment(os.userInfo().username);\n } catch {\n return sanitizeScopeSegment(\n process.env[\"USER\"] ?? process.env[\"USERNAME\"] ?? process.env[\"LOGNAME\"] ?? \"formspec\"\n );\n }\n}\n\nfunction sanitizeScopeSegment(value: string): string {\n const trimmed = value.trim().toLowerCase();\n const sanitized = trimmed.replace(/[^a-z0-9_-]+/gu, \"-\").replace(/-+/gu, \"-\");\n return sanitized.length > 0 ? sanitized : \"formspec\";\n}\n"],"mappings":"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","names":["import_analysis","path","os","fs","net"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type * as tsServer from "typescript/lib/tsserverlibrary.js";
|
|
2
|
+
/**
|
|
3
|
+
* Initializes the FormSpec TypeScript language service plugin.
|
|
4
|
+
*
|
|
5
|
+
* @public
|
|
6
|
+
*/
|
|
7
|
+
export declare function init(modules: {
|
|
8
|
+
readonly typescript: typeof tsServer;
|
|
9
|
+
}): tsServer.server.PluginModule;
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,QAAQ,MAAM,mCAAmC,CAAC;AA6EnE;;;;GAIG;AACH,wBAAgB,IAAI,CAAC,OAAO,EAAE;IAC5B,QAAQ,CAAC,UAAU,EAAE,OAAO,QAAQ,CAAC;CACtC,GAAG,QAAQ,CAAC,MAAM,CAAC,YAAY,CAQ/B"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
// src/service.ts
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import net from "net";
|
|
4
|
+
import "typescript";
|
|
5
|
+
import {
|
|
6
|
+
FORMSPEC_ANALYSIS_PROTOCOL_VERSION as FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
|
|
7
|
+
buildFormSpecAnalysisFileSnapshot,
|
|
8
|
+
computeFormSpecTextHash,
|
|
9
|
+
findDeclarationForCommentOffset,
|
|
10
|
+
getSubjectType,
|
|
11
|
+
getCommentHoverInfoAtOffset,
|
|
12
|
+
getSemanticCommentCompletionContextAtOffset,
|
|
13
|
+
isFormSpecSemanticQuery,
|
|
14
|
+
resolveDeclarationPlacement,
|
|
15
|
+
serializeCompletionContext,
|
|
16
|
+
serializeHoverInfo
|
|
17
|
+
} from "@formspec/analysis";
|
|
18
|
+
|
|
19
|
+
// src/workspace.ts
|
|
20
|
+
import os from "os";
|
|
21
|
+
import path from "path";
|
|
22
|
+
import {
|
|
23
|
+
FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
24
|
+
FORMSPEC_ANALYSIS_SCHEMA_VERSION,
|
|
25
|
+
getFormSpecManifestPath,
|
|
26
|
+
getFormSpecWorkspaceId,
|
|
27
|
+
getFormSpecWorkspaceRuntimeDirectory
|
|
28
|
+
} from "@formspec/analysis";
|
|
29
|
+
function getFormSpecWorkspaceRuntimePaths(workspaceRoot, platform = process.platform, userScope = getFormSpecUserScope()) {
|
|
30
|
+
const workspaceId = getFormSpecWorkspaceId(workspaceRoot);
|
|
31
|
+
const runtimeDirectory = getFormSpecWorkspaceRuntimeDirectory(workspaceRoot);
|
|
32
|
+
const sanitizedUserScope = sanitizeScopeSegment(userScope);
|
|
33
|
+
const endpoint = platform === "win32" ? {
|
|
34
|
+
kind: "windows-pipe",
|
|
35
|
+
address: `\\\\.\\pipe\\formspec-${sanitizedUserScope}-${workspaceId}`
|
|
36
|
+
} : {
|
|
37
|
+
kind: "unix-socket",
|
|
38
|
+
address: path.join(os.tmpdir(), `formspec-${sanitizedUserScope}-${workspaceId}.sock`)
|
|
39
|
+
};
|
|
40
|
+
return {
|
|
41
|
+
workspaceRoot,
|
|
42
|
+
workspaceId,
|
|
43
|
+
runtimeDirectory,
|
|
44
|
+
manifestPath: getFormSpecManifestPath(workspaceRoot),
|
|
45
|
+
endpoint
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function createFormSpecAnalysisManifest(workspaceRoot, typescriptVersion, generation, extensionFingerprint = "builtin") {
|
|
49
|
+
const paths = getFormSpecWorkspaceRuntimePaths(workspaceRoot);
|
|
50
|
+
return {
|
|
51
|
+
protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,
|
|
52
|
+
analysisSchemaVersion: FORMSPEC_ANALYSIS_SCHEMA_VERSION,
|
|
53
|
+
workspaceRoot,
|
|
54
|
+
workspaceId: paths.workspaceId,
|
|
55
|
+
endpoint: paths.endpoint,
|
|
56
|
+
typescriptVersion,
|
|
57
|
+
extensionFingerprint,
|
|
58
|
+
generation,
|
|
59
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function getFormSpecUserScope() {
|
|
63
|
+
try {
|
|
64
|
+
return sanitizeScopeSegment(os.userInfo().username);
|
|
65
|
+
} catch {
|
|
66
|
+
return sanitizeScopeSegment(
|
|
67
|
+
process.env["USER"] ?? process.env["USERNAME"] ?? process.env["LOGNAME"] ?? "formspec"
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function sanitizeScopeSegment(value) {
|
|
72
|
+
const trimmed = value.trim().toLowerCase();
|
|
73
|
+
const sanitized = trimmed.replace(/[^a-z0-9_-]+/gu, "-").replace(/-+/gu, "-");
|
|
74
|
+
return sanitized.length > 0 ? sanitized : "formspec";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// src/service.ts
|
|
78
|
+
var FormSpecPluginService = class {
|
|
79
|
+
constructor(options) {
|
|
80
|
+
this.options = options;
|
|
81
|
+
this.runtimePaths = getFormSpecWorkspaceRuntimePaths(options.workspaceRoot);
|
|
82
|
+
this.manifest = createFormSpecAnalysisManifest(
|
|
83
|
+
options.workspaceRoot,
|
|
84
|
+
options.typescriptVersion,
|
|
85
|
+
Date.now()
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
manifest;
|
|
89
|
+
runtimePaths;
|
|
90
|
+
snapshotCache = /* @__PURE__ */ new Map();
|
|
91
|
+
refreshTimers = /* @__PURE__ */ new Map();
|
|
92
|
+
server = null;
|
|
93
|
+
getManifest() {
|
|
94
|
+
return this.manifest;
|
|
95
|
+
}
|
|
96
|
+
async start() {
|
|
97
|
+
if (this.server !== null) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
await fs.mkdir(this.runtimePaths.runtimeDirectory, { recursive: true });
|
|
101
|
+
if (this.runtimePaths.endpoint.kind === "unix-socket") {
|
|
102
|
+
await fs.rm(this.runtimePaths.endpoint.address, { force: true });
|
|
103
|
+
}
|
|
104
|
+
this.server = net.createServer((socket) => {
|
|
105
|
+
let buffer = "";
|
|
106
|
+
socket.setEncoding("utf8");
|
|
107
|
+
socket.on("data", (chunk) => {
|
|
108
|
+
buffer += String(chunk);
|
|
109
|
+
const newlineIndex = buffer.indexOf("\n");
|
|
110
|
+
if (newlineIndex < 0) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const payload = buffer.slice(0, newlineIndex);
|
|
114
|
+
const remaining = buffer.slice(newlineIndex + 1);
|
|
115
|
+
if (remaining.trim().length > 0) {
|
|
116
|
+
this.options.logger?.info(
|
|
117
|
+
`[FormSpec] Ignoring extra semantic query payload data for ${this.runtimePaths.workspaceRoot}`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
buffer = remaining;
|
|
121
|
+
this.respondToSocket(socket, payload);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
await new Promise((resolve, reject) => {
|
|
125
|
+
const handleError = (error) => {
|
|
126
|
+
reject(error);
|
|
127
|
+
};
|
|
128
|
+
this.server?.once("error", handleError);
|
|
129
|
+
this.server?.listen(this.runtimePaths.endpoint.address, () => {
|
|
130
|
+
this.server?.off("error", handleError);
|
|
131
|
+
resolve();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
await this.writeManifest();
|
|
135
|
+
}
|
|
136
|
+
async stop() {
|
|
137
|
+
for (const timer of this.refreshTimers.values()) {
|
|
138
|
+
clearTimeout(timer);
|
|
139
|
+
}
|
|
140
|
+
this.refreshTimers.clear();
|
|
141
|
+
this.snapshotCache.clear();
|
|
142
|
+
const server = this.server;
|
|
143
|
+
this.server = null;
|
|
144
|
+
if (server?.listening === true) {
|
|
145
|
+
await new Promise((resolve, reject) => {
|
|
146
|
+
server.close((error) => {
|
|
147
|
+
if (error === void 0) {
|
|
148
|
+
resolve();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
reject(error);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
await this.cleanupRuntimeArtifacts();
|
|
156
|
+
}
|
|
157
|
+
scheduleSnapshotRefresh(filePath) {
|
|
158
|
+
const existing = this.refreshTimers.get(filePath);
|
|
159
|
+
if (existing !== void 0) {
|
|
160
|
+
clearTimeout(existing);
|
|
161
|
+
}
|
|
162
|
+
const timer = setTimeout(() => {
|
|
163
|
+
try {
|
|
164
|
+
this.getFileSnapshot(filePath);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
this.options.logger?.info(
|
|
167
|
+
`[FormSpec] Failed to refresh semantic snapshot for ${filePath}: ${String(error)}`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
this.refreshTimers.delete(filePath);
|
|
171
|
+
}, this.options.snapshotDebounceMs ?? 250);
|
|
172
|
+
this.refreshTimers.set(filePath, timer);
|
|
173
|
+
}
|
|
174
|
+
handleQuery(query) {
|
|
175
|
+
switch (query.kind) {
|
|
176
|
+
case "health":
|
|
177
|
+
return {
|
|
178
|
+
protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
|
|
179
|
+
kind: "health",
|
|
180
|
+
manifest: this.manifest
|
|
181
|
+
};
|
|
182
|
+
case "completion": {
|
|
183
|
+
const environment = this.getSourceEnvironment(query.filePath);
|
|
184
|
+
if (environment === null) {
|
|
185
|
+
return {
|
|
186
|
+
protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
|
|
187
|
+
kind: "error",
|
|
188
|
+
error: `Unable to resolve TypeScript source file for ${query.filePath}`
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
const declaration = findDeclarationForCommentOffset(environment.sourceFile, query.offset);
|
|
192
|
+
const placement = declaration === null ? null : resolveDeclarationPlacement(declaration);
|
|
193
|
+
const subjectType = declaration === null ? void 0 : getSubjectType(declaration, environment.checker);
|
|
194
|
+
const context = getSemanticCommentCompletionContextAtOffset(
|
|
195
|
+
environment.sourceFile.text,
|
|
196
|
+
query.offset,
|
|
197
|
+
{
|
|
198
|
+
checker: environment.checker,
|
|
199
|
+
...placement === null ? {} : { placement },
|
|
200
|
+
...subjectType === void 0 ? {} : { subjectType }
|
|
201
|
+
}
|
|
202
|
+
);
|
|
203
|
+
return {
|
|
204
|
+
protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
|
|
205
|
+
kind: "completion",
|
|
206
|
+
sourceHash: computeFormSpecTextHash(environment.sourceFile.text),
|
|
207
|
+
context: serializeCompletionContext(context)
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
case "hover": {
|
|
211
|
+
const environment = this.getSourceEnvironment(query.filePath);
|
|
212
|
+
if (environment === null) {
|
|
213
|
+
return {
|
|
214
|
+
protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
|
|
215
|
+
kind: "error",
|
|
216
|
+
error: `Unable to resolve TypeScript source file for ${query.filePath}`
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
const declaration = findDeclarationForCommentOffset(environment.sourceFile, query.offset);
|
|
220
|
+
const placement = declaration === null ? null : resolveDeclarationPlacement(declaration);
|
|
221
|
+
const subjectType = declaration === null ? void 0 : getSubjectType(declaration, environment.checker);
|
|
222
|
+
const hover = getCommentHoverInfoAtOffset(environment.sourceFile.text, query.offset, {
|
|
223
|
+
checker: environment.checker,
|
|
224
|
+
...placement === null ? {} : { placement },
|
|
225
|
+
...subjectType === void 0 ? {} : { subjectType }
|
|
226
|
+
});
|
|
227
|
+
return {
|
|
228
|
+
protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
|
|
229
|
+
kind: "hover",
|
|
230
|
+
sourceHash: computeFormSpecTextHash(environment.sourceFile.text),
|
|
231
|
+
hover: serializeHoverInfo(hover)
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
case "diagnostics": {
|
|
235
|
+
const snapshot = this.getFileSnapshot(query.filePath);
|
|
236
|
+
return {
|
|
237
|
+
protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
|
|
238
|
+
kind: "diagnostics",
|
|
239
|
+
sourceHash: snapshot.sourceHash,
|
|
240
|
+
diagnostics: snapshot.diagnostics
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
case "file-snapshot":
|
|
244
|
+
return {
|
|
245
|
+
protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
|
|
246
|
+
kind: "file-snapshot",
|
|
247
|
+
snapshot: this.getFileSnapshot(query.filePath)
|
|
248
|
+
};
|
|
249
|
+
default: {
|
|
250
|
+
throw new Error(`Unhandled semantic query: ${JSON.stringify(query)}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
respondToSocket(socket, payload) {
|
|
255
|
+
try {
|
|
256
|
+
const query = JSON.parse(payload);
|
|
257
|
+
if (!isFormSpecSemanticQuery(query)) {
|
|
258
|
+
throw new Error("Invalid FormSpec semantic query payload");
|
|
259
|
+
}
|
|
260
|
+
const response = this.handleQuery(query);
|
|
261
|
+
socket.end(`${JSON.stringify(response)}
|
|
262
|
+
`);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
socket.end(
|
|
265
|
+
`${JSON.stringify({
|
|
266
|
+
protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION2,
|
|
267
|
+
kind: "error",
|
|
268
|
+
error: error instanceof Error ? error.message : String(error)
|
|
269
|
+
})}
|
|
270
|
+
`
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
async writeManifest() {
|
|
275
|
+
const tempManifestPath = `${this.runtimePaths.manifestPath}.tmp`;
|
|
276
|
+
await fs.writeFile(tempManifestPath, `${JSON.stringify(this.manifest, null, 2)}
|
|
277
|
+
`, "utf8");
|
|
278
|
+
await fs.rename(tempManifestPath, this.runtimePaths.manifestPath);
|
|
279
|
+
}
|
|
280
|
+
async cleanupRuntimeArtifacts() {
|
|
281
|
+
await fs.rm(this.runtimePaths.manifestPath, { force: true });
|
|
282
|
+
if (this.runtimePaths.endpoint.kind === "unix-socket") {
|
|
283
|
+
await fs.rm(this.runtimePaths.endpoint.address, { force: true });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
getSourceEnvironment(filePath) {
|
|
287
|
+
const program = this.options.getProgram();
|
|
288
|
+
if (program === void 0) {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
const sourceFile = program.getSourceFile(filePath);
|
|
292
|
+
if (sourceFile === void 0) {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
return {
|
|
296
|
+
sourceFile,
|
|
297
|
+
checker: program.getTypeChecker()
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
getFileSnapshot(filePath) {
|
|
301
|
+
const environment = this.getSourceEnvironment(filePath);
|
|
302
|
+
if (environment === null) {
|
|
303
|
+
return {
|
|
304
|
+
filePath,
|
|
305
|
+
sourceHash: "",
|
|
306
|
+
generatedAt: this.getNow().toISOString(),
|
|
307
|
+
comments: [],
|
|
308
|
+
diagnostics: [
|
|
309
|
+
{
|
|
310
|
+
code: "MISSING_SOURCE_FILE",
|
|
311
|
+
message: `Unable to resolve TypeScript source file for ${filePath}`,
|
|
312
|
+
range: { start: 0, end: 0 },
|
|
313
|
+
severity: "warning"
|
|
314
|
+
}
|
|
315
|
+
]
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
const sourceHash = computeFormSpecTextHash(environment.sourceFile.text);
|
|
319
|
+
const cached = this.snapshotCache.get(filePath);
|
|
320
|
+
if (cached?.sourceHash === sourceHash) {
|
|
321
|
+
return cached.snapshot;
|
|
322
|
+
}
|
|
323
|
+
const snapshot = buildFormSpecAnalysisFileSnapshot(environment.sourceFile, {
|
|
324
|
+
checker: environment.checker
|
|
325
|
+
});
|
|
326
|
+
this.snapshotCache.set(filePath, {
|
|
327
|
+
sourceHash,
|
|
328
|
+
snapshot
|
|
329
|
+
});
|
|
330
|
+
return snapshot;
|
|
331
|
+
}
|
|
332
|
+
getNow() {
|
|
333
|
+
return this.options.now?.() ?? /* @__PURE__ */ new Date();
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
function createLanguageServiceProxy(languageService, semanticService) {
|
|
337
|
+
const wrapWithSnapshotRefresh = (fn) => {
|
|
338
|
+
return (fileName, ...args) => {
|
|
339
|
+
semanticService.scheduleSnapshotRefresh(fileName);
|
|
340
|
+
return fn(fileName, ...args);
|
|
341
|
+
};
|
|
342
|
+
};
|
|
343
|
+
const getSemanticDiagnostics = wrapWithSnapshotRefresh(
|
|
344
|
+
(fileName) => languageService.getSemanticDiagnostics(fileName)
|
|
345
|
+
);
|
|
346
|
+
const getCompletionsAtPosition = wrapWithSnapshotRefresh(
|
|
347
|
+
(fileName, position, options) => languageService.getCompletionsAtPosition(fileName, position, options)
|
|
348
|
+
);
|
|
349
|
+
const getQuickInfoAtPosition = wrapWithSnapshotRefresh(
|
|
350
|
+
(fileName, position) => languageService.getQuickInfoAtPosition(fileName, position)
|
|
351
|
+
);
|
|
352
|
+
return new Proxy(languageService, {
|
|
353
|
+
get(target, property, receiver) {
|
|
354
|
+
switch (property) {
|
|
355
|
+
case "getSemanticDiagnostics":
|
|
356
|
+
return getSemanticDiagnostics;
|
|
357
|
+
case "getCompletionsAtPosition":
|
|
358
|
+
return getCompletionsAtPosition;
|
|
359
|
+
case "getQuickInfoAtPosition":
|
|
360
|
+
return getQuickInfoAtPosition;
|
|
361
|
+
default:
|
|
362
|
+
return Reflect.get(target, property, receiver);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// src/index.ts
|
|
369
|
+
var services = /* @__PURE__ */ new Map();
|
|
370
|
+
function formatPluginError(error) {
|
|
371
|
+
return error instanceof Error ? error.stack ?? error.message : String(error);
|
|
372
|
+
}
|
|
373
|
+
function getOrCreateService(info, typescriptVersion) {
|
|
374
|
+
const workspaceRoot = info.project.getCurrentDirectory();
|
|
375
|
+
const existing = services.get(workspaceRoot);
|
|
376
|
+
if (existing !== void 0) {
|
|
377
|
+
existing.referenceCount += 1;
|
|
378
|
+
attachProjectCloseHandler(info, workspaceRoot, existing);
|
|
379
|
+
return existing.service;
|
|
380
|
+
}
|
|
381
|
+
const service = new FormSpecPluginService({
|
|
382
|
+
workspaceRoot,
|
|
383
|
+
typescriptVersion,
|
|
384
|
+
getProgram: () => info.languageService.getProgram(),
|
|
385
|
+
logger: info.project.projectService.logger
|
|
386
|
+
});
|
|
387
|
+
const serviceEntry = {
|
|
388
|
+
service,
|
|
389
|
+
referenceCount: 1
|
|
390
|
+
};
|
|
391
|
+
attachProjectCloseHandler(info, workspaceRoot, serviceEntry);
|
|
392
|
+
service.start().catch((error) => {
|
|
393
|
+
info.project.projectService.logger.info(
|
|
394
|
+
`[FormSpec] Plugin service failed to start for ${workspaceRoot}: ${formatPluginError(error)}`
|
|
395
|
+
);
|
|
396
|
+
services.delete(workspaceRoot);
|
|
397
|
+
});
|
|
398
|
+
services.set(workspaceRoot, serviceEntry);
|
|
399
|
+
return service;
|
|
400
|
+
}
|
|
401
|
+
function attachProjectCloseHandler(info, workspaceRoot, serviceEntry) {
|
|
402
|
+
const originalClose = info.project.close.bind(info.project);
|
|
403
|
+
let closed = false;
|
|
404
|
+
info.project.close = () => {
|
|
405
|
+
if (closed) {
|
|
406
|
+
originalClose();
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
closed = true;
|
|
410
|
+
serviceEntry.referenceCount -= 1;
|
|
411
|
+
if (serviceEntry.referenceCount <= 0) {
|
|
412
|
+
services.delete(workspaceRoot);
|
|
413
|
+
void serviceEntry.service.stop().catch((error) => {
|
|
414
|
+
info.project.projectService.logger.info(
|
|
415
|
+
`[FormSpec] Failed to stop plugin service for ${workspaceRoot}: ${formatPluginError(error)}`
|
|
416
|
+
);
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
originalClose();
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
function init(modules) {
|
|
423
|
+
const typescriptVersion = modules.typescript.version;
|
|
424
|
+
return {
|
|
425
|
+
create(info) {
|
|
426
|
+
const service = getOrCreateService(info, typescriptVersion);
|
|
427
|
+
return createLanguageServiceProxy(info.languageService, service);
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
export {
|
|
432
|
+
init
|
|
433
|
+
};
|
|
434
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/service.ts","../src/workspace.ts","../src/index.ts"],"sourcesContent":["import fs from \"node:fs/promises\";\nimport net from \"node:net\";\nimport * as ts from \"typescript\";\nimport {\n FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n buildFormSpecAnalysisFileSnapshot,\n computeFormSpecTextHash,\n findDeclarationForCommentOffset,\n getSubjectType,\n getCommentHoverInfoAtOffset,\n getSemanticCommentCompletionContextAtOffset,\n isFormSpecSemanticQuery,\n resolveDeclarationPlacement,\n serializeCompletionContext,\n serializeHoverInfo,\n type BuildFormSpecAnalysisFileSnapshotOptions,\n type FormSpecAnalysisFileSnapshot,\n type FormSpecAnalysisManifest,\n type FormSpecSemanticQuery,\n type FormSpecSemanticResponse,\n} from \"@formspec/analysis\";\nimport {\n createFormSpecAnalysisManifest,\n getFormSpecWorkspaceRuntimePaths,\n type FormSpecWorkspaceRuntimePaths,\n} from \"./workspace.js\";\n\ninterface LoggerLike {\n info(message: string): void;\n}\n\nexport interface FormSpecPluginServiceOptions {\n readonly workspaceRoot: string;\n readonly typescriptVersion: string;\n readonly getProgram: () => ts.Program | undefined;\n readonly logger?: LoggerLike;\n readonly snapshotDebounceMs?: number;\n readonly now?: () => Date;\n}\n\ninterface CachedFileSnapshot {\n readonly sourceHash: string;\n readonly snapshot: FormSpecAnalysisFileSnapshot;\n}\n\nexport class FormSpecPluginService {\n private readonly manifest: FormSpecAnalysisManifest;\n private readonly runtimePaths: FormSpecWorkspaceRuntimePaths;\n private readonly snapshotCache = new Map<string, CachedFileSnapshot>();\n private readonly refreshTimers = new Map<string, NodeJS.Timeout>();\n private server: net.Server | null = null;\n\n public constructor(private readonly options: FormSpecPluginServiceOptions) {\n this.runtimePaths = getFormSpecWorkspaceRuntimePaths(options.workspaceRoot);\n this.manifest = createFormSpecAnalysisManifest(\n options.workspaceRoot,\n options.typescriptVersion,\n Date.now()\n );\n }\n\n public getManifest(): FormSpecAnalysisManifest {\n return this.manifest;\n }\n\n public async start(): Promise<void> {\n if (this.server !== null) {\n return;\n }\n\n await fs.mkdir(this.runtimePaths.runtimeDirectory, { recursive: true });\n if (this.runtimePaths.endpoint.kind === \"unix-socket\") {\n await fs.rm(this.runtimePaths.endpoint.address, { force: true });\n }\n\n this.server = net.createServer((socket) => {\n let buffer = \"\";\n socket.setEncoding(\"utf8\");\n socket.on(\"data\", (chunk) => {\n buffer += String(chunk);\n const newlineIndex = buffer.indexOf(\"\\n\");\n if (newlineIndex < 0) {\n return;\n }\n\n const payload = buffer.slice(0, newlineIndex);\n const remaining = buffer.slice(newlineIndex + 1);\n if (remaining.trim().length > 0) {\n this.options.logger?.info(\n `[FormSpec] Ignoring extra semantic query payload data for ${this.runtimePaths.workspaceRoot}`\n );\n }\n buffer = remaining;\n // The FormSpec IPC transport is intentionally one-request-per-connection.\n this.respondToSocket(socket, payload);\n });\n });\n\n await new Promise<void>((resolve, reject) => {\n const handleError = (error: Error) => {\n reject(error);\n };\n this.server?.once(\"error\", handleError);\n this.server?.listen(this.runtimePaths.endpoint.address, () => {\n this.server?.off(\"error\", handleError);\n resolve();\n });\n });\n\n await this.writeManifest();\n }\n\n public async stop(): Promise<void> {\n for (const timer of this.refreshTimers.values()) {\n clearTimeout(timer);\n }\n this.refreshTimers.clear();\n this.snapshotCache.clear();\n\n const server = this.server;\n this.server = null;\n if (server?.listening === true) {\n await new Promise<void>((resolve, reject) => {\n server.close((error) => {\n if (error === undefined) {\n resolve();\n return;\n }\n reject(error);\n });\n });\n }\n\n await this.cleanupRuntimeArtifacts();\n }\n\n public scheduleSnapshotRefresh(filePath: string): void {\n const existing = this.refreshTimers.get(filePath);\n if (existing !== undefined) {\n clearTimeout(existing);\n }\n\n const timer = setTimeout(() => {\n try {\n this.getFileSnapshot(filePath);\n } catch (error: unknown) {\n this.options.logger?.info(\n `[FormSpec] Failed to refresh semantic snapshot for ${filePath}: ${String(error)}`\n );\n }\n this.refreshTimers.delete(filePath);\n }, this.options.snapshotDebounceMs ?? 250);\n\n this.refreshTimers.set(filePath, timer);\n }\n\n public handleQuery(query: FormSpecSemanticQuery): FormSpecSemanticResponse {\n switch (query.kind) {\n case \"health\":\n return {\n protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n kind: \"health\",\n manifest: this.manifest,\n };\n case \"completion\": {\n const environment = this.getSourceEnvironment(query.filePath);\n if (environment === null) {\n return {\n protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n kind: \"error\",\n error: `Unable to resolve TypeScript source file for ${query.filePath}`,\n };\n }\n\n const declaration = findDeclarationForCommentOffset(environment.sourceFile, query.offset);\n const placement = declaration === null ? null : resolveDeclarationPlacement(declaration);\n const subjectType =\n declaration === null ? undefined : getSubjectType(declaration, environment.checker);\n const context = getSemanticCommentCompletionContextAtOffset(\n environment.sourceFile.text,\n query.offset,\n {\n checker: environment.checker,\n ...(placement === null ? {} : { placement }),\n ...(subjectType === undefined ? {} : { subjectType }),\n }\n );\n\n return {\n protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n kind: \"completion\",\n sourceHash: computeFormSpecTextHash(environment.sourceFile.text),\n context: serializeCompletionContext(context),\n };\n }\n case \"hover\": {\n const environment = this.getSourceEnvironment(query.filePath);\n if (environment === null) {\n return {\n protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n kind: \"error\",\n error: `Unable to resolve TypeScript source file for ${query.filePath}`,\n };\n }\n\n const declaration = findDeclarationForCommentOffset(environment.sourceFile, query.offset);\n const placement = declaration === null ? null : resolveDeclarationPlacement(declaration);\n const subjectType =\n declaration === null ? undefined : getSubjectType(declaration, environment.checker);\n const hover = getCommentHoverInfoAtOffset(environment.sourceFile.text, query.offset, {\n checker: environment.checker,\n ...(placement === null ? {} : { placement }),\n ...(subjectType === undefined ? {} : { subjectType }),\n });\n\n return {\n protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n kind: \"hover\",\n sourceHash: computeFormSpecTextHash(environment.sourceFile.text),\n hover: serializeHoverInfo(hover),\n };\n }\n case \"diagnostics\": {\n const snapshot = this.getFileSnapshot(query.filePath);\n return {\n protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n kind: \"diagnostics\",\n sourceHash: snapshot.sourceHash,\n diagnostics: snapshot.diagnostics,\n };\n }\n case \"file-snapshot\":\n return {\n protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n kind: \"file-snapshot\",\n snapshot: this.getFileSnapshot(query.filePath),\n };\n default: {\n throw new Error(`Unhandled semantic query: ${JSON.stringify(query)}`);\n }\n }\n }\n\n private respondToSocket(socket: net.Socket, payload: string): void {\n try {\n const query = JSON.parse(payload) as unknown;\n if (!isFormSpecSemanticQuery(query)) {\n throw new Error(\"Invalid FormSpec semantic query payload\");\n }\n const response = this.handleQuery(query);\n socket.end(`${JSON.stringify(response)}\\n`);\n } catch (error) {\n socket.end(\n `${JSON.stringify({\n protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n kind: \"error\",\n error: error instanceof Error ? error.message : String(error),\n } satisfies FormSpecSemanticResponse)}\\n`\n );\n }\n }\n\n private async writeManifest(): Promise<void> {\n const tempManifestPath = `${this.runtimePaths.manifestPath}.tmp`;\n await fs.writeFile(tempManifestPath, `${JSON.stringify(this.manifest, null, 2)}\\n`, \"utf8\");\n await fs.rename(tempManifestPath, this.runtimePaths.manifestPath);\n }\n\n private async cleanupRuntimeArtifacts(): Promise<void> {\n await fs.rm(this.runtimePaths.manifestPath, { force: true });\n if (this.runtimePaths.endpoint.kind === \"unix-socket\") {\n await fs.rm(this.runtimePaths.endpoint.address, { force: true });\n }\n }\n\n private getSourceEnvironment(filePath: string): {\n readonly sourceFile: ts.SourceFile;\n readonly checker: ts.TypeChecker;\n } | null {\n const program = this.options.getProgram();\n if (program === undefined) {\n return null;\n }\n\n const sourceFile = program.getSourceFile(filePath);\n if (sourceFile === undefined) {\n return null;\n }\n\n return {\n sourceFile,\n checker: program.getTypeChecker(),\n };\n }\n\n private getFileSnapshot(filePath: string): FormSpecAnalysisFileSnapshot {\n const environment = this.getSourceEnvironment(filePath);\n if (environment === null) {\n return {\n filePath,\n sourceHash: \"\",\n generatedAt: this.getNow().toISOString(),\n comments: [],\n diagnostics: [\n {\n code: \"MISSING_SOURCE_FILE\",\n message: `Unable to resolve TypeScript source file for ${filePath}`,\n range: { start: 0, end: 0 },\n severity: \"warning\",\n },\n ],\n };\n }\n\n const sourceHash = computeFormSpecTextHash(environment.sourceFile.text);\n const cached = this.snapshotCache.get(filePath);\n if (cached?.sourceHash === sourceHash) {\n return cached.snapshot;\n }\n\n const snapshot = buildFormSpecAnalysisFileSnapshot(environment.sourceFile, {\n checker: environment.checker,\n } satisfies BuildFormSpecAnalysisFileSnapshotOptions);\n this.snapshotCache.set(filePath, {\n sourceHash,\n snapshot,\n });\n return snapshot;\n }\n\n private getNow(): Date {\n return this.options.now?.() ?? new Date();\n }\n}\n\nexport function createLanguageServiceProxy(\n languageService: ts.LanguageService,\n semanticService: FormSpecPluginService\n): ts.LanguageService {\n const wrapWithSnapshotRefresh = <Args extends readonly unknown[], Result>(\n fn: (fileName: string, ...args: Args) => Result\n ) => {\n return (fileName: string, ...args: Args): Result => {\n semanticService.scheduleSnapshotRefresh(fileName);\n return fn(fileName, ...args);\n };\n };\n\n // The plugin keeps semantic snapshots fresh for the lightweight LSP. The\n // underlying tsserver results still come from the original language service.\n const getSemanticDiagnostics = wrapWithSnapshotRefresh((fileName) =>\n languageService.getSemanticDiagnostics(fileName)\n );\n\n const getCompletionsAtPosition = wrapWithSnapshotRefresh(\n (fileName: string, position: number, options: ts.GetCompletionsAtPositionOptions | undefined) =>\n languageService.getCompletionsAtPosition(fileName, position, options)\n );\n\n const getQuickInfoAtPosition = wrapWithSnapshotRefresh((fileName, position: number) =>\n languageService.getQuickInfoAtPosition(fileName, position)\n );\n\n return new Proxy(languageService, {\n get(target, property, receiver) {\n switch (property) {\n case \"getSemanticDiagnostics\":\n return getSemanticDiagnostics;\n case \"getCompletionsAtPosition\":\n return getCompletionsAtPosition;\n case \"getQuickInfoAtPosition\":\n return getQuickInfoAtPosition;\n default:\n return Reflect.get(target, property, receiver) as unknown;\n }\n },\n });\n}\n","import os from \"node:os\";\nimport path from \"node:path\";\nimport {\n FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n FORMSPEC_ANALYSIS_SCHEMA_VERSION,\n getFormSpecManifestPath,\n getFormSpecWorkspaceId,\n getFormSpecWorkspaceRuntimeDirectory,\n type FormSpecAnalysisManifest,\n type FormSpecIpcEndpoint,\n} from \"@formspec/analysis\";\n\nexport interface FormSpecWorkspaceRuntimePaths {\n readonly workspaceRoot: string;\n readonly workspaceId: string;\n readonly runtimeDirectory: string;\n readonly manifestPath: string;\n readonly endpoint: FormSpecIpcEndpoint;\n}\n\nexport function getFormSpecWorkspaceRuntimePaths(\n workspaceRoot: string,\n platform = process.platform,\n userScope = getFormSpecUserScope()\n): FormSpecWorkspaceRuntimePaths {\n const workspaceId = getFormSpecWorkspaceId(workspaceRoot);\n const runtimeDirectory = getFormSpecWorkspaceRuntimeDirectory(workspaceRoot);\n const sanitizedUserScope = sanitizeScopeSegment(userScope);\n const endpoint: FormSpecIpcEndpoint =\n platform === \"win32\"\n ? {\n kind: \"windows-pipe\",\n address: `\\\\\\\\.\\\\pipe\\\\formspec-${sanitizedUserScope}-${workspaceId}`,\n }\n : {\n kind: \"unix-socket\",\n address: path.join(os.tmpdir(), `formspec-${sanitizedUserScope}-${workspaceId}.sock`),\n };\n\n return {\n workspaceRoot,\n workspaceId,\n runtimeDirectory,\n manifestPath: getFormSpecManifestPath(workspaceRoot),\n endpoint,\n };\n}\n\nexport function createFormSpecAnalysisManifest(\n workspaceRoot: string,\n typescriptVersion: string,\n generation: number,\n extensionFingerprint = \"builtin\"\n): FormSpecAnalysisManifest {\n const paths = getFormSpecWorkspaceRuntimePaths(workspaceRoot);\n return {\n protocolVersion: FORMSPEC_ANALYSIS_PROTOCOL_VERSION,\n analysisSchemaVersion: FORMSPEC_ANALYSIS_SCHEMA_VERSION,\n workspaceRoot,\n workspaceId: paths.workspaceId,\n endpoint: paths.endpoint,\n typescriptVersion,\n extensionFingerprint,\n generation,\n updatedAt: new Date().toISOString(),\n };\n}\n\nfunction getFormSpecUserScope(): string {\n try {\n return sanitizeScopeSegment(os.userInfo().username);\n } catch {\n return sanitizeScopeSegment(\n process.env[\"USER\"] ?? process.env[\"USERNAME\"] ?? process.env[\"LOGNAME\"] ?? \"formspec\"\n );\n }\n}\n\nfunction sanitizeScopeSegment(value: string): string {\n const trimmed = value.trim().toLowerCase();\n const sanitized = trimmed.replace(/[^a-z0-9_-]+/gu, \"-\").replace(/-+/gu, \"-\");\n return sanitized.length > 0 ? sanitized : \"formspec\";\n}\n","import type * as tsServer from \"typescript/lib/tsserverlibrary.js\";\nimport { createLanguageServiceProxy, FormSpecPluginService } from \"./service.js\";\n\ninterface ServiceEntry {\n readonly service: FormSpecPluginService;\n referenceCount: number;\n}\n\nconst services = new Map<string, ServiceEntry>();\n\nfunction formatPluginError(error: unknown): string {\n return error instanceof Error ? (error.stack ?? error.message) : String(error);\n}\n\nfunction getOrCreateService(\n info: tsServer.server.PluginCreateInfo,\n typescriptVersion: string\n): FormSpecPluginService {\n const workspaceRoot = info.project.getCurrentDirectory();\n const existing = services.get(workspaceRoot);\n if (existing !== undefined) {\n existing.referenceCount += 1;\n attachProjectCloseHandler(info, workspaceRoot, existing);\n return existing.service;\n }\n\n const service = new FormSpecPluginService({\n workspaceRoot,\n typescriptVersion,\n getProgram: () => info.languageService.getProgram(),\n logger: info.project.projectService.logger,\n });\n\n const serviceEntry: ServiceEntry = {\n service,\n referenceCount: 1,\n };\n attachProjectCloseHandler(info, workspaceRoot, serviceEntry);\n\n service.start().catch((error: unknown) => {\n info.project.projectService.logger.info(\n `[FormSpec] Plugin service failed to start for ${workspaceRoot}: ${formatPluginError(error)}`\n );\n services.delete(workspaceRoot);\n });\n services.set(workspaceRoot, serviceEntry);\n return service;\n}\n\nfunction attachProjectCloseHandler(\n info: tsServer.server.PluginCreateInfo,\n workspaceRoot: string,\n serviceEntry: ServiceEntry\n): void {\n const originalClose = info.project.close.bind(info.project);\n let closed = false;\n\n info.project.close = () => {\n if (closed) {\n originalClose();\n return;\n }\n\n closed = true;\n serviceEntry.referenceCount -= 1;\n if (serviceEntry.referenceCount <= 0) {\n services.delete(workspaceRoot);\n void serviceEntry.service.stop().catch((error: unknown) => {\n info.project.projectService.logger.info(\n `[FormSpec] Failed to stop plugin service for ${workspaceRoot}: ${formatPluginError(error)}`\n );\n });\n }\n originalClose();\n };\n}\n\n/**\n * Initializes the FormSpec TypeScript language service plugin.\n *\n * @public\n */\nexport function init(modules: {\n readonly typescript: typeof tsServer;\n}): tsServer.server.PluginModule {\n const typescriptVersion = modules.typescript.version;\n return {\n create(info) {\n const service = getOrCreateService(info, typescriptVersion);\n return createLanguageServiceProxy(info.languageService, service);\n },\n };\n}\n"],"mappings":"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","names":["FORMSPEC_ANALYSIS_PROTOCOL_VERSION","FORMSPEC_ANALYSIS_PROTOCOL_VERSION"]}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import { type FormSpecAnalysisManifest, type FormSpecSemanticQuery, type FormSpecSemanticResponse } from "@formspec/analysis";
|
|
3
|
+
interface LoggerLike {
|
|
4
|
+
info(message: string): void;
|
|
5
|
+
}
|
|
6
|
+
export interface FormSpecPluginServiceOptions {
|
|
7
|
+
readonly workspaceRoot: string;
|
|
8
|
+
readonly typescriptVersion: string;
|
|
9
|
+
readonly getProgram: () => ts.Program | undefined;
|
|
10
|
+
readonly logger?: LoggerLike;
|
|
11
|
+
readonly snapshotDebounceMs?: number;
|
|
12
|
+
readonly now?: () => Date;
|
|
13
|
+
}
|
|
14
|
+
export declare class FormSpecPluginService {
|
|
15
|
+
private readonly options;
|
|
16
|
+
private readonly manifest;
|
|
17
|
+
private readonly runtimePaths;
|
|
18
|
+
private readonly snapshotCache;
|
|
19
|
+
private readonly refreshTimers;
|
|
20
|
+
private server;
|
|
21
|
+
constructor(options: FormSpecPluginServiceOptions);
|
|
22
|
+
getManifest(): FormSpecAnalysisManifest;
|
|
23
|
+
start(): Promise<void>;
|
|
24
|
+
stop(): Promise<void>;
|
|
25
|
+
scheduleSnapshotRefresh(filePath: string): void;
|
|
26
|
+
handleQuery(query: FormSpecSemanticQuery): FormSpecSemanticResponse;
|
|
27
|
+
private respondToSocket;
|
|
28
|
+
private writeManifest;
|
|
29
|
+
private cleanupRuntimeArtifacts;
|
|
30
|
+
private getSourceEnvironment;
|
|
31
|
+
private getFileSnapshot;
|
|
32
|
+
private getNow;
|
|
33
|
+
}
|
|
34
|
+
export declare function createLanguageServiceProxy(languageService: ts.LanguageService, semanticService: FormSpecPluginService): ts.LanguageService;
|
|
35
|
+
export {};
|
|
36
|
+
//# sourceMappingURL=service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,YAAY,CAAC;AACjC,OAAO,EAcL,KAAK,wBAAwB,EAC7B,KAAK,qBAAqB,EAC1B,KAAK,wBAAwB,EAC9B,MAAM,oBAAoB,CAAC;AAO5B,UAAU,UAAU;IAClB,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,UAAU,EAAE,MAAM,EAAE,CAAC,OAAO,GAAG,SAAS,CAAC;IAClD,QAAQ,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC;IAC7B,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAC3B;AAOD,qBAAa,qBAAqB;IAOb,OAAO,CAAC,QAAQ,CAAC,OAAO;IAN3C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA2B;IACpD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAgC;IAC7D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAyC;IACvE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqC;IACnE,OAAO,CAAC,MAAM,CAA2B;gBAEL,OAAO,EAAE,4BAA4B;IASlE,WAAW,IAAI,wBAAwB;IAIjC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IA+CtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAwB3B,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAoB/C,WAAW,CAAC,KAAK,EAAE,qBAAqB,GAAG,wBAAwB;IAuF1E,OAAO,CAAC,eAAe;YAmBT,aAAa;YAMb,uBAAuB;IAOrC,OAAO,CAAC,oBAAoB;IAoB5B,OAAO,CAAC,eAAe;IAmCvB,OAAO,CAAC,MAAM;CAGf;AAED,wBAAgB,0BAA0B,CACxC,eAAe,EAAE,EAAE,CAAC,eAAe,EACnC,eAAe,EAAE,qBAAqB,GACrC,EAAE,CAAC,eAAe,CAuCpB"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type * as tsServer from 'typescript/lib/tsserverlibrary.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Initializes the FormSpec TypeScript language service plugin.
|
|
5
|
+
*
|
|
6
|
+
* @public
|
|
7
|
+
*/
|
|
8
|
+
export declare function init(modules: {
|
|
9
|
+
readonly typescript: typeof tsServer;
|
|
10
|
+
}): tsServer.server.PluginModule;
|
|
11
|
+
|
|
12
|
+
export { }
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type FormSpecAnalysisManifest, type FormSpecIpcEndpoint } from "@formspec/analysis";
|
|
2
|
+
export interface FormSpecWorkspaceRuntimePaths {
|
|
3
|
+
readonly workspaceRoot: string;
|
|
4
|
+
readonly workspaceId: string;
|
|
5
|
+
readonly runtimeDirectory: string;
|
|
6
|
+
readonly manifestPath: string;
|
|
7
|
+
readonly endpoint: FormSpecIpcEndpoint;
|
|
8
|
+
}
|
|
9
|
+
export declare function getFormSpecWorkspaceRuntimePaths(workspaceRoot: string, platform?: NodeJS.Platform, userScope?: string): FormSpecWorkspaceRuntimePaths;
|
|
10
|
+
export declare function createFormSpecAnalysisManifest(workspaceRoot: string, typescriptVersion: string, generation: number, extensionFingerprint?: string): FormSpecAnalysisManifest;
|
|
11
|
+
//# sourceMappingURL=workspace.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace.d.ts","sourceRoot":"","sources":["../src/workspace.ts"],"names":[],"mappings":"AAEA,OAAO,EAML,KAAK,wBAAwB,EAC7B,KAAK,mBAAmB,EACzB,MAAM,oBAAoB,CAAC;AAE5B,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,mBAAmB,CAAC;CACxC;AAED,wBAAgB,gCAAgC,CAC9C,aAAa,EAAE,MAAM,EACrB,QAAQ,kBAAmB,EAC3B,SAAS,SAAyB,GACjC,6BAA6B,CAsB/B;AAED,wBAAgB,8BAA8B,CAC5C,aAAa,EAAE,MAAM,EACrB,iBAAiB,EAAE,MAAM,EACzB,UAAU,EAAE,MAAM,EAClB,oBAAoB,SAAY,GAC/B,wBAAwB,CAa1B"}
|
package/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require("./dist/index.cjs").init;
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@formspec/ts-plugin",
|
|
3
|
+
"version": "0.1.0-alpha.20",
|
|
4
|
+
"description": "TypeScript language service plugin for FormSpec semantic comment analysis",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/ts-plugin.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/ts-plugin.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"index.cjs",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@formspec/analysis": "0.1.0-alpha.20"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"typescript": "^5.0.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@microsoft/api-extractor": "^7.52.12",
|
|
29
|
+
"vitest": "^3.0.0"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"license": "UNLICENSED",
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsup && tsc --emitDeclarationOnly && api-extractor run --local",
|
|
37
|
+
"clean": "rm -rf dist temp",
|
|
38
|
+
"typecheck": "tsc --noEmit",
|
|
39
|
+
"test": "vitest run",
|
|
40
|
+
"api-extractor": "api-extractor run",
|
|
41
|
+
"api-extractor:local": "api-extractor run --local",
|
|
42
|
+
"api-documenter": "api-documenter markdown -i temp -o docs"
|
|
43
|
+
}
|
|
44
|
+
}
|