@treelsp/cli 0.0.1 → 0.0.2
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 +139 -0
- package/bin/treelsp.js +2 -0
- package/dist/index.js +361 -157
- package/package.json +9 -3
package/README.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# @treelsp/cli
|
|
2
|
+
|
|
3
|
+
CLI tool for [treelsp](https://github.com/dhrubomoy/treelsp) — generate Tree-sitter grammars, WASM parsers, and language servers from TypeScript definitions.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -D @treelsp/cli
|
|
9
|
+
# or
|
|
10
|
+
pnpm add -D @treelsp/cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Commands
|
|
14
|
+
|
|
15
|
+
### `treelsp init`
|
|
16
|
+
|
|
17
|
+
Scaffold a new language project interactively.
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
treelsp init
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Prompts for a language name and file extension, then creates a project directory with:
|
|
24
|
+
- `grammar.ts` — language definition template
|
|
25
|
+
- `package.json` — dependencies and scripts
|
|
26
|
+
- `tsconfig.json` — TypeScript configuration
|
|
27
|
+
- `.gitignore`
|
|
28
|
+
|
|
29
|
+
### `treelsp generate`
|
|
30
|
+
|
|
31
|
+
Generate grammar.js, AST types, manifest, and syntax highlighting queries from a language definition.
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
Usage: treelsp generate [options]
|
|
35
|
+
|
|
36
|
+
Options:
|
|
37
|
+
-f, --file <file> path to treelsp-config.json or package.json with treelsp field
|
|
38
|
+
-w, --watch watch for changes
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Output files** (written to the output directory, default `generated/`):
|
|
42
|
+
- `grammar.js` — Tree-sitter grammar (CommonJS)
|
|
43
|
+
- `ast.ts` — typed AST interfaces
|
|
44
|
+
- `treelsp.json` — manifest for VS Code extension discovery
|
|
45
|
+
- `queries/highlights.scm` — Tree-sitter syntax highlighting
|
|
46
|
+
- `queries/locals.scm` — Tree-sitter scope/locals
|
|
47
|
+
|
|
48
|
+
### `treelsp build`
|
|
49
|
+
|
|
50
|
+
Compile the generated grammar to WASM and bundle the language server.
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
Usage: treelsp build [options]
|
|
54
|
+
|
|
55
|
+
Options:
|
|
56
|
+
-f, --file <file> path to treelsp-config.json or package.json with treelsp field
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Requires the [tree-sitter CLI](https://github.com/tree-sitter/tree-sitter/blob/master/cli/README.md) to be installed (`npm install -g tree-sitter-cli` or `cargo install tree-sitter-cli`).
|
|
60
|
+
|
|
61
|
+
**Output files:**
|
|
62
|
+
- `grammar.wasm` — compiled WebAssembly parser
|
|
63
|
+
- `server.bundle.cjs` — self-contained language server bundle
|
|
64
|
+
- `tree-sitter.wasm` — web-tree-sitter runtime
|
|
65
|
+
|
|
66
|
+
### `treelsp watch`
|
|
67
|
+
|
|
68
|
+
Watch mode — re-runs `generate` + `build` automatically when grammar files change.
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
Usage: treelsp watch [options]
|
|
72
|
+
|
|
73
|
+
Options:
|
|
74
|
+
-f, --file <file> path to treelsp-config.json or package.json with treelsp field
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Configuration
|
|
78
|
+
|
|
79
|
+
By default, treelsp looks for `grammar.ts` in the current directory and outputs to `generated/`. For multi-language projects, create a config file to specify all languages.
|
|
80
|
+
|
|
81
|
+
### Config discovery order
|
|
82
|
+
|
|
83
|
+
When `-f` is not provided, the CLI searches from the current directory upward for:
|
|
84
|
+
|
|
85
|
+
1. `treelsp-config.json`
|
|
86
|
+
2. A `"treelsp"` field in `package.json`
|
|
87
|
+
3. Falls back to legacy mode (`grammar.ts` in cwd)
|
|
88
|
+
|
|
89
|
+
### `treelsp-config.json`
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"languages": [
|
|
94
|
+
{ "grammar": "mini-lang/grammar.ts" },
|
|
95
|
+
{ "grammar": "schema-lang/grammar.ts" }
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### `package.json`
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"treelsp": {
|
|
105
|
+
"languages": [
|
|
106
|
+
{ "grammar": "src/grammar.ts", "out": "src/generated" }
|
|
107
|
+
]
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Schema
|
|
113
|
+
|
|
114
|
+
| Field | Type | Required | Description |
|
|
115
|
+
|-------|------|----------|-------------|
|
|
116
|
+
| `languages` | array | yes | List of language projects |
|
|
117
|
+
| `languages[].grammar` | string | yes | Path to `grammar.ts`, relative to the config file |
|
|
118
|
+
| `languages[].out` | string | no | Output directory (default: `<grammar dir>/generated`) |
|
|
119
|
+
|
|
120
|
+
## Typical Workflow
|
|
121
|
+
|
|
122
|
+
**Single language (no config needed):**
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
treelsp init # scaffold project
|
|
126
|
+
cd my-lang
|
|
127
|
+
npm install
|
|
128
|
+
treelsp generate # generate grammar.js, ast.ts, etc.
|
|
129
|
+
treelsp build # compile WASM + bundle server
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Multi-language monorepo:**
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
# treelsp-config.json at repo root
|
|
136
|
+
treelsp generate # generates all languages
|
|
137
|
+
treelsp build # builds all languages
|
|
138
|
+
treelsp watch # watches all grammar files
|
|
139
|
+
```
|
package/bin/treelsp.js
ADDED
package/dist/index.js
CHANGED
|
@@ -1,18 +1,31 @@
|
|
|
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
31
|
const answers = await prompts([{
|
|
@@ -46,13 +59,15 @@ async function init() {
|
|
|
46
59
|
process.exit(1);
|
|
47
60
|
}
|
|
48
61
|
await mkdir(projectDir);
|
|
62
|
+
const version = await getCliVersion();
|
|
63
|
+
const versionRange = `^${version}`;
|
|
49
64
|
const packageJson = {
|
|
50
65
|
name,
|
|
51
66
|
version: "0.1.0",
|
|
52
67
|
type: "module",
|
|
53
|
-
dependencies: { treelsp:
|
|
68
|
+
dependencies: { treelsp: versionRange },
|
|
54
69
|
devDependencies: {
|
|
55
|
-
"@treelsp/cli":
|
|
70
|
+
"@treelsp/cli": versionRange,
|
|
56
71
|
typescript: "^5.7.3"
|
|
57
72
|
},
|
|
58
73
|
scripts: {
|
|
@@ -63,8 +78,13 @@ async function init() {
|
|
|
63
78
|
};
|
|
64
79
|
await writeFile(resolve(projectDir, "package.json"), JSON.stringify(packageJson, null, 2) + "\n", "utf-8");
|
|
65
80
|
const tsconfig = {
|
|
66
|
-
|
|
67
|
-
|
|
81
|
+
compilerOptions: {
|
|
82
|
+
target: "ES2022",
|
|
83
|
+
module: "NodeNext",
|
|
84
|
+
moduleResolution: "NodeNext",
|
|
85
|
+
strict: true,
|
|
86
|
+
outDir: "./dist"
|
|
87
|
+
},
|
|
68
88
|
include: ["grammar.ts"]
|
|
69
89
|
};
|
|
70
90
|
await writeFile(resolve(projectDir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2) + "\n", "utf-8");
|
|
@@ -230,142 +250,189 @@ MIT
|
|
|
230
250
|
}
|
|
231
251
|
}
|
|
232
252
|
|
|
253
|
+
//#endregion
|
|
254
|
+
//#region src/backends.ts
|
|
255
|
+
const treeSitterBin = resolve(dirname(fileURLToPath(import.meta.resolve("tree-sitter-cli/cli.js"))), "tree-sitter");
|
|
256
|
+
const BACKENDS = {
|
|
257
|
+
"tree-sitter": async () => {
|
|
258
|
+
const { TreeSitterCodegen } = await import("treelsp/codegen/tree-sitter");
|
|
259
|
+
return new TreeSitterCodegen({ treeSitterBin });
|
|
260
|
+
},
|
|
261
|
+
"lezer": async () => {
|
|
262
|
+
const { LezerCodegen } = await import("treelsp/codegen/lezer");
|
|
263
|
+
return new LezerCodegen();
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
/**
|
|
267
|
+
* Resolve a codegen backend by identifier.
|
|
268
|
+
* Defaults to "tree-sitter" if not specified.
|
|
269
|
+
*/
|
|
270
|
+
async function getCodegenBackend(id = "tree-sitter") {
|
|
271
|
+
const factory = BACKENDS[id];
|
|
272
|
+
if (!factory) {
|
|
273
|
+
const available = Object.keys(BACKENDS).join(", ");
|
|
274
|
+
throw new Error(`Unknown parser backend: "${id}". Available backends: ${available}`);
|
|
275
|
+
}
|
|
276
|
+
return factory();
|
|
277
|
+
}
|
|
278
|
+
|
|
233
279
|
//#endregion
|
|
234
280
|
//#region src/commands/generate.ts
|
|
235
|
-
|
|
236
|
-
|
|
281
|
+
/**
|
|
282
|
+
* Generate code for a single language project.
|
|
283
|
+
*/
|
|
284
|
+
async function generateProject(project) {
|
|
285
|
+
const label = relative(process.cwd(), project.grammarPath) || project.grammarPath;
|
|
286
|
+
const spinner = ora(`Loading ${label}...`).start();
|
|
237
287
|
try {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
spinner.fail("Could not find grammar.ts in current directory");
|
|
288
|
+
if (!existsSync(project.grammarPath)) {
|
|
289
|
+
spinner.fail(`Could not find ${label}`);
|
|
241
290
|
console.log(pc.dim("\nRun \"treelsp init\" to create a new language project"));
|
|
242
|
-
|
|
291
|
+
throw new Error(`Grammar file not found: ${project.grammarPath}`);
|
|
292
|
+
}
|
|
293
|
+
const tmpPath = project.grammarPath.replace(/\.ts$/, ".tmp.mjs");
|
|
294
|
+
await build({
|
|
295
|
+
entryPoints: [project.grammarPath],
|
|
296
|
+
bundle: true,
|
|
297
|
+
format: "esm",
|
|
298
|
+
platform: "node",
|
|
299
|
+
outfile: tmpPath,
|
|
300
|
+
packages: "external",
|
|
301
|
+
logLevel: "silent"
|
|
302
|
+
});
|
|
303
|
+
let definition;
|
|
304
|
+
try {
|
|
305
|
+
const grammarUrl = pathToFileURL(tmpPath).href;
|
|
306
|
+
const mod = await import(grammarUrl);
|
|
307
|
+
definition = mod.default;
|
|
308
|
+
} finally {
|
|
309
|
+
try {
|
|
310
|
+
unlinkSync(tmpPath);
|
|
311
|
+
} catch {}
|
|
243
312
|
}
|
|
244
|
-
const grammarUrl = pathToFileURL(grammarPath).href;
|
|
245
|
-
const module = await import(grammarUrl);
|
|
246
|
-
const definition = module.default;
|
|
247
313
|
if (!definition || !definition.name || !definition.grammar) {
|
|
248
314
|
spinner.fail("Invalid language definition");
|
|
249
315
|
console.log(pc.dim("\nEnsure grammar.ts exports a valid language definition using defineLanguage()"));
|
|
250
|
-
|
|
316
|
+
throw new Error("Invalid language definition");
|
|
251
317
|
}
|
|
252
318
|
spinner.text = `Generating code for ${definition.name}...`;
|
|
253
|
-
const
|
|
319
|
+
const backend = await getCodegenBackend(project.backend);
|
|
320
|
+
const artifacts = backend.generate(definition);
|
|
254
321
|
const astTypes = generateAstTypes(definition);
|
|
255
322
|
const manifest = generateManifest(definition);
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
const
|
|
259
|
-
const
|
|
260
|
-
await mkdir(queriesDir, { recursive: true });
|
|
323
|
+
const textmateGrammar = generateTextmate(definition);
|
|
324
|
+
await mkdir(project.outDir, { recursive: true });
|
|
325
|
+
const artifactDirs = new Set(artifacts.map((a) => a.path.includes("/") ? resolve(project.outDir, a.path, "..") : null).filter((d) => d !== null));
|
|
326
|
+
for (const dir of artifactDirs) await mkdir(dir, { recursive: true });
|
|
261
327
|
await Promise.all([
|
|
262
|
-
writeFile(resolve(
|
|
263
|
-
writeFile(resolve(
|
|
264
|
-
writeFile(resolve(
|
|
265
|
-
writeFile(resolve(
|
|
266
|
-
writeFile(resolve(queriesDir, "locals.scm"), localsSCM, "utf-8")
|
|
328
|
+
...artifacts.map((a) => writeFile(resolve(project.outDir, a.path), a.content, "utf-8")),
|
|
329
|
+
writeFile(resolve(project.outDir, "ast.ts"), astTypes, "utf-8"),
|
|
330
|
+
writeFile(resolve(project.outDir, "treelsp.json"), manifest, "utf-8"),
|
|
331
|
+
writeFile(resolve(project.outDir, "syntax.tmLanguage.json"), textmateGrammar, "utf-8")
|
|
267
332
|
]);
|
|
268
|
-
|
|
269
|
-
|
|
333
|
+
const outLabel = relative(process.cwd(), project.outDir) || project.outDir;
|
|
334
|
+
spinner.succeed(`Generated ${definition.name} -> ${outLabel}/`);
|
|
270
335
|
} catch (error) {
|
|
271
|
-
spinner.fail(
|
|
336
|
+
if (!spinner.isSpinning) {} else spinner.fail(`Generation failed for ${label}`);
|
|
272
337
|
if (error instanceof Error) if (error.message.includes("Cannot find module")) {
|
|
273
|
-
console.error(pc.red("\nFailed to load grammar
|
|
338
|
+
console.error(pc.red("\nFailed to load grammar file"));
|
|
274
339
|
console.log(pc.dim("Ensure the file exists and has no syntax errors"));
|
|
275
340
|
} else if (error.message.includes("EACCES") || error.message.includes("EPERM")) {
|
|
276
|
-
console.error(pc.red(
|
|
277
|
-
console.log(pc.dim("Check file permissions
|
|
341
|
+
console.error(pc.red(`\nPermission denied writing to ${project.outDir}`));
|
|
342
|
+
console.log(pc.dim("Check file permissions"));
|
|
278
343
|
} else console.error(pc.red(`\n${error.message}`));
|
|
279
|
-
|
|
344
|
+
throw error;
|
|
280
345
|
}
|
|
281
346
|
}
|
|
347
|
+
/**
|
|
348
|
+
* Top-level generate command handler.
|
|
349
|
+
*/
|
|
350
|
+
async function generate(options, configResult) {
|
|
351
|
+
for (const project of configResult.projects) await generateProject(project);
|
|
352
|
+
if (!options.watch) console.log(pc.dim("\nNext step: Run \"treelsp build\" to compile grammar to WASM"));
|
|
353
|
+
}
|
|
282
354
|
|
|
283
355
|
//#endregion
|
|
284
356
|
//#region src/commands/build.ts
|
|
285
|
-
|
|
286
|
-
|
|
357
|
+
/**
|
|
358
|
+
* Map from backend id to the runtime import specifier used in the server entry.
|
|
359
|
+
*/
|
|
360
|
+
const BACKEND_RUNTIME_IMPORT = {
|
|
361
|
+
"tree-sitter": {
|
|
362
|
+
specifier: "treelsp/backend/tree-sitter",
|
|
363
|
+
className: "TreeSitterRuntime"
|
|
364
|
+
},
|
|
365
|
+
"lezer": {
|
|
366
|
+
specifier: "treelsp/backend/lezer",
|
|
367
|
+
className: "LezerRuntime"
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
/**
|
|
371
|
+
* Build a single language project: compile parser + bundle server.
|
|
372
|
+
*/
|
|
373
|
+
async function buildProject(project) {
|
|
374
|
+
const label = relative(process.cwd(), project.projectDir) || project.projectDir;
|
|
375
|
+
const spinner = ora(`Building ${label}...`).start();
|
|
287
376
|
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 });
|
|
377
|
+
const backend = await getCodegenBackend(project.backend);
|
|
378
|
+
spinner.text = `Compiling parser for ${label}...`;
|
|
379
|
+
await backend.compile(project.projectDir, project.outDir, { onProgress: (msg) => {
|
|
380
|
+
spinner.text = `${msg} (${label})`;
|
|
381
|
+
} });
|
|
382
|
+
if (backend.cleanupPatterns) {
|
|
383
|
+
const { directories, files, globs } = backend.cleanupPatterns;
|
|
384
|
+
if (directories) for (const dir of directories) {
|
|
385
|
+
const p = resolve(project.projectDir, dir);
|
|
386
|
+
if (existsSync(p)) rmSync(p, {
|
|
387
|
+
recursive: true,
|
|
388
|
+
force: true
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
if (files) for (const file of files) {
|
|
392
|
+
const p = resolve(project.projectDir, file);
|
|
393
|
+
if (existsSync(p)) rmSync(p, { force: true });
|
|
394
|
+
}
|
|
395
|
+
if (globs) for (const pattern of globs) {
|
|
396
|
+
const parts = pattern.split("*");
|
|
397
|
+
if (parts.length === 2) {
|
|
398
|
+
const [prefix, suffix] = parts;
|
|
399
|
+
for (const f of readdirSync(project.projectDir)) if (f.startsWith(prefix) && f.endsWith(suffix)) rmSync(resolve(project.projectDir, f), { force: true });
|
|
400
|
+
}
|
|
401
|
+
}
|
|
348
402
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
403
|
+
spinner.text = `Bundling language server for ${label}...`;
|
|
404
|
+
const runtimeImport = BACKEND_RUNTIME_IMPORT[project.backend];
|
|
405
|
+
if (!runtimeImport) throw new Error(`No runtime import configured for backend "${project.backend}"`);
|
|
406
|
+
let serverEntry;
|
|
407
|
+
if (project.backend === "lezer") {
|
|
408
|
+
const relOut = "./" + relative(project.projectDir, project.outDir).replace(/\\/g, "/");
|
|
409
|
+
serverEntry = [
|
|
410
|
+
`import { startStdioServer } from 'treelsp/server';`,
|
|
411
|
+
`import { ${runtimeImport.className} } from '${runtimeImport.specifier}';`,
|
|
412
|
+
`import definition from './grammar.ts';`,
|
|
413
|
+
`import { parser } from '${relOut}/parser.bundle.js';`,
|
|
414
|
+
`import parserMeta from '${relOut}/parser-meta.json';`,
|
|
415
|
+
``,
|
|
416
|
+
`const backend = new ${runtimeImport.className}(parser, parserMeta);`,
|
|
417
|
+
`startStdioServer({ definition, parserPath: '', backend });`
|
|
418
|
+
].join("\n");
|
|
419
|
+
} else serverEntry = [
|
|
352
420
|
`import { startStdioServer } from 'treelsp/server';`,
|
|
353
|
-
`import {
|
|
354
|
-
`import {
|
|
421
|
+
`import { ${runtimeImport.className} } from '${runtimeImport.specifier}';`,
|
|
422
|
+
`import { resolve } from 'node:path';`,
|
|
355
423
|
`import definition from './grammar.ts';`,
|
|
356
424
|
``,
|
|
357
|
-
`const
|
|
358
|
-
`const wasmPath = resolve(__dirname, 'grammar.wasm');`,
|
|
425
|
+
`const parserPath = resolve(__dirname, 'grammar.wasm');`,
|
|
359
426
|
``,
|
|
360
|
-
`startStdioServer({ definition,
|
|
427
|
+
`startStdioServer({ definition, parserPath, backend: new ${runtimeImport.className}() });`
|
|
361
428
|
].join("\n");
|
|
362
429
|
const treelspServer = import.meta.resolve("treelsp/server");
|
|
363
430
|
const treelspPkg = resolve(new URL(treelspServer).pathname, "..", "..", "..");
|
|
364
|
-
const bundlePath = resolve(
|
|
431
|
+
const bundlePath = resolve(project.outDir, "server.bundle.cjs");
|
|
365
432
|
await build({
|
|
366
433
|
stdin: {
|
|
367
434
|
contents: serverEntry,
|
|
368
|
-
resolveDir:
|
|
435
|
+
resolveDir: project.projectDir,
|
|
369
436
|
loader: "ts"
|
|
370
437
|
},
|
|
371
438
|
bundle: true,
|
|
@@ -374,50 +441,63 @@ async function build$1() {
|
|
|
374
441
|
outfile: bundlePath,
|
|
375
442
|
sourcemap: true,
|
|
376
443
|
nodePaths: [resolve(treelspPkg, "node_modules")],
|
|
377
|
-
logLevel: "
|
|
444
|
+
logLevel: "warning",
|
|
445
|
+
logOverride: { "empty-import-meta": "silent" }
|
|
378
446
|
});
|
|
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"));
|
|
447
|
+
if (backend.getRuntimeFiles) for (const { src, dest } of backend.getRuntimeFiles(treelspPkg)) {
|
|
448
|
+
const destPath = resolve(project.outDir, dest);
|
|
449
|
+
if (existsSync(src) && !existsSync(destPath)) copyFileSync(src, destPath);
|
|
450
|
+
}
|
|
451
|
+
const outLabel = relative(process.cwd(), project.outDir) || project.outDir;
|
|
452
|
+
spinner.succeed(`Built ${label} -> ${outLabel}/`);
|
|
392
453
|
} catch (error) {
|
|
393
|
-
spinner.fail(
|
|
454
|
+
spinner.fail(`Build failed for ${label}`);
|
|
394
455
|
if (error instanceof Error) {
|
|
456
|
+
const esbuildError = error;
|
|
395
457
|
const execError = error;
|
|
396
|
-
if (
|
|
458
|
+
if (esbuildError.errors && esbuildError.errors.length > 0) {
|
|
459
|
+
console.error(pc.red("\nServer bundling failed:"));
|
|
460
|
+
for (const err of esbuildError.errors) {
|
|
461
|
+
const loc = err.location ? ` (${err.location.file}:${err.location.line})` : "";
|
|
462
|
+
console.error(pc.dim(` ${err.text}${loc}`));
|
|
463
|
+
}
|
|
464
|
+
} else if (execError.stderr) {
|
|
397
465
|
const stderr = execError.stderr.toString();
|
|
398
|
-
console.error(pc.red("\
|
|
466
|
+
console.error(pc.red("\nBackend compilation error:"));
|
|
399
467
|
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
468
|
} else console.error(pc.red(`\n${error.message}`));
|
|
406
469
|
}
|
|
407
|
-
|
|
470
|
+
throw error;
|
|
408
471
|
}
|
|
409
472
|
}
|
|
473
|
+
/**
|
|
474
|
+
* Top-level build command handler.
|
|
475
|
+
*/
|
|
476
|
+
async function build$1(configResult) {
|
|
477
|
+
for (const project of configResult.projects) await buildProject(project);
|
|
478
|
+
}
|
|
410
479
|
|
|
411
480
|
//#endregion
|
|
412
481
|
//#region src/commands/watch.ts
|
|
413
|
-
async function watch() {
|
|
482
|
+
async function watch(configResult) {
|
|
414
483
|
console.log(pc.bold("treelsp watch\n"));
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
484
|
+
const { projects } = configResult;
|
|
485
|
+
const projectByGrammar = /* @__PURE__ */ new Map();
|
|
486
|
+
const grammarPaths = [];
|
|
487
|
+
for (const project of projects) {
|
|
488
|
+
if (!existsSync(project.grammarPath)) {
|
|
489
|
+
console.error(pc.red(`Could not find ${relative(process.cwd(), project.grammarPath)}`));
|
|
490
|
+
process.exit(1);
|
|
491
|
+
}
|
|
492
|
+
projectByGrammar.set(project.grammarPath, project);
|
|
493
|
+
grammarPaths.push(project.grammarPath);
|
|
494
|
+
}
|
|
495
|
+
if (projects.length > 1) {
|
|
496
|
+
console.log(pc.dim(`Watching ${String(projects.length)} language projects:`));
|
|
497
|
+
for (const p of projects) console.log(pc.dim(` - ${relative(process.cwd(), p.grammarPath)}`));
|
|
498
|
+
console.log("");
|
|
419
499
|
}
|
|
420
|
-
const watcher = chokidar.watch(
|
|
500
|
+
const watcher = chokidar.watch(grammarPaths, {
|
|
421
501
|
persistent: true,
|
|
422
502
|
awaitWriteFinish: {
|
|
423
503
|
stabilityThreshold: 100,
|
|
@@ -425,20 +505,22 @@ async function watch() {
|
|
|
425
505
|
}
|
|
426
506
|
});
|
|
427
507
|
let isBuilding = false;
|
|
428
|
-
watcher.on("change", (
|
|
508
|
+
watcher.on("change", (changedPath) => {
|
|
429
509
|
(async () => {
|
|
430
510
|
if (isBuilding) {
|
|
431
511
|
console.log(pc.dim("Build in progress, skipping..."));
|
|
432
512
|
return;
|
|
433
513
|
}
|
|
434
|
-
|
|
514
|
+
const project = projectByGrammar.get(changedPath);
|
|
515
|
+
if (!project) return;
|
|
516
|
+
console.log(pc.dim(`\n${relative(process.cwd(), changedPath)} changed`));
|
|
435
517
|
isBuilding = true;
|
|
436
518
|
try {
|
|
437
|
-
await
|
|
438
|
-
await
|
|
439
|
-
console.log(pc.green("
|
|
440
|
-
} catch
|
|
441
|
-
console.log(pc.red("
|
|
519
|
+
await generateProject(project);
|
|
520
|
+
await buildProject(project);
|
|
521
|
+
console.log(pc.green(" Rebuild successful\n"));
|
|
522
|
+
} catch {
|
|
523
|
+
console.log(pc.red(" Rebuild failed\n"));
|
|
442
524
|
} finally {
|
|
443
525
|
isBuilding = false;
|
|
444
526
|
console.log(pc.dim("Watching for changes..."));
|
|
@@ -449,14 +531,116 @@ async function watch() {
|
|
|
449
531
|
console.error(pc.red("Watcher error:"), error instanceof Error ? error.message : String(error));
|
|
450
532
|
});
|
|
451
533
|
console.log(pc.dim("Running initial build...\n"));
|
|
452
|
-
try {
|
|
453
|
-
await
|
|
454
|
-
await
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
534
|
+
for (const project of projects) try {
|
|
535
|
+
await generateProject(project);
|
|
536
|
+
await buildProject(project);
|
|
537
|
+
} catch {
|
|
538
|
+
console.log(pc.red(` ${relative(process.cwd(), project.projectDir)} - FAILED\n`));
|
|
539
|
+
}
|
|
540
|
+
console.log(pc.dim("\nWatching for changes..."));
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
//#endregion
|
|
544
|
+
//#region src/config.ts
|
|
545
|
+
/**
|
|
546
|
+
* Resolve config: use -f flag, auto-discover, or fall back to legacy.
|
|
547
|
+
*/
|
|
548
|
+
function resolveConfig(fileFlag) {
|
|
549
|
+
if (fileFlag) return loadConfigFromFile(fileFlag);
|
|
550
|
+
const discovered = discoverConfig();
|
|
551
|
+
if (discovered) return discovered;
|
|
552
|
+
const cwd = process.cwd();
|
|
553
|
+
return {
|
|
554
|
+
source: "legacy",
|
|
555
|
+
projects: [{
|
|
556
|
+
grammarPath: resolve(cwd, "grammar.ts"),
|
|
557
|
+
projectDir: cwd,
|
|
558
|
+
outDir: resolve(cwd, "generated"),
|
|
559
|
+
backend: "tree-sitter"
|
|
560
|
+
}]
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
function loadConfigFromFile(filePath) {
|
|
564
|
+
const absPath = resolve(filePath);
|
|
565
|
+
if (!existsSync(absPath)) throw new Error(`Config file not found: ${absPath}`);
|
|
566
|
+
const raw = readFileSync(absPath, "utf-8");
|
|
567
|
+
if (basename(absPath) === "package.json") {
|
|
568
|
+
const pkg = JSON.parse(raw);
|
|
569
|
+
const treelspField = pkg["treelsp"];
|
|
570
|
+
if (!treelspField) throw new Error(`No "treelsp" field found in ${absPath}`);
|
|
571
|
+
const config$1 = validateConfig(treelspField, absPath);
|
|
572
|
+
return {
|
|
573
|
+
source: "package.json",
|
|
574
|
+
configPath: absPath,
|
|
575
|
+
projects: resolveProjects(config$1, dirname(absPath))
|
|
576
|
+
};
|
|
458
577
|
}
|
|
459
|
-
|
|
578
|
+
const config = validateConfig(JSON.parse(raw), absPath);
|
|
579
|
+
return {
|
|
580
|
+
source: "file",
|
|
581
|
+
configPath: absPath,
|
|
582
|
+
projects: resolveProjects(config, dirname(absPath))
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
function discoverConfig() {
|
|
586
|
+
let dir = resolve(process.cwd());
|
|
587
|
+
while (true) {
|
|
588
|
+
const configFile = resolve(dir, "treelsp-config.json");
|
|
589
|
+
if (existsSync(configFile)) {
|
|
590
|
+
const raw = readFileSync(configFile, "utf-8");
|
|
591
|
+
const config = validateConfig(JSON.parse(raw), configFile);
|
|
592
|
+
return {
|
|
593
|
+
source: "file",
|
|
594
|
+
configPath: configFile,
|
|
595
|
+
projects: resolveProjects(config, dir)
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
const pkgFile = resolve(dir, "package.json");
|
|
599
|
+
if (existsSync(pkgFile)) {
|
|
600
|
+
const raw = readFileSync(pkgFile, "utf-8");
|
|
601
|
+
const pkg = JSON.parse(raw);
|
|
602
|
+
if (pkg["treelsp"]) {
|
|
603
|
+
const config = validateConfig(pkg["treelsp"], pkgFile);
|
|
604
|
+
return {
|
|
605
|
+
source: "package.json",
|
|
606
|
+
configPath: pkgFile,
|
|
607
|
+
projects: resolveProjects(config, dir)
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
const parent = dirname(dir);
|
|
612
|
+
if (parent === dir) break;
|
|
613
|
+
dir = parent;
|
|
614
|
+
}
|
|
615
|
+
return null;
|
|
616
|
+
}
|
|
617
|
+
function validateConfig(raw, filePath) {
|
|
618
|
+
if (typeof raw !== "object" || raw === null) throw new Error(`Invalid config in ${filePath}: expected an object`);
|
|
619
|
+
const obj = raw;
|
|
620
|
+
if (!Array.isArray(obj["languages"])) throw new Error(`Invalid config in ${filePath}: "languages" must be an array`);
|
|
621
|
+
const languages = obj["languages"];
|
|
622
|
+
if (languages.length === 0) throw new Error(`Invalid config in ${filePath}: "languages" must not be empty`);
|
|
623
|
+
for (let i = 0; i < languages.length; i++) {
|
|
624
|
+
const lang = languages[i];
|
|
625
|
+
if (typeof lang !== "object" || lang === null) throw new Error(`Invalid config in ${filePath}: languages[${String(i)}] must be an object`);
|
|
626
|
+
const langObj = lang;
|
|
627
|
+
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`);
|
|
628
|
+
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`);
|
|
629
|
+
}
|
|
630
|
+
return obj;
|
|
631
|
+
}
|
|
632
|
+
function resolveProjects(config, configDir) {
|
|
633
|
+
return config.languages.map((lang) => {
|
|
634
|
+
const grammarPath = resolve(configDir, lang.grammar);
|
|
635
|
+
const projectDir = dirname(grammarPath);
|
|
636
|
+
const outDir = lang.out ? resolve(configDir, lang.out) : resolve(projectDir, "generated");
|
|
637
|
+
return {
|
|
638
|
+
grammarPath,
|
|
639
|
+
projectDir,
|
|
640
|
+
outDir,
|
|
641
|
+
backend: lang.backend ?? "tree-sitter"
|
|
642
|
+
};
|
|
643
|
+
});
|
|
460
644
|
}
|
|
461
645
|
|
|
462
646
|
//#endregion
|
|
@@ -464,9 +648,29 @@ async function watch() {
|
|
|
464
648
|
const program = new Command();
|
|
465
649
|
program.name("treelsp").description("CLI for treelsp - LSP generator using Tree-sitter").version("0.0.1");
|
|
466
650
|
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
|
-
|
|
651
|
+
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) => {
|
|
652
|
+
try {
|
|
653
|
+
const config = resolveConfig(options.file);
|
|
654
|
+
if (config.configPath) console.log(pc.dim(`Using config: ${config.configPath}\n`));
|
|
655
|
+
await generate(options, config);
|
|
656
|
+
} catch {
|
|
657
|
+
process.exit(1);
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
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) => {
|
|
661
|
+
try {
|
|
662
|
+
const config = resolveConfig(options.file);
|
|
663
|
+
if (config.configPath) console.log(pc.dim(`Using config: ${config.configPath}\n`));
|
|
664
|
+
await build$1(config);
|
|
665
|
+
} catch {
|
|
666
|
+
process.exit(1);
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
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) => {
|
|
670
|
+
const config = resolveConfig(options.file);
|
|
671
|
+
if (config.configPath) console.log(pc.dim(`Using config: ${config.configPath}\n`));
|
|
672
|
+
await watch(config);
|
|
673
|
+
});
|
|
470
674
|
program.parse();
|
|
471
675
|
|
|
472
676
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@treelsp/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
7
|
"description": "CLI tool for treelsp - LSP generator using Tree-sitter",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"bin": {
|
|
10
|
-
"treelsp": "./
|
|
10
|
+
"treelsp": "./bin/treelsp.js"
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
13
|
"dist",
|
|
14
|
+
"bin",
|
|
14
15
|
"README.md"
|
|
15
16
|
],
|
|
16
17
|
"keywords": [
|
|
@@ -21,6 +22,9 @@
|
|
|
21
22
|
],
|
|
22
23
|
"author": "Dhrubo Moy",
|
|
23
24
|
"license": "MIT",
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=20"
|
|
27
|
+
},
|
|
24
28
|
"repository": {
|
|
25
29
|
"type": "git",
|
|
26
30
|
"url": "https://github.com/dhrubomoy/treelsp.git",
|
|
@@ -29,13 +33,15 @@
|
|
|
29
33
|
"homepage": "https://github.com/dhrubomoy/treelsp#readme",
|
|
30
34
|
"bugs": "https://github.com/dhrubomoy/treelsp/issues",
|
|
31
35
|
"dependencies": {
|
|
36
|
+
"@lezer/generator": "^1.8.0",
|
|
32
37
|
"chokidar": "^4.0.3",
|
|
33
38
|
"commander": "^12.1.0",
|
|
34
39
|
"esbuild": "^0.27.3",
|
|
35
40
|
"ora": "^8.1.1",
|
|
36
41
|
"picocolors": "^1.1.1",
|
|
37
42
|
"prompts": "^2.4.2",
|
|
38
|
-
"
|
|
43
|
+
"tree-sitter-cli": "^0.26.5",
|
|
44
|
+
"treelsp": "0.0.2"
|
|
39
45
|
},
|
|
40
46
|
"devDependencies": {
|
|
41
47
|
"@types/prompts": "^2.4.9",
|