@treelsp/cli 0.0.1 → 0.0.3
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 +167 -0
- package/bin/treelsp.js +2 -0
- package/dist/index.js +771 -249
- package/package.json +9 -3
package/dist/index.js
CHANGED
|
@@ -1,42 +1,75 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
|
+
import pc from "picocolors";
|
|
3
4
|
import prompts from "prompts";
|
|
4
5
|
import ora from "ora";
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { pathToFileURL } from "node:url";
|
|
10
|
-
import { generateAstTypes, generateGrammar, generateHighlights, generateLocals, generateManifest } from "treelsp/codegen";
|
|
11
|
-
import { execSync } from "node:child_process";
|
|
6
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
7
|
+
import { basename, dirname, relative, resolve } from "node:path";
|
|
8
|
+
import { copyFileSync, existsSync, readFileSync, readdirSync, rmSync, unlinkSync } from "node:fs";
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
12
10
|
import { build } from "esbuild";
|
|
11
|
+
import { generateAstTypes, generateManifest, generateTextmate } from "treelsp/codegen";
|
|
13
12
|
import chokidar from "chokidar";
|
|
14
13
|
|
|
15
14
|
//#region src/commands/init.ts
|
|
15
|
+
/**
|
|
16
|
+
* Read the CLI package version at runtime so scaffolded projects
|
|
17
|
+
* always pin to the version of the CLI that created them.
|
|
18
|
+
*/
|
|
19
|
+
async function getCliVersion() {
|
|
20
|
+
try {
|
|
21
|
+
const distDir = dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const pkgPath = resolve(distDir, "..", "package.json");
|
|
23
|
+
const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
|
|
24
|
+
return pkg.version;
|
|
25
|
+
} catch {
|
|
26
|
+
return "0.0.1";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
16
29
|
async function init() {
|
|
17
30
|
console.log(pc.bold("treelsp init\n"));
|
|
18
|
-
const answers = await prompts([
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
const answers = await prompts([
|
|
32
|
+
{
|
|
33
|
+
type: "text",
|
|
34
|
+
name: "name",
|
|
35
|
+
message: "Language name:",
|
|
36
|
+
initial: "my-lang",
|
|
37
|
+
validate: (value) => value.length > 0 || "Name is required"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
type: "text",
|
|
41
|
+
name: "extension",
|
|
42
|
+
message: "File extension:",
|
|
43
|
+
initial: ".mylang",
|
|
44
|
+
validate: (value) => {
|
|
45
|
+
if (!value.startsWith(".")) return "Extension must start with a dot";
|
|
46
|
+
if (value.length < 2) return "Extension is too short";
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
type: "select",
|
|
52
|
+
name: "backend",
|
|
53
|
+
message: "Parser backend:",
|
|
54
|
+
choices: [{
|
|
55
|
+
title: "Tree-sitter",
|
|
56
|
+
description: "Default — generates WASM parser, requires tree-sitter CLI",
|
|
57
|
+
value: "tree-sitter"
|
|
58
|
+
}, {
|
|
59
|
+
title: "Lezer",
|
|
60
|
+
description: "Pure JavaScript — no external CLI required",
|
|
61
|
+
value: "lezer"
|
|
62
|
+
}],
|
|
63
|
+
initial: 0
|
|
33
64
|
}
|
|
34
|
-
|
|
35
|
-
if (!answers.name || !answers.extension) {
|
|
65
|
+
]);
|
|
66
|
+
if (!answers.name || !answers.extension || !answers.backend) {
|
|
36
67
|
console.log(pc.dim("\nCancelled"));
|
|
37
68
|
process.exit(0);
|
|
38
69
|
}
|
|
39
|
-
const { name, extension } = answers;
|
|
70
|
+
const { name, extension, backend } = answers;
|
|
71
|
+
const capitalizedName = name.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
|
|
72
|
+
const languageId = name.replace(/-/g, "").toLowerCase();
|
|
40
73
|
const spinner = ora("Creating project structure...").start();
|
|
41
74
|
try {
|
|
42
75
|
const projectDir = resolve(process.cwd(), name);
|
|
@@ -45,31 +78,178 @@ async function init() {
|
|
|
45
78
|
console.log(pc.dim("\nChoose a different name or remove the existing directory"));
|
|
46
79
|
process.exit(1);
|
|
47
80
|
}
|
|
48
|
-
await mkdir(projectDir);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
await writeFile(resolve(projectDir,
|
|
71
|
-
|
|
72
|
-
|
|
81
|
+
await mkdir(resolve(projectDir, ".vscode"), { recursive: true });
|
|
82
|
+
await mkdir(resolve(projectDir, "packages", "language"), { recursive: true });
|
|
83
|
+
await mkdir(resolve(projectDir, "packages", "extension", "src"), { recursive: true });
|
|
84
|
+
const version = await getCliVersion();
|
|
85
|
+
const versionRange = `^${version}`;
|
|
86
|
+
const files = [
|
|
87
|
+
["pnpm-workspace.yaml", pnpmWorkspaceYaml()],
|
|
88
|
+
["package.json", rootPackageJson(name, versionRange)],
|
|
89
|
+
["treelsp-config.json", treelspConfigJson(backend)],
|
|
90
|
+
[".gitignore", rootGitignore()],
|
|
91
|
+
["README.md", rootReadme(capitalizedName, backend)],
|
|
92
|
+
[".vscode/launch.json", vscodeLaunchJson()],
|
|
93
|
+
[".vscode/tasks.json", vscodeTasksJson()],
|
|
94
|
+
["packages/language/package.json", languagePackageJson(name, versionRange)],
|
|
95
|
+
["packages/language/tsconfig.json", languageTsconfig()],
|
|
96
|
+
["packages/language/grammar.ts", grammarTemplate(capitalizedName, extension)],
|
|
97
|
+
["packages/extension/package.json", extensionPackageJson(name, capitalizedName, languageId, extension)],
|
|
98
|
+
["packages/extension/tsconfig.json", extensionTsconfig()],
|
|
99
|
+
["packages/extension/tsdown.config.ts", extensionTsdownConfig()],
|
|
100
|
+
["packages/extension/.vscodeignore", extensionVscodeignore()],
|
|
101
|
+
["packages/extension/src/extension.ts", extensionTs(capitalizedName, languageId)]
|
|
102
|
+
];
|
|
103
|
+
for (const [filePath, content] of files) await writeFile(resolve(projectDir, filePath), content, "utf-8");
|
|
104
|
+
spinner.succeed("Project created!");
|
|
105
|
+
console.log(pc.dim("\nNext steps:"));
|
|
106
|
+
console.log(pc.dim(` cd ${name}`));
|
|
107
|
+
console.log(pc.dim(" pnpm install"));
|
|
108
|
+
console.log(pc.dim(" Edit packages/language/grammar.ts to define your language"));
|
|
109
|
+
console.log(pc.dim(" pnpm build"));
|
|
110
|
+
console.log(pc.dim(" Press F5 in VS Code to launch the extension"));
|
|
111
|
+
} catch (error) {
|
|
112
|
+
spinner.fail("Failed to create project");
|
|
113
|
+
if (error instanceof Error) console.error(pc.red(`\n${error.message}`));
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function pnpmWorkspaceYaml() {
|
|
118
|
+
return `packages:\n - 'packages/*'\n`;
|
|
119
|
+
}
|
|
120
|
+
function rootPackageJson(name, versionRange) {
|
|
121
|
+
const pkg = {
|
|
122
|
+
name,
|
|
123
|
+
version: "0.1.0",
|
|
124
|
+
private: true,
|
|
125
|
+
type: "module",
|
|
126
|
+
scripts: {
|
|
127
|
+
generate: "treelsp generate",
|
|
128
|
+
build: "treelsp generate && treelsp build",
|
|
129
|
+
"build:extension": `pnpm --filter ${name}-extension build`,
|
|
130
|
+
watch: "treelsp watch"
|
|
131
|
+
},
|
|
132
|
+
devDependencies: {
|
|
133
|
+
"@treelsp/cli": versionRange,
|
|
134
|
+
typescript: "^5.7.3"
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
return JSON.stringify(pkg, null, 2) + "\n";
|
|
138
|
+
}
|
|
139
|
+
function treelspConfigJson(backend) {
|
|
140
|
+
const entry = { grammar: "packages/language/grammar.ts" };
|
|
141
|
+
if (backend !== "tree-sitter") entry["backend"] = backend;
|
|
142
|
+
const config = { languages: [entry] };
|
|
143
|
+
return JSON.stringify(config, null, 2) + "\n";
|
|
144
|
+
}
|
|
145
|
+
function rootGitignore() {
|
|
146
|
+
return `node_modules
|
|
147
|
+
dist
|
|
148
|
+
generated/
|
|
149
|
+
generated-lezer/
|
|
150
|
+
*.wasm
|
|
151
|
+
*.log
|
|
152
|
+
*.tsbuildinfo
|
|
153
|
+
.DS_Store
|
|
154
|
+
`;
|
|
155
|
+
}
|
|
156
|
+
function rootReadme(capitalizedName, backend) {
|
|
157
|
+
const generatedDir = backend === "lezer" ? "generated-lezer" : "generated";
|
|
158
|
+
return `# ${capitalizedName}
|
|
159
|
+
|
|
160
|
+
A language powered by [treelsp](https://github.com/dhrubomoy/treelsp) using the ${backend} parser backend.
|
|
161
|
+
|
|
162
|
+
## Getting Started
|
|
163
|
+
|
|
164
|
+
Install dependencies:
|
|
165
|
+
|
|
166
|
+
\`\`\`bash
|
|
167
|
+
pnpm install
|
|
168
|
+
\`\`\`
|
|
169
|
+
|
|
170
|
+
Generate grammar and build parser:
|
|
171
|
+
|
|
172
|
+
\`\`\`bash
|
|
173
|
+
pnpm build
|
|
174
|
+
\`\`\`
|
|
175
|
+
|
|
176
|
+
Launch the VS Code extension for development:
|
|
177
|
+
|
|
178
|
+
\`\`\`
|
|
179
|
+
Press F5 in VS Code, or:
|
|
180
|
+
pnpm build:extension
|
|
181
|
+
\`\`\`
|
|
182
|
+
|
|
183
|
+
Development workflow:
|
|
184
|
+
|
|
185
|
+
\`\`\`bash
|
|
186
|
+
pnpm watch # Auto-rebuild grammar on changes
|
|
187
|
+
\`\`\`
|
|
188
|
+
|
|
189
|
+
## Project Structure
|
|
190
|
+
|
|
191
|
+
- \`packages/language/grammar.ts\` - Language definition (grammar, semantics, validation, LSP)
|
|
192
|
+
- \`packages/language/${generatedDir}/\` - Generated files (parser, AST types, server bundle)
|
|
193
|
+
- \`packages/extension/\` - VS Code extension that launches the language server
|
|
194
|
+
|
|
195
|
+
## License
|
|
196
|
+
|
|
197
|
+
MIT
|
|
198
|
+
`;
|
|
199
|
+
}
|
|
200
|
+
function vscodeLaunchJson() {
|
|
201
|
+
const config = {
|
|
202
|
+
version: "0.2.0",
|
|
203
|
+
configurations: [{
|
|
204
|
+
name: "Launch Extension",
|
|
205
|
+
type: "extensionHost",
|
|
206
|
+
request: "launch",
|
|
207
|
+
args: ["--extensionDevelopmentPath=${workspaceFolder}/packages/extension", "${workspaceFolder}/packages/language"],
|
|
208
|
+
outFiles: ["${workspaceFolder}/packages/extension/dist/**/*.js"],
|
|
209
|
+
preLaunchTask: "build"
|
|
210
|
+
}]
|
|
211
|
+
};
|
|
212
|
+
return JSON.stringify(config, null, 2) + "\n";
|
|
213
|
+
}
|
|
214
|
+
function vscodeTasksJson() {
|
|
215
|
+
const config = {
|
|
216
|
+
version: "2.0.0",
|
|
217
|
+
tasks: [{
|
|
218
|
+
label: "build",
|
|
219
|
+
type: "shell",
|
|
220
|
+
command: "pnpm build && pnpm build:extension",
|
|
221
|
+
group: "build",
|
|
222
|
+
problemMatcher: []
|
|
223
|
+
}]
|
|
224
|
+
};
|
|
225
|
+
return JSON.stringify(config, null, 2) + "\n";
|
|
226
|
+
}
|
|
227
|
+
function languagePackageJson(name, versionRange) {
|
|
228
|
+
const pkg = {
|
|
229
|
+
name: `${name}-language`,
|
|
230
|
+
version: "0.1.0",
|
|
231
|
+
private: true,
|
|
232
|
+
type: "module",
|
|
233
|
+
dependencies: { treelsp: versionRange },
|
|
234
|
+
devDependencies: { typescript: "^5.7.3" }
|
|
235
|
+
};
|
|
236
|
+
return JSON.stringify(pkg, null, 2) + "\n";
|
|
237
|
+
}
|
|
238
|
+
function languageTsconfig() {
|
|
239
|
+
const config = {
|
|
240
|
+
compilerOptions: {
|
|
241
|
+
target: "ES2022",
|
|
242
|
+
module: "NodeNext",
|
|
243
|
+
moduleResolution: "NodeNext",
|
|
244
|
+
strict: true,
|
|
245
|
+
outDir: "./dist"
|
|
246
|
+
},
|
|
247
|
+
include: ["grammar.ts"]
|
|
248
|
+
};
|
|
249
|
+
return JSON.stringify(config, null, 2) + "\n";
|
|
250
|
+
}
|
|
251
|
+
function grammarTemplate(capitalizedName, extension) {
|
|
252
|
+
return `/**
|
|
73
253
|
* ${capitalizedName} - Language definition for treelsp
|
|
74
254
|
*/
|
|
75
255
|
|
|
@@ -164,208 +344,413 @@ export default defineLanguage({
|
|
|
164
344
|
},
|
|
165
345
|
});
|
|
166
346
|
`;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
347
|
+
}
|
|
348
|
+
function extensionPackageJson(name, capitalizedName, languageId, extension) {
|
|
349
|
+
const pkg = {
|
|
350
|
+
name: `${name}-extension`,
|
|
351
|
+
displayName: capitalizedName,
|
|
352
|
+
description: `VS Code extension for ${capitalizedName}`,
|
|
353
|
+
version: "0.1.0",
|
|
354
|
+
private: true,
|
|
355
|
+
publisher: name,
|
|
356
|
+
engines: { vscode: "^1.80.0" },
|
|
357
|
+
categories: ["Programming Languages"],
|
|
358
|
+
activationEvents: ["workspaceContains:**/generated/treelsp.json", "workspaceContains:**/generated-lezer/treelsp.json"],
|
|
359
|
+
main: "./dist/extension.js",
|
|
360
|
+
contributes: { languages: [{
|
|
361
|
+
id: languageId,
|
|
362
|
+
extensions: [extension],
|
|
363
|
+
aliases: [capitalizedName]
|
|
364
|
+
}] },
|
|
365
|
+
scripts: {
|
|
366
|
+
build: "tsdown",
|
|
367
|
+
dev: "tsdown --watch",
|
|
368
|
+
package: "vsce package"
|
|
369
|
+
},
|
|
370
|
+
dependencies: { "vscode-languageclient": "^9.0.1" },
|
|
371
|
+
devDependencies: {
|
|
372
|
+
"@types/vscode": "^1.80.0",
|
|
373
|
+
tsdown: "^0.2.17",
|
|
374
|
+
typescript: "^5.7.3"
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
return JSON.stringify(pkg, null, 2) + "\n";
|
|
378
|
+
}
|
|
379
|
+
function extensionTsconfig() {
|
|
380
|
+
const config = {
|
|
381
|
+
compilerOptions: {
|
|
382
|
+
target: "ES2022",
|
|
383
|
+
module: "NodeNext",
|
|
384
|
+
moduleResolution: "NodeNext",
|
|
385
|
+
strict: true,
|
|
386
|
+
outDir: "./dist",
|
|
387
|
+
rootDir: "./src",
|
|
388
|
+
lib: ["ES2022"]
|
|
389
|
+
},
|
|
390
|
+
include: ["src/**/*"],
|
|
391
|
+
exclude: ["node_modules", "dist"]
|
|
392
|
+
};
|
|
393
|
+
return JSON.stringify(config, null, 2) + "\n";
|
|
394
|
+
}
|
|
395
|
+
function extensionTsdownConfig() {
|
|
396
|
+
return `import { defineConfig } from 'tsdown';
|
|
397
|
+
|
|
398
|
+
export default defineConfig({
|
|
399
|
+
entry: ['src/extension.ts'],
|
|
400
|
+
format: ['cjs'],
|
|
401
|
+
dts: false,
|
|
402
|
+
clean: true,
|
|
403
|
+
platform: 'node',
|
|
404
|
+
external: ['vscode'],
|
|
405
|
+
});
|
|
406
|
+
`;
|
|
407
|
+
}
|
|
408
|
+
function extensionVscodeignore() {
|
|
409
|
+
return `src/
|
|
410
|
+
node_modules/
|
|
411
|
+
.gitignore
|
|
412
|
+
tsconfig.json
|
|
413
|
+
tsdown.config.ts
|
|
173
414
|
*.tsbuildinfo
|
|
174
|
-
.DS_Store
|
|
175
415
|
`;
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
Install dependencies:
|
|
184
|
-
|
|
185
|
-
\`\`\`bash
|
|
186
|
-
npm install
|
|
187
|
-
# or: pnpm install
|
|
188
|
-
\`\`\`
|
|
189
|
-
|
|
190
|
-
Generate grammar and build parser:
|
|
191
|
-
|
|
192
|
-
\`\`\`bash
|
|
193
|
-
npm run generate # Generates grammar.js, ast.ts, server.ts
|
|
194
|
-
npm run build # Compiles to WASM
|
|
195
|
-
\`\`\`
|
|
196
|
-
|
|
197
|
-
Development workflow:
|
|
416
|
+
}
|
|
417
|
+
function extensionTs(capitalizedName, languageId) {
|
|
418
|
+
return `/**
|
|
419
|
+
* ${capitalizedName} - VS Code Extension
|
|
420
|
+
* Discovers and launches the treelsp-generated language server.
|
|
421
|
+
*/
|
|
198
422
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
423
|
+
import * as vscode from 'vscode';
|
|
424
|
+
import * as path from 'path';
|
|
425
|
+
import * as fs from 'fs';
|
|
426
|
+
import {
|
|
427
|
+
LanguageClient,
|
|
428
|
+
LanguageClientOptions,
|
|
429
|
+
ServerOptions,
|
|
430
|
+
TransportKind,
|
|
431
|
+
State,
|
|
432
|
+
} from 'vscode-languageclient/node';
|
|
433
|
+
|
|
434
|
+
interface TreelspManifest {
|
|
435
|
+
name: string;
|
|
436
|
+
languageId: string;
|
|
437
|
+
fileExtensions: string[];
|
|
438
|
+
server: string;
|
|
439
|
+
textmateGrammar?: string;
|
|
440
|
+
}
|
|
202
441
|
|
|
203
|
-
|
|
442
|
+
const clients = new Map<string, LanguageClient>();
|
|
443
|
+
|
|
444
|
+
export async function activate(context: vscode.ExtensionContext) {
|
|
445
|
+
const manifests = await discoverManifests();
|
|
446
|
+
|
|
447
|
+
for (const { manifest, manifestPath } of manifests) {
|
|
448
|
+
await startLanguageClient(manifest, manifestPath, context);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Watch for new or updated manifests
|
|
452
|
+
for (const pattern of ['**/generated/treelsp.json', '**/generated-lezer/treelsp.json']) {
|
|
453
|
+
const watcher = vscode.workspace.createFileSystemWatcher(pattern);
|
|
454
|
+
watcher.onDidCreate(async (uri) => {
|
|
455
|
+
const data = await readManifest(uri.fsPath);
|
|
456
|
+
if (data) await startLanguageClient(data, uri.fsPath, context);
|
|
457
|
+
});
|
|
458
|
+
watcher.onDidChange(async (uri) => {
|
|
459
|
+
await stopClient(uri.fsPath);
|
|
460
|
+
const data = await readManifest(uri.fsPath);
|
|
461
|
+
if (data) await startLanguageClient(data, uri.fsPath, context);
|
|
462
|
+
});
|
|
463
|
+
watcher.onDidDelete(async (uri) => {
|
|
464
|
+
await stopClient(uri.fsPath);
|
|
465
|
+
});
|
|
466
|
+
context.subscriptions.push(watcher);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
204
469
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
470
|
+
export async function deactivate(): Promise<void> {
|
|
471
|
+
const stops = [...clients.values()].map(c => c.stop());
|
|
472
|
+
await Promise.all(stops);
|
|
473
|
+
clients.clear();
|
|
474
|
+
}
|
|
208
475
|
|
|
209
|
-
|
|
476
|
+
async function discoverManifests(): Promise<Array<{ manifest: TreelspManifest; manifestPath: string }>> {
|
|
477
|
+
const results: Array<{ manifest: TreelspManifest; manifestPath: string }> = [];
|
|
478
|
+
for (const pattern of ['**/generated/treelsp.json', '**/generated-lezer/treelsp.json']) {
|
|
479
|
+
const uris = await vscode.workspace.findFiles(pattern, '**/node_modules/**');
|
|
480
|
+
for (const uri of uris) {
|
|
481
|
+
const data = await readManifest(uri.fsPath);
|
|
482
|
+
if (data) results.push({ manifest: data, manifestPath: uri.fsPath });
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return results;
|
|
486
|
+
}
|
|
210
487
|
|
|
211
|
-
|
|
212
|
-
|
|
488
|
+
async function readManifest(fsPath: string): Promise<TreelspManifest | null> {
|
|
489
|
+
try {
|
|
490
|
+
const doc = await vscode.workspace.openTextDocument(fsPath);
|
|
491
|
+
const data = JSON.parse(doc.getText()) as TreelspManifest;
|
|
492
|
+
if (!data.name || !data.languageId || !data.fileExtensions || !data.server) return null;
|
|
493
|
+
return data;
|
|
494
|
+
} catch {
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
213
498
|
|
|
214
|
-
|
|
499
|
+
async function startLanguageClient(
|
|
500
|
+
manifest: TreelspManifest,
|
|
501
|
+
manifestPath: string,
|
|
502
|
+
context: vscode.ExtensionContext,
|
|
503
|
+
): Promise<void> {
|
|
504
|
+
if (clients.has(manifestPath)) return;
|
|
505
|
+
|
|
506
|
+
const generatedDir = path.dirname(manifestPath);
|
|
507
|
+
const serverModule = path.resolve(generatedDir, manifest.server);
|
|
508
|
+
|
|
509
|
+
if (!fs.existsSync(serverModule)) {
|
|
510
|
+
void vscode.window.showErrorMessage(
|
|
511
|
+
'${capitalizedName}: Server bundle not found. Run "pnpm build" first.',
|
|
512
|
+
);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
const serverOptions: ServerOptions = {
|
|
517
|
+
run: { module: serverModule, transport: TransportKind.stdio },
|
|
518
|
+
debug: {
|
|
519
|
+
module: serverModule,
|
|
520
|
+
transport: TransportKind.stdio,
|
|
521
|
+
options: { execArgv: ['--nolazy', '--inspect=6009'] },
|
|
522
|
+
},
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
const clientOptions: LanguageClientOptions = {
|
|
526
|
+
documentSelector: manifest.fileExtensions.map(ext => ({
|
|
527
|
+
scheme: 'file' as const,
|
|
528
|
+
language: manifest.languageId,
|
|
529
|
+
pattern: \`**/*\${ext}\`,
|
|
530
|
+
})),
|
|
531
|
+
outputChannelName: '${capitalizedName} Language Server',
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
const client = new LanguageClient(
|
|
535
|
+
'${languageId}',
|
|
536
|
+
'${capitalizedName} Language Server',
|
|
537
|
+
serverOptions,
|
|
538
|
+
clientOptions,
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
client.onDidChangeState((event) => {
|
|
542
|
+
if (event.oldState === State.Running && event.newState === State.Stopped) {
|
|
543
|
+
void vscode.window.showWarningMessage(
|
|
544
|
+
'${capitalizedName}: Language server stopped unexpectedly.',
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
clients.set(manifestPath, client);
|
|
550
|
+
context.subscriptions.push(client);
|
|
551
|
+
|
|
552
|
+
try {
|
|
553
|
+
await client.start();
|
|
554
|
+
} catch (e) {
|
|
555
|
+
clients.delete(manifestPath);
|
|
556
|
+
void vscode.window.showErrorMessage(
|
|
557
|
+
\`${capitalizedName}: Failed to start language server: \${e instanceof Error ? e.message : String(e)}\`,
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
215
561
|
|
|
216
|
-
|
|
562
|
+
async function stopClient(key: string): Promise<void> {
|
|
563
|
+
const client = clients.get(key);
|
|
564
|
+
if (!client) return;
|
|
565
|
+
await client.stop();
|
|
566
|
+
clients.delete(key);
|
|
567
|
+
}
|
|
217
568
|
`;
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
//#endregion
|
|
572
|
+
//#region src/backends.ts
|
|
573
|
+
const treeSitterBin = resolve(dirname(fileURLToPath(import.meta.resolve("tree-sitter-cli/cli.js"))), "tree-sitter");
|
|
574
|
+
const BACKENDS = {
|
|
575
|
+
"tree-sitter": async () => {
|
|
576
|
+
const { TreeSitterCodegen } = await import("treelsp/codegen/tree-sitter");
|
|
577
|
+
return new TreeSitterCodegen({ treeSitterBin });
|
|
578
|
+
},
|
|
579
|
+
"lezer": async () => {
|
|
580
|
+
const { LezerCodegen } = await import("treelsp/codegen/lezer");
|
|
581
|
+
return new LezerCodegen();
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
/**
|
|
585
|
+
* Resolve a codegen backend by identifier.
|
|
586
|
+
* Defaults to "tree-sitter" if not specified.
|
|
587
|
+
*/
|
|
588
|
+
async function getCodegenBackend(id = "tree-sitter") {
|
|
589
|
+
const factory = BACKENDS[id];
|
|
590
|
+
if (!factory) {
|
|
591
|
+
const available = Object.keys(BACKENDS).join(", ");
|
|
592
|
+
throw new Error(`Unknown parser backend: "${id}". Available backends: ${available}`);
|
|
230
593
|
}
|
|
594
|
+
return factory();
|
|
231
595
|
}
|
|
232
596
|
|
|
233
597
|
//#endregion
|
|
234
598
|
//#region src/commands/generate.ts
|
|
235
|
-
|
|
236
|
-
|
|
599
|
+
/**
|
|
600
|
+
* Generate code for a single language project.
|
|
601
|
+
*/
|
|
602
|
+
async function generateProject(project) {
|
|
603
|
+
const label = relative(process.cwd(), project.grammarPath) || project.grammarPath;
|
|
604
|
+
const spinner = ora(`Loading ${label}...`).start();
|
|
237
605
|
try {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
spinner.fail("Could not find grammar.ts in current directory");
|
|
606
|
+
if (!existsSync(project.grammarPath)) {
|
|
607
|
+
spinner.fail(`Could not find ${label}`);
|
|
241
608
|
console.log(pc.dim("\nRun \"treelsp init\" to create a new language project"));
|
|
242
|
-
|
|
609
|
+
throw new Error(`Grammar file not found: ${project.grammarPath}`);
|
|
610
|
+
}
|
|
611
|
+
const tmpPath = project.grammarPath.replace(/\.ts$/, ".tmp.mjs");
|
|
612
|
+
await build({
|
|
613
|
+
entryPoints: [project.grammarPath],
|
|
614
|
+
bundle: true,
|
|
615
|
+
format: "esm",
|
|
616
|
+
platform: "node",
|
|
617
|
+
outfile: tmpPath,
|
|
618
|
+
packages: "external",
|
|
619
|
+
logLevel: "silent"
|
|
620
|
+
});
|
|
621
|
+
let definition;
|
|
622
|
+
try {
|
|
623
|
+
const grammarUrl = pathToFileURL(tmpPath).href;
|
|
624
|
+
const mod = await import(grammarUrl);
|
|
625
|
+
definition = mod.default;
|
|
626
|
+
} finally {
|
|
627
|
+
try {
|
|
628
|
+
unlinkSync(tmpPath);
|
|
629
|
+
} catch {}
|
|
243
630
|
}
|
|
244
|
-
const grammarUrl = pathToFileURL(grammarPath).href;
|
|
245
|
-
const module = await import(grammarUrl);
|
|
246
|
-
const definition = module.default;
|
|
247
631
|
if (!definition || !definition.name || !definition.grammar) {
|
|
248
632
|
spinner.fail("Invalid language definition");
|
|
249
633
|
console.log(pc.dim("\nEnsure grammar.ts exports a valid language definition using defineLanguage()"));
|
|
250
|
-
|
|
634
|
+
throw new Error("Invalid language definition");
|
|
251
635
|
}
|
|
252
636
|
spinner.text = `Generating code for ${definition.name}...`;
|
|
253
|
-
const
|
|
637
|
+
const backend = await getCodegenBackend(project.backend);
|
|
638
|
+
const artifacts = backend.generate(definition);
|
|
254
639
|
const astTypes = generateAstTypes(definition);
|
|
255
640
|
const manifest = generateManifest(definition);
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
const
|
|
259
|
-
const
|
|
260
|
-
await mkdir(queriesDir, { recursive: true });
|
|
641
|
+
const textmateGrammar = generateTextmate(definition);
|
|
642
|
+
await mkdir(project.outDir, { recursive: true });
|
|
643
|
+
const artifactDirs = new Set(artifacts.map((a) => a.path.includes("/") ? resolve(project.outDir, a.path, "..") : null).filter((d) => d !== null));
|
|
644
|
+
for (const dir of artifactDirs) await mkdir(dir, { recursive: true });
|
|
261
645
|
await Promise.all([
|
|
262
|
-
writeFile(resolve(
|
|
263
|
-
writeFile(resolve(
|
|
264
|
-
writeFile(resolve(
|
|
265
|
-
writeFile(resolve(
|
|
266
|
-
writeFile(resolve(queriesDir, "locals.scm"), localsSCM, "utf-8")
|
|
646
|
+
...artifacts.map((a) => writeFile(resolve(project.outDir, a.path), a.content, "utf-8")),
|
|
647
|
+
writeFile(resolve(project.outDir, "ast.ts"), astTypes, "utf-8"),
|
|
648
|
+
writeFile(resolve(project.outDir, "treelsp.json"), manifest, "utf-8"),
|
|
649
|
+
writeFile(resolve(project.outDir, "syntax.tmLanguage.json"), textmateGrammar, "utf-8")
|
|
267
650
|
]);
|
|
268
|
-
|
|
269
|
-
|
|
651
|
+
const outLabel = relative(process.cwd(), project.outDir) || project.outDir;
|
|
652
|
+
spinner.succeed(`Generated ${definition.name} -> ${outLabel}/`);
|
|
270
653
|
} catch (error) {
|
|
271
|
-
spinner.fail(
|
|
654
|
+
if (!spinner.isSpinning) {} else spinner.fail(`Generation failed for ${label}`);
|
|
272
655
|
if (error instanceof Error) if (error.message.includes("Cannot find module")) {
|
|
273
|
-
console.error(pc.red("\nFailed to load grammar
|
|
656
|
+
console.error(pc.red("\nFailed to load grammar file"));
|
|
274
657
|
console.log(pc.dim("Ensure the file exists and has no syntax errors"));
|
|
275
658
|
} else if (error.message.includes("EACCES") || error.message.includes("EPERM")) {
|
|
276
|
-
console.error(pc.red(
|
|
277
|
-
console.log(pc.dim("Check file permissions
|
|
659
|
+
console.error(pc.red(`\nPermission denied writing to ${project.outDir}`));
|
|
660
|
+
console.log(pc.dim("Check file permissions"));
|
|
278
661
|
} else console.error(pc.red(`\n${error.message}`));
|
|
279
|
-
|
|
662
|
+
throw error;
|
|
280
663
|
}
|
|
281
664
|
}
|
|
665
|
+
/**
|
|
666
|
+
* Top-level generate command handler.
|
|
667
|
+
*/
|
|
668
|
+
async function generate(options, configResult) {
|
|
669
|
+
for (const project of configResult.projects) await generateProject(project);
|
|
670
|
+
if (!options.watch) console.log(pc.dim("\nNext step: Run \"treelsp build\" to compile grammar to WASM"));
|
|
671
|
+
}
|
|
282
672
|
|
|
283
673
|
//#endregion
|
|
284
674
|
//#region src/commands/build.ts
|
|
285
|
-
|
|
286
|
-
|
|
675
|
+
/**
|
|
676
|
+
* Map from backend id to the runtime import specifier used in the server entry.
|
|
677
|
+
*/
|
|
678
|
+
const BACKEND_RUNTIME_IMPORT = {
|
|
679
|
+
"tree-sitter": {
|
|
680
|
+
specifier: "treelsp/backend/tree-sitter",
|
|
681
|
+
className: "TreeSitterRuntime"
|
|
682
|
+
},
|
|
683
|
+
"lezer": {
|
|
684
|
+
specifier: "treelsp/backend/lezer",
|
|
685
|
+
className: "LezerRuntime"
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
/**
|
|
689
|
+
* Build a single language project: compile parser + bundle server.
|
|
690
|
+
*/
|
|
691
|
+
async function buildProject(project) {
|
|
692
|
+
const label = relative(process.cwd(), project.projectDir) || project.projectDir;
|
|
693
|
+
const spinner = ora(`Building ${label}...`).start();
|
|
287
694
|
try {
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
}
|
|
313
|
-
} finally {
|
|
314
|
-
if (!hadPkgJson) rmSync(genPkgJson, { force: true });
|
|
315
|
-
}
|
|
316
|
-
spinner.text = "Compiling to WASM...";
|
|
317
|
-
execSync("tree-sitter build --wasm", {
|
|
318
|
-
stdio: "pipe",
|
|
319
|
-
cwd: process.cwd()
|
|
320
|
-
});
|
|
321
|
-
spinner.text = "Moving WASM output...";
|
|
322
|
-
const cwd = process.cwd();
|
|
323
|
-
const wasmFiles = readdirSync(cwd).filter((f) => f.startsWith("tree-sitter-") && f.endsWith(".wasm"));
|
|
324
|
-
if (wasmFiles.length === 0) {
|
|
325
|
-
spinner.fail("tree-sitter build --wasm did not produce a .wasm file");
|
|
326
|
-
process.exit(1);
|
|
327
|
-
}
|
|
328
|
-
const sourceWasm = resolve(cwd, wasmFiles[0]);
|
|
329
|
-
const destWasm = resolve(cwd, "generated", "grammar.wasm");
|
|
330
|
-
renameSync(sourceWasm, destWasm);
|
|
331
|
-
const cleanupDirs = ["src", "bindings"];
|
|
332
|
-
const cleanupFiles = [
|
|
333
|
-
"binding.gyp",
|
|
334
|
-
"Makefile",
|
|
335
|
-
"Package.swift",
|
|
336
|
-
".editorconfig"
|
|
337
|
-
];
|
|
338
|
-
for (const dir of cleanupDirs) {
|
|
339
|
-
const p = resolve(cwd, dir);
|
|
340
|
-
if (existsSync(p)) rmSync(p, {
|
|
341
|
-
recursive: true,
|
|
342
|
-
force: true
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
for (const file of cleanupFiles) {
|
|
346
|
-
const p = resolve(cwd, file);
|
|
347
|
-
if (existsSync(p)) rmSync(p, { force: true });
|
|
695
|
+
const backend = await getCodegenBackend(project.backend);
|
|
696
|
+
spinner.text = `Compiling parser for ${label}...`;
|
|
697
|
+
await backend.compile(project.projectDir, project.outDir, { onProgress: (msg) => {
|
|
698
|
+
spinner.text = `${msg} (${label})`;
|
|
699
|
+
} });
|
|
700
|
+
if (backend.cleanupPatterns) {
|
|
701
|
+
const { directories, files, globs } = backend.cleanupPatterns;
|
|
702
|
+
if (directories) for (const dir of directories) {
|
|
703
|
+
const p = resolve(project.projectDir, dir);
|
|
704
|
+
if (existsSync(p)) rmSync(p, {
|
|
705
|
+
recursive: true,
|
|
706
|
+
force: true
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
if (files) for (const file of files) {
|
|
710
|
+
const p = resolve(project.projectDir, file);
|
|
711
|
+
if (existsSync(p)) rmSync(p, { force: true });
|
|
712
|
+
}
|
|
713
|
+
if (globs) for (const pattern of globs) {
|
|
714
|
+
const parts = pattern.split("*");
|
|
715
|
+
if (parts.length === 2) {
|
|
716
|
+
const [prefix, suffix] = parts;
|
|
717
|
+
for (const f of readdirSync(project.projectDir)) if (f.startsWith(prefix) && f.endsWith(suffix)) rmSync(resolve(project.projectDir, f), { force: true });
|
|
718
|
+
}
|
|
719
|
+
}
|
|
348
720
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
721
|
+
spinner.text = `Bundling language server for ${label}...`;
|
|
722
|
+
const runtimeImport = BACKEND_RUNTIME_IMPORT[project.backend];
|
|
723
|
+
if (!runtimeImport) throw new Error(`No runtime import configured for backend "${project.backend}"`);
|
|
724
|
+
let serverEntry;
|
|
725
|
+
if (project.backend === "lezer") {
|
|
726
|
+
const relOut = "./" + relative(project.projectDir, project.outDir).replace(/\\/g, "/");
|
|
727
|
+
serverEntry = [
|
|
728
|
+
`import { startStdioServer } from 'treelsp/server';`,
|
|
729
|
+
`import { ${runtimeImport.className} } from '${runtimeImport.specifier}';`,
|
|
730
|
+
`import definition from './grammar.ts';`,
|
|
731
|
+
`import { parser } from '${relOut}/parser.bundle.js';`,
|
|
732
|
+
`import parserMeta from '${relOut}/parser-meta.json';`,
|
|
733
|
+
``,
|
|
734
|
+
`const backend = new ${runtimeImport.className}(parser, parserMeta);`,
|
|
735
|
+
`startStdioServer({ definition, parserPath: '', backend });`
|
|
736
|
+
].join("\n");
|
|
737
|
+
} else serverEntry = [
|
|
352
738
|
`import { startStdioServer } from 'treelsp/server';`,
|
|
353
|
-
`import {
|
|
354
|
-
`import {
|
|
739
|
+
`import { ${runtimeImport.className} } from '${runtimeImport.specifier}';`,
|
|
740
|
+
`import { resolve } from 'node:path';`,
|
|
355
741
|
`import definition from './grammar.ts';`,
|
|
356
742
|
``,
|
|
357
|
-
`const
|
|
358
|
-
`const wasmPath = resolve(__dirname, 'grammar.wasm');`,
|
|
743
|
+
`const parserPath = resolve(__dirname, 'grammar.wasm');`,
|
|
359
744
|
``,
|
|
360
|
-
`startStdioServer({ definition,
|
|
745
|
+
`startStdioServer({ definition, parserPath, backend: new ${runtimeImport.className}() });`
|
|
361
746
|
].join("\n");
|
|
362
747
|
const treelspServer = import.meta.resolve("treelsp/server");
|
|
363
748
|
const treelspPkg = resolve(new URL(treelspServer).pathname, "..", "..", "..");
|
|
364
|
-
const bundlePath = resolve(
|
|
749
|
+
const bundlePath = resolve(project.outDir, "server.bundle.cjs");
|
|
365
750
|
await build({
|
|
366
751
|
stdin: {
|
|
367
752
|
contents: serverEntry,
|
|
368
|
-
resolveDir:
|
|
753
|
+
resolveDir: project.projectDir,
|
|
369
754
|
loader: "ts"
|
|
370
755
|
},
|
|
371
756
|
bundle: true,
|
|
@@ -374,50 +759,63 @@ async function build$1() {
|
|
|
374
759
|
outfile: bundlePath,
|
|
375
760
|
sourcemap: true,
|
|
376
761
|
nodePaths: [resolve(treelspPkg, "node_modules")],
|
|
377
|
-
logLevel: "
|
|
762
|
+
logLevel: "warning",
|
|
763
|
+
logOverride: { "empty-import-meta": "silent" }
|
|
378
764
|
});
|
|
379
|
-
const {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
const tsWasmDest = resolve(genDir, "tree-sitter.wasm");
|
|
386
|
-
if (existsSync(tsWasmSrc) && !existsSync(tsWasmDest)) copyFileSync(tsWasmSrc, tsWasmDest);
|
|
387
|
-
spinner.succeed("Build complete");
|
|
388
|
-
console.log(pc.dim("\nGenerated files:"));
|
|
389
|
-
console.log(pc.dim(" generated/grammar.js"));
|
|
390
|
-
console.log(pc.dim(" generated/grammar.wasm"));
|
|
391
|
-
if (existsSync(resolve(genDir, "server.bundle.cjs"))) console.log(pc.dim(" generated/server.bundle.cjs"));
|
|
765
|
+
if (backend.getRuntimeFiles) for (const { src, dest } of backend.getRuntimeFiles(treelspPkg)) {
|
|
766
|
+
const destPath = resolve(project.outDir, dest);
|
|
767
|
+
if (existsSync(src) && !existsSync(destPath)) copyFileSync(src, destPath);
|
|
768
|
+
}
|
|
769
|
+
const outLabel = relative(process.cwd(), project.outDir) || project.outDir;
|
|
770
|
+
spinner.succeed(`Built ${label} -> ${outLabel}/`);
|
|
392
771
|
} catch (error) {
|
|
393
|
-
spinner.fail(
|
|
772
|
+
spinner.fail(`Build failed for ${label}`);
|
|
394
773
|
if (error instanceof Error) {
|
|
774
|
+
const esbuildError = error;
|
|
395
775
|
const execError = error;
|
|
396
|
-
if (
|
|
776
|
+
if (esbuildError.errors && esbuildError.errors.length > 0) {
|
|
777
|
+
console.error(pc.red("\nServer bundling failed:"));
|
|
778
|
+
for (const err of esbuildError.errors) {
|
|
779
|
+
const loc = err.location ? ` (${err.location.file}:${err.location.line})` : "";
|
|
780
|
+
console.error(pc.dim(` ${err.text}${loc}`));
|
|
781
|
+
}
|
|
782
|
+
} else if (execError.stderr) {
|
|
397
783
|
const stderr = execError.stderr.toString();
|
|
398
|
-
console.error(pc.red("\
|
|
784
|
+
console.error(pc.red("\nBackend compilation error:"));
|
|
399
785
|
console.error(pc.dim(stderr));
|
|
400
|
-
if (stderr.includes("grammar")) console.log(pc.dim("\nSuggestion: Check your grammar definition for errors"));
|
|
401
|
-
else if (stderr.includes("emcc") || stderr.includes("compiler")) {
|
|
402
|
-
console.log(pc.dim("\nSuggestion: Ensure Emscripten is installed for WASM compilation"));
|
|
403
|
-
console.log(pc.dim(" See: https://tree-sitter.github.io/tree-sitter/creating-parsers#tool-overview"));
|
|
404
|
-
}
|
|
405
786
|
} else console.error(pc.red(`\n${error.message}`));
|
|
406
787
|
}
|
|
407
|
-
|
|
788
|
+
throw error;
|
|
408
789
|
}
|
|
409
790
|
}
|
|
791
|
+
/**
|
|
792
|
+
* Top-level build command handler.
|
|
793
|
+
*/
|
|
794
|
+
async function build$1(configResult) {
|
|
795
|
+
for (const project of configResult.projects) await buildProject(project);
|
|
796
|
+
}
|
|
410
797
|
|
|
411
798
|
//#endregion
|
|
412
799
|
//#region src/commands/watch.ts
|
|
413
|
-
async function watch() {
|
|
800
|
+
async function watch(configResult) {
|
|
414
801
|
console.log(pc.bold("treelsp watch\n"));
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
802
|
+
const { projects } = configResult;
|
|
803
|
+
const projectByGrammar = /* @__PURE__ */ new Map();
|
|
804
|
+
const grammarPaths = [];
|
|
805
|
+
for (const project of projects) {
|
|
806
|
+
if (!existsSync(project.grammarPath)) {
|
|
807
|
+
console.error(pc.red(`Could not find ${relative(process.cwd(), project.grammarPath)}`));
|
|
808
|
+
process.exit(1);
|
|
809
|
+
}
|
|
810
|
+
projectByGrammar.set(project.grammarPath, project);
|
|
811
|
+
grammarPaths.push(project.grammarPath);
|
|
812
|
+
}
|
|
813
|
+
if (projects.length > 1) {
|
|
814
|
+
console.log(pc.dim(`Watching ${String(projects.length)} language projects:`));
|
|
815
|
+
for (const p of projects) console.log(pc.dim(` - ${relative(process.cwd(), p.grammarPath)}`));
|
|
816
|
+
console.log("");
|
|
419
817
|
}
|
|
420
|
-
const watcher = chokidar.watch(
|
|
818
|
+
const watcher = chokidar.watch(grammarPaths, {
|
|
421
819
|
persistent: true,
|
|
422
820
|
awaitWriteFinish: {
|
|
423
821
|
stabilityThreshold: 100,
|
|
@@ -425,20 +823,22 @@ async function watch() {
|
|
|
425
823
|
}
|
|
426
824
|
});
|
|
427
825
|
let isBuilding = false;
|
|
428
|
-
watcher.on("change", (
|
|
826
|
+
watcher.on("change", (changedPath) => {
|
|
429
827
|
(async () => {
|
|
430
828
|
if (isBuilding) {
|
|
431
829
|
console.log(pc.dim("Build in progress, skipping..."));
|
|
432
830
|
return;
|
|
433
831
|
}
|
|
434
|
-
|
|
832
|
+
const project = projectByGrammar.get(changedPath);
|
|
833
|
+
if (!project) return;
|
|
834
|
+
console.log(pc.dim(`\n${relative(process.cwd(), changedPath)} changed`));
|
|
435
835
|
isBuilding = true;
|
|
436
836
|
try {
|
|
437
|
-
await
|
|
438
|
-
await
|
|
439
|
-
console.log(pc.green("
|
|
440
|
-
} catch
|
|
441
|
-
console.log(pc.red("
|
|
837
|
+
await generateProject(project);
|
|
838
|
+
await buildProject(project);
|
|
839
|
+
console.log(pc.green(" Rebuild successful\n"));
|
|
840
|
+
} catch {
|
|
841
|
+
console.log(pc.red(" Rebuild failed\n"));
|
|
442
842
|
} finally {
|
|
443
843
|
isBuilding = false;
|
|
444
844
|
console.log(pc.dim("Watching for changes..."));
|
|
@@ -449,14 +849,116 @@ async function watch() {
|
|
|
449
849
|
console.error(pc.red("Watcher error:"), error instanceof Error ? error.message : String(error));
|
|
450
850
|
});
|
|
451
851
|
console.log(pc.dim("Running initial build...\n"));
|
|
452
|
-
try {
|
|
453
|
-
await
|
|
454
|
-
await
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
852
|
+
for (const project of projects) try {
|
|
853
|
+
await generateProject(project);
|
|
854
|
+
await buildProject(project);
|
|
855
|
+
} catch {
|
|
856
|
+
console.log(pc.red(` ${relative(process.cwd(), project.projectDir)} - FAILED\n`));
|
|
857
|
+
}
|
|
858
|
+
console.log(pc.dim("\nWatching for changes..."));
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
//#endregion
|
|
862
|
+
//#region src/config.ts
|
|
863
|
+
/**
|
|
864
|
+
* Resolve config: use -f flag, auto-discover, or fall back to legacy.
|
|
865
|
+
*/
|
|
866
|
+
function resolveConfig(fileFlag) {
|
|
867
|
+
if (fileFlag) return loadConfigFromFile(fileFlag);
|
|
868
|
+
const discovered = discoverConfig();
|
|
869
|
+
if (discovered) return discovered;
|
|
870
|
+
const cwd = process.cwd();
|
|
871
|
+
return {
|
|
872
|
+
source: "legacy",
|
|
873
|
+
projects: [{
|
|
874
|
+
grammarPath: resolve(cwd, "grammar.ts"),
|
|
875
|
+
projectDir: cwd,
|
|
876
|
+
outDir: resolve(cwd, "generated"),
|
|
877
|
+
backend: "tree-sitter"
|
|
878
|
+
}]
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
function loadConfigFromFile(filePath) {
|
|
882
|
+
const absPath = resolve(filePath);
|
|
883
|
+
if (!existsSync(absPath)) throw new Error(`Config file not found: ${absPath}`);
|
|
884
|
+
const raw = readFileSync(absPath, "utf-8");
|
|
885
|
+
if (basename(absPath) === "package.json") {
|
|
886
|
+
const pkg = JSON.parse(raw);
|
|
887
|
+
const treelspField = pkg["treelsp"];
|
|
888
|
+
if (!treelspField) throw new Error(`No "treelsp" field found in ${absPath}`);
|
|
889
|
+
const config$1 = validateConfig(treelspField, absPath);
|
|
890
|
+
return {
|
|
891
|
+
source: "package.json",
|
|
892
|
+
configPath: absPath,
|
|
893
|
+
projects: resolveProjects(config$1, dirname(absPath))
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
const config = validateConfig(JSON.parse(raw), absPath);
|
|
897
|
+
return {
|
|
898
|
+
source: "file",
|
|
899
|
+
configPath: absPath,
|
|
900
|
+
projects: resolveProjects(config, dirname(absPath))
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
function discoverConfig() {
|
|
904
|
+
let dir = resolve(process.cwd());
|
|
905
|
+
while (true) {
|
|
906
|
+
const configFile = resolve(dir, "treelsp-config.json");
|
|
907
|
+
if (existsSync(configFile)) {
|
|
908
|
+
const raw = readFileSync(configFile, "utf-8");
|
|
909
|
+
const config = validateConfig(JSON.parse(raw), configFile);
|
|
910
|
+
return {
|
|
911
|
+
source: "file",
|
|
912
|
+
configPath: configFile,
|
|
913
|
+
projects: resolveProjects(config, dir)
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
const pkgFile = resolve(dir, "package.json");
|
|
917
|
+
if (existsSync(pkgFile)) {
|
|
918
|
+
const raw = readFileSync(pkgFile, "utf-8");
|
|
919
|
+
const pkg = JSON.parse(raw);
|
|
920
|
+
if (pkg["treelsp"]) {
|
|
921
|
+
const config = validateConfig(pkg["treelsp"], pkgFile);
|
|
922
|
+
return {
|
|
923
|
+
source: "package.json",
|
|
924
|
+
configPath: pkgFile,
|
|
925
|
+
projects: resolveProjects(config, dir)
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
const parent = dirname(dir);
|
|
930
|
+
if (parent === dir) break;
|
|
931
|
+
dir = parent;
|
|
458
932
|
}
|
|
459
|
-
|
|
933
|
+
return null;
|
|
934
|
+
}
|
|
935
|
+
function validateConfig(raw, filePath) {
|
|
936
|
+
if (typeof raw !== "object" || raw === null) throw new Error(`Invalid config in ${filePath}: expected an object`);
|
|
937
|
+
const obj = raw;
|
|
938
|
+
if (!Array.isArray(obj["languages"])) throw new Error(`Invalid config in ${filePath}: "languages" must be an array`);
|
|
939
|
+
const languages = obj["languages"];
|
|
940
|
+
if (languages.length === 0) throw new Error(`Invalid config in ${filePath}: "languages" must not be empty`);
|
|
941
|
+
for (let i = 0; i < languages.length; i++) {
|
|
942
|
+
const lang = languages[i];
|
|
943
|
+
if (typeof lang !== "object" || lang === null) throw new Error(`Invalid config in ${filePath}: languages[${String(i)}] must be an object`);
|
|
944
|
+
const langObj = lang;
|
|
945
|
+
if (typeof langObj["grammar"] !== "string" || langObj["grammar"].length === 0) throw new Error(`Invalid config in ${filePath}: languages[${String(i)}].grammar must be a non-empty string`);
|
|
946
|
+
if (langObj["out"] !== void 0 && (typeof langObj["out"] !== "string" || langObj["out"].length === 0)) throw new Error(`Invalid config in ${filePath}: languages[${String(i)}].out must be a non-empty string if provided`);
|
|
947
|
+
}
|
|
948
|
+
return obj;
|
|
949
|
+
}
|
|
950
|
+
function resolveProjects(config, configDir) {
|
|
951
|
+
return config.languages.map((lang) => {
|
|
952
|
+
const grammarPath = resolve(configDir, lang.grammar);
|
|
953
|
+
const projectDir = dirname(grammarPath);
|
|
954
|
+
const outDir = lang.out ? resolve(configDir, lang.out) : resolve(projectDir, "generated");
|
|
955
|
+
return {
|
|
956
|
+
grammarPath,
|
|
957
|
+
projectDir,
|
|
958
|
+
outDir,
|
|
959
|
+
backend: lang.backend ?? "tree-sitter"
|
|
960
|
+
};
|
|
961
|
+
});
|
|
460
962
|
}
|
|
461
963
|
|
|
462
964
|
//#endregion
|
|
@@ -464,9 +966,29 @@ async function watch() {
|
|
|
464
966
|
const program = new Command();
|
|
465
967
|
program.name("treelsp").description("CLI for treelsp - LSP generator using Tree-sitter").version("0.0.1");
|
|
466
968
|
program.command("init").description("Scaffold a new language project").action(init);
|
|
467
|
-
program.command("generate").description("Generate grammar.js, AST types, and server from language definition").option("-w, --watch", "Watch for changes").action(
|
|
468
|
-
|
|
469
|
-
|
|
969
|
+
program.command("generate").description("Generate grammar.js, AST types, and server from language definition").option("-f, --file <file>", "Path to treelsp-config.json or package.json with treelsp field").option("-w, --watch", "Watch for changes").action(async (options) => {
|
|
970
|
+
try {
|
|
971
|
+
const config = resolveConfig(options.file);
|
|
972
|
+
if (config.configPath) console.log(pc.dim(`Using config: ${config.configPath}\n`));
|
|
973
|
+
await generate(options, config);
|
|
974
|
+
} catch {
|
|
975
|
+
process.exit(1);
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
program.command("build").description("Compile grammar.js to WASM using Tree-sitter CLI").option("-f, --file <file>", "Path to treelsp-config.json or package.json with treelsp field").action(async (options) => {
|
|
979
|
+
try {
|
|
980
|
+
const config = resolveConfig(options.file);
|
|
981
|
+
if (config.configPath) console.log(pc.dim(`Using config: ${config.configPath}\n`));
|
|
982
|
+
await build$1(config);
|
|
983
|
+
} catch {
|
|
984
|
+
process.exit(1);
|
|
985
|
+
}
|
|
986
|
+
});
|
|
987
|
+
program.command("watch").description("Watch mode - re-run generate + build on changes").option("-f, --file <file>", "Path to treelsp-config.json or package.json with treelsp field").action(async (options) => {
|
|
988
|
+
const config = resolveConfig(options.file);
|
|
989
|
+
if (config.configPath) console.log(pc.dim(`Using config: ${config.configPath}\n`));
|
|
990
|
+
await watch(config);
|
|
991
|
+
});
|
|
470
992
|
program.parse();
|
|
471
993
|
|
|
472
994
|
//#endregion
|