@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/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 pc from "picocolors";
6
- import { mkdir, writeFile } from "node:fs/promises";
7
- import { resolve } from "node:path";
8
- import { copyFileSync, existsSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
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
- type: "text",
20
- name: "name",
21
- message: "Language name:",
22
- initial: "my-lang",
23
- validate: (value) => value.length > 0 || "Name is required"
24
- }, {
25
- type: "text",
26
- name: "extension",
27
- message: "File extension:",
28
- initial: ".mylang",
29
- validate: (value) => {
30
- if (!value.startsWith(".")) return "Extension must start with a dot";
31
- if (value.length < 2) return "Extension is too short";
32
- return true;
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
- const packageJson = {
50
- name,
51
- version: "0.1.0",
52
- type: "module",
53
- dependencies: { treelsp: "^0.0.1" },
54
- devDependencies: {
55
- "@treelsp/cli": "^0.0.1",
56
- typescript: "^5.7.3"
57
- },
58
- scripts: {
59
- generate: "treelsp generate",
60
- build: "treelsp build",
61
- watch: "treelsp watch"
62
- }
63
- };
64
- await writeFile(resolve(projectDir, "package.json"), JSON.stringify(packageJson, null, 2) + "\n", "utf-8");
65
- const tsconfig = {
66
- extends: "treelsp/tsconfig.base.json",
67
- compilerOptions: { outDir: "./dist" },
68
- include: ["grammar.ts"]
69
- };
70
- await writeFile(resolve(projectDir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2) + "\n", "utf-8");
71
- const capitalizedName = name.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join("");
72
- const grammarTemplate = `/**
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
- await writeFile(resolve(projectDir, "grammar.ts"), grammarTemplate, "utf-8");
168
- const gitignore = `node_modules
169
- dist
170
- generated/
171
- *.wasm
172
- *.log
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
- await writeFile(resolve(projectDir, ".gitignore"), gitignore, "utf-8");
177
- const readme = `# ${capitalizedName}
178
-
179
- A language definition for treelsp.
180
-
181
- ## Getting Started
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
- \`\`\`bash
200
- npm run watch # Auto-rebuild on changes
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
- ## Project Structure
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
- - \`grammar.ts\` - Language definition (grammar, semantic, validation, LSP)
206
- - \`generated/\` - Generated files (grammar.js, WASM, types)
207
- - \`package.json\` - Project dependencies
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
- ## Documentation
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
- - [treelsp Documentation](https://github.com/yourusername/treelsp)
212
- - [Tree-sitter](https://tree-sitter.github.io/tree-sitter/)
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
- ## License
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
- MIT
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
- await writeFile(resolve(projectDir, "README.md"), readme, "utf-8");
219
- spinner.succeed("Project created!");
220
- console.log(pc.dim("\nNext steps:"));
221
- console.log(pc.dim(` cd ${name}`));
222
- console.log(pc.dim(" npm install"));
223
- console.log(pc.dim(" Edit grammar.ts to define your language"));
224
- console.log(pc.dim(" npm run generate"));
225
- console.log(pc.dim(" npm run build"));
226
- } catch (error) {
227
- spinner.fail("Failed to create project");
228
- if (error instanceof Error) console.error(pc.red(`\n${error.message}`));
229
- process.exit(1);
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
- async function generate(options) {
236
- const spinner = ora("Loading grammar.ts...").start();
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
- const grammarPath = resolve(process.cwd(), "grammar.ts");
239
- if (!existsSync(grammarPath)) {
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
- process.exit(1);
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
- process.exit(1);
634
+ throw new Error("Invalid language definition");
251
635
  }
252
636
  spinner.text = `Generating code for ${definition.name}...`;
253
- const grammarJs = generateGrammar(definition);
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 highlightsSCM = generateHighlights(definition);
257
- const localsSCM = generateLocals(definition);
258
- const genDir = resolve(process.cwd(), "generated");
259
- const queriesDir = resolve(genDir, "queries");
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(genDir, "grammar.js"), grammarJs, "utf-8"),
263
- writeFile(resolve(genDir, "ast.ts"), astTypes, "utf-8"),
264
- writeFile(resolve(genDir, "treelsp.json"), manifest, "utf-8"),
265
- writeFile(resolve(queriesDir, "highlights.scm"), highlightsSCM, "utf-8"),
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
- spinner.succeed("Generated grammar.js, ast.ts, treelsp.json, queries/highlights.scm, queries/locals.scm");
269
- if (!options.watch) console.log(pc.dim("\nNext step: Run \"treelsp build\" to compile grammar to WASM"));
651
+ const outLabel = relative(process.cwd(), project.outDir) || project.outDir;
652
+ spinner.succeed(`Generated ${definition.name} -> ${outLabel}/`);
270
653
  } catch (error) {
271
- spinner.fail("Generation failed");
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.ts"));
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("\nPermission denied writing to generated/"));
277
- console.log(pc.dim("Check file permissions in the current directory"));
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
- process.exit(1);
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
- async function build$1() {
286
- const spinner = ora("Checking prerequisites...").start();
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 grammarPath = resolve(process.cwd(), "generated", "grammar.js");
289
- if (!existsSync(grammarPath)) {
290
- spinner.fail("generated/grammar.js not found");
291
- console.log(pc.dim("\nRun \"treelsp generate\" first to create grammar.js"));
292
- process.exit(1);
293
- }
294
- try {
295
- execSync("tree-sitter --version", { stdio: "ignore" });
296
- } catch {
297
- spinner.fail("tree-sitter CLI not found");
298
- console.log(pc.dim("\nInstall tree-sitter CLI:"));
299
- console.log(pc.dim(" npm install -g tree-sitter-cli"));
300
- console.log(pc.dim(" or: cargo install tree-sitter-cli"));
301
- process.exit(1);
302
- }
303
- spinner.text = "Generating C parser...";
304
- const genDir = resolve(process.cwd(), "generated");
305
- const genPkgJson = resolve(genDir, "package.json");
306
- const hadPkgJson = existsSync(genPkgJson);
307
- if (!hadPkgJson) writeFileSync(genPkgJson, "{\"type\":\"commonjs\"}\n");
308
- try {
309
- execSync("tree-sitter generate generated/grammar.js", {
310
- stdio: "pipe",
311
- cwd: process.cwd()
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
- for (const f of readdirSync(cwd)) if (f.startsWith("tree-sitter-") && f.endsWith(".pc")) rmSync(resolve(cwd, f), { force: true });
350
- spinner.text = "Bundling language server...";
351
- const serverEntry = [
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 { resolve, dirname } from 'node:path';`,
354
- `import { fileURLToPath } from 'node:url';`,
739
+ `import { ${runtimeImport.className} } from '${runtimeImport.specifier}';`,
740
+ `import { resolve } from 'node:path';`,
355
741
  `import definition from './grammar.ts';`,
356
742
  ``,
357
- `const __dirname = dirname(fileURLToPath(import.meta.url));`,
358
- `const wasmPath = resolve(__dirname, 'grammar.wasm');`,
743
+ `const parserPath = resolve(__dirname, 'grammar.wasm');`,
359
744
  ``,
360
- `startStdioServer({ definition, wasmPath });`
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(genDir, "server.bundle.cjs");
749
+ const bundlePath = resolve(project.outDir, "server.bundle.cjs");
365
750
  await build({
366
751
  stdin: {
367
752
  contents: serverEntry,
368
- resolveDir: process.cwd(),
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: "silent"
762
+ logLevel: "warning",
763
+ logOverride: { "empty-import-meta": "silent" }
378
764
  });
379
- const { readFileSync } = await import("node:fs");
380
- let bundleCode = readFileSync(bundlePath, "utf-8");
381
- bundleCode = bundleCode.replace(/var import_meta\s*=\s*\{\s*\};/, "var import_meta = { url: require(\"url\").pathToFileURL(__filename).href };");
382
- writeFileSync(bundlePath, bundleCode);
383
- const treelspNodeModules = resolve(treelspPkg, "node_modules");
384
- const tsWasmSrc = resolve(treelspNodeModules, "web-tree-sitter", "tree-sitter.wasm");
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("Build failed");
772
+ spinner.fail(`Build failed for ${label}`);
394
773
  if (error instanceof Error) {
774
+ const esbuildError = error;
395
775
  const execError = error;
396
- if (execError.stderr) {
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("\nTree-sitter error:"));
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
- process.exit(1);
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
- if (!existsSync("grammar.ts")) {
416
- console.error(pc.red("Could not find grammar.ts in current directory"));
417
- console.log(pc.dim("\nRun \"treelsp init\" to create a new language project"));
418
- process.exit(1);
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("grammar.ts", {
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", (path) => {
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
- console.log(pc.dim(`\n${path} changed`));
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 generate({});
438
- await build$1();
439
- console.log(pc.green("Rebuild successful\n"));
440
- } catch (_error) {
441
- console.log(pc.red("Rebuild failed\n"));
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 generate({});
454
- await build$1();
455
- console.log(pc.green("✓ Initial build successful\n"));
456
- } catch (_error) {
457
- console.log(pc.red("✗ Initial build failed\n"));
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
- console.log(pc.dim("Watching for changes..."));
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(generate);
468
- program.command("build").description("Compile grammar.js to WASM using Tree-sitter CLI").action(build$1);
469
- program.command("watch").description("Watch mode - re-run generate + build on changes").action(watch);
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