@hydranium/cli 1.0.0-next.30 → 1.0.0-next.301
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 +75 -204
- package/bin/hydranium-cli.js +13 -0
- package/heap-analysis/heap/core/load.mjs +2 -2
- package/lib/cli-version.js.map +1 -1
- package/lib/cli.js +7 -2
- package/lib/cli.js.map +1 -1
- package/lib/commands/analyze-heap-args.js.map +1 -1
- package/lib/commands/analyze-heap.d.ts +3 -0
- package/lib/commands/analyze-heap.d.ts.map +1 -1
- package/lib/commands/analyze-heap.js +2 -1
- package/lib/commands/analyze-heap.js.map +1 -1
- package/lib/commands/ast-ground-truth-args.d.ts +3 -2
- package/lib/commands/ast-ground-truth-args.d.ts.map +1 -1
- package/lib/commands/ast-ground-truth-args.js +11 -9
- package/lib/commands/ast-ground-truth-args.js.map +1 -1
- package/lib/commands/ast-ground-truth-driver.js +2 -1
- package/lib/commands/ast-ground-truth-driver.js.map +1 -1
- package/lib/commands/ast-ground-truth.js +1 -1
- package/lib/commands/ast-ground-truth.js.map +1 -1
- package/lib/commands/client-session.d.ts +17 -0
- package/lib/commands/client-session.d.ts.map +1 -0
- package/lib/commands/client-session.js +27 -0
- package/lib/commands/client-session.js.map +1 -0
- package/lib/commands/create-args.d.ts +17 -0
- package/lib/commands/create-args.d.ts.map +1 -0
- package/lib/commands/create-args.js +53 -0
- package/lib/commands/create-args.js.map +1 -0
- package/lib/commands/create.d.ts +38 -0
- package/lib/commands/create.d.ts.map +1 -0
- package/lib/commands/create.js +43 -0
- package/lib/commands/create.js.map +1 -0
- package/lib/commands/delete-args.d.ts +17 -0
- package/lib/commands/delete-args.d.ts.map +1 -0
- package/lib/commands/delete-args.js +49 -0
- package/lib/commands/delete-args.js.map +1 -0
- package/lib/commands/delete.d.ts +30 -0
- package/lib/commands/delete.d.ts.map +1 -0
- package/lib/commands/delete.js +32 -0
- package/lib/commands/delete.js.map +1 -0
- package/lib/commands/generate-ast-builder.d.ts +49 -0
- package/lib/commands/generate-ast-builder.d.ts.map +1 -0
- package/lib/commands/generate-ast-builder.js +200 -0
- package/lib/commands/generate-ast-builder.js.map +1 -0
- package/lib/commands/generate-transfer-model-args.d.ts.map +1 -1
- package/lib/commands/generate-transfer-model-args.js +11 -1
- package/lib/commands/generate-transfer-model-args.js.map +1 -1
- package/lib/commands/generate-transfer-model-config.d.ts.map +1 -1
- package/lib/commands/generate-transfer-model-config.js +6 -3
- package/lib/commands/generate-transfer-model-config.js.map +1 -1
- package/lib/commands/generate-transfer-model.d.ts +9 -0
- package/lib/commands/generate-transfer-model.d.ts.map +1 -1
- package/lib/commands/generate-transfer-model.js +18 -4
- package/lib/commands/generate-transfer-model.js.map +1 -1
- package/lib/commands/harness-args.d.ts +44 -42
- package/lib/commands/harness-args.d.ts.map +1 -1
- package/lib/commands/harness-args.js +109 -27
- package/lib/commands/harness-args.js.map +1 -1
- package/lib/commands/headless-harness.d.ts +37 -20
- package/lib/commands/headless-harness.d.ts.map +1 -1
- package/lib/commands/headless-harness.js +68 -17
- package/lib/commands/headless-harness.js.map +1 -1
- package/lib/commands/init-args.js +6 -6
- package/lib/commands/init-args.js.map +1 -1
- package/lib/commands/init-prompt.d.ts.map +1 -1
- package/lib/commands/init-templates.d.ts +8 -10
- package/lib/commands/init-templates.d.ts.map +1 -1
- package/lib/commands/init-templates.js +261 -167
- package/lib/commands/init-templates.js.map +1 -1
- package/lib/commands/init-wizard.js +4 -4
- package/lib/commands/init-wizard.js.map +1 -1
- package/lib/commands/init-workspace.d.ts +13 -3
- package/lib/commands/init-workspace.d.ts.map +1 -1
- package/lib/commands/init-workspace.js +21 -6
- package/lib/commands/init-workspace.js.map +1 -1
- package/lib/commands/init.d.ts +16 -10
- package/lib/commands/init.d.ts.map +1 -1
- package/lib/commands/init.js +17 -15
- package/lib/commands/init.js.map +1 -1
- package/lib/commands/lint-grammar-args.d.ts +3 -2
- package/lib/commands/lint-grammar-args.d.ts.map +1 -1
- package/lib/commands/lint-grammar-args.js +13 -10
- package/lib/commands/lint-grammar-args.js.map +1 -1
- package/lib/commands/lint-grammar-driver.js +2 -1
- package/lib/commands/lint-grammar-driver.js.map +1 -1
- package/lib/commands/lint-grammar-report.js.map +1 -1
- package/lib/commands/lint-grammar.js +1 -1
- package/lib/commands/lint-grammar.js.map +1 -1
- package/lib/commands/measure-memory-args.d.ts +3 -2
- package/lib/commands/measure-memory-args.d.ts.map +1 -1
- package/lib/commands/measure-memory-args.js +22 -11
- package/lib/commands/measure-memory-args.js.map +1 -1
- package/lib/commands/measure-memory-driver.js +5 -3
- package/lib/commands/measure-memory-driver.js.map +1 -1
- package/lib/commands/measure-memory.js +1 -1
- package/lib/commands/measure-memory.js.map +1 -1
- package/lib/commands/model-docs-args.d.ts +4 -3
- package/lib/commands/model-docs-args.d.ts.map +1 -1
- package/lib/commands/model-docs-args.js +12 -10
- package/lib/commands/model-docs-args.js.map +1 -1
- package/lib/commands/model-docs-driver.js +2 -1
- package/lib/commands/model-docs-driver.js.map +1 -1
- package/lib/commands/model-docs.js +1 -1
- package/lib/commands/model-docs.js.map +1 -1
- package/lib/commands/projects-args.d.ts.map +1 -1
- package/lib/commands/projects-args.js +5 -5
- package/lib/commands/projects-args.js.map +1 -1
- package/lib/commands/projects.d.ts +3 -4
- package/lib/commands/projects.d.ts.map +1 -1
- package/lib/commands/projects.js +2 -2
- package/lib/commands/projects.js.map +1 -1
- package/lib/commands/query-args.d.ts.map +1 -1
- package/lib/commands/query-args.js +5 -5
- package/lib/commands/query-args.js.map +1 -1
- package/lib/commands/query.d.ts +3 -4
- package/lib/commands/query.d.ts.map +1 -1
- package/lib/commands/query.js +2 -2
- package/lib/commands/query.js.map +1 -1
- package/lib/commands/reflect-args.d.ts +3 -2
- package/lib/commands/reflect-args.d.ts.map +1 -1
- package/lib/commands/reflect-args.js +13 -10
- package/lib/commands/reflect-args.js.map +1 -1
- package/lib/commands/reflect-driver.js +2 -1
- package/lib/commands/reflect-driver.js.map +1 -1
- package/lib/commands/reflect-report.js.map +1 -1
- package/lib/commands/reflect.js +1 -1
- package/lib/commands/reflect.js.map +1 -1
- package/lib/commands/save-args.d.ts.map +1 -1
- package/lib/commands/save-args.js +7 -4
- package/lib/commands/save-args.js.map +1 -1
- package/lib/commands/save.d.ts +8 -4
- package/lib/commands/save.d.ts.map +1 -1
- package/lib/commands/save.js +12 -23
- package/lib/commands/save.js.map +1 -1
- package/lib/commands/validate-args.d.ts +3 -2
- package/lib/commands/validate-args.d.ts.map +1 -1
- package/lib/commands/validate-args.js +13 -10
- package/lib/commands/validate-args.js.map +1 -1
- package/lib/commands/validate-driver.js +2 -1
- package/lib/commands/validate-driver.js.map +1 -1
- package/lib/commands/validate-report.js.map +1 -1
- package/lib/commands/validate.js +1 -1
- package/lib/commands/validate.js.map +1 -1
- package/lib/commands/watch-args.d.ts.map +1 -1
- package/lib/commands/watch-args.js +6 -4
- package/lib/commands/watch-args.js.map +1 -1
- package/lib/commands/watch.d.ts +3 -4
- package/lib/commands/watch.d.ts.map +1 -1
- package/lib/commands/watch.js +20 -2
- package/lib/commands/watch.js.map +1 -1
- package/lib/driver-heap.d.ts +52 -0
- package/lib/driver-heap.d.ts.map +1 -0
- package/lib/driver-heap.js +51 -0
- package/lib/driver-heap.js.map +1 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +3 -0
- package/lib/index.js.map +1 -1
- package/lib/log-level.d.ts +21 -11
- package/lib/log-level.d.ts.map +1 -1
- package/lib/log-level.js +25 -14
- package/lib/log-level.js.map +1 -1
- package/lib/spawn-data-server.d.ts +9 -4
- package/lib/spawn-data-server.d.ts.map +1 -1
- package/lib/spawn-data-server.js +5 -4
- package/lib/spawn-data-server.js.map +1 -1
- package/lib/stderr-connection-logger.d.ts +12 -0
- package/lib/stderr-connection-logger.d.ts.map +1 -0
- package/lib/stderr-connection-logger.js +16 -0
- package/lib/stderr-connection-logger.js.map +1 -0
- package/lib/testing/echo-server.js +9 -1
- package/lib/testing/echo-server.js.map +1 -1
- package/package.json +50 -39
- package/src/cli.ts +7 -2
- package/src/commands/analyze-heap.ts +4 -1
- package/src/commands/ast-ground-truth-args.ts +15 -12
- package/src/commands/ast-ground-truth-driver.ts +2 -1
- package/src/commands/ast-ground-truth.ts +1 -1
- package/src/commands/client-session.ts +33 -0
- package/src/commands/create-args.ts +68 -0
- package/src/commands/create.ts +83 -0
- package/src/commands/delete-args.ts +64 -0
- package/src/commands/delete.ts +61 -0
- package/src/commands/generate-ast-builder.ts +246 -0
- package/src/commands/generate-transfer-model-args.ts +11 -1
- package/src/commands/generate-transfer-model-config.ts +6 -3
- package/src/commands/generate-transfer-model.ts +27 -4
- package/src/commands/harness-args.ts +99 -31
- package/src/commands/headless-harness.ts +86 -26
- package/src/commands/init-args.ts +6 -6
- package/src/commands/init-templates.ts +272 -169
- package/src/commands/init-wizard.ts +4 -4
- package/src/commands/init-workspace.ts +31 -7
- package/src/commands/init.ts +36 -26
- package/src/commands/lint-grammar-args.ts +17 -13
- package/src/commands/lint-grammar-driver.ts +2 -1
- package/src/commands/lint-grammar.ts +1 -1
- package/src/commands/measure-memory-args.ts +27 -14
- package/src/commands/measure-memory-driver.ts +5 -3
- package/src/commands/measure-memory.ts +1 -1
- package/src/commands/model-docs-args.ts +16 -13
- package/src/commands/model-docs-driver.ts +2 -1
- package/src/commands/model-docs.ts +1 -1
- package/src/commands/projects-args.ts +5 -4
- package/src/commands/projects.ts +4 -6
- package/src/commands/query-args.ts +5 -4
- package/src/commands/query.ts +4 -6
- package/src/commands/reflect-args.ts +17 -13
- package/src/commands/reflect-driver.ts +2 -1
- package/src/commands/reflect.ts +1 -1
- package/src/commands/save-args.ts +7 -3
- package/src/commands/save.ts +24 -27
- package/src/commands/validate-args.ts +17 -13
- package/src/commands/validate-driver.ts +2 -1
- package/src/commands/validate.ts +1 -1
- package/src/commands/watch-args.ts +6 -3
- package/src/commands/watch.ts +28 -7
- package/src/driver-heap.ts +65 -0
- package/src/index.ts +3 -0
- package/src/log-level.ts +42 -14
- package/src/spawn-data-server.ts +17 -5
- package/src/stderr-connection-logger.ts +18 -0
- package/src/testing/echo-server.ts +11 -2
|
@@ -82,16 +82,6 @@ export interface InitTemplate {
|
|
|
82
82
|
content: string;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
/**
|
|
86
|
-
* The version every framework package carries before the first release.
|
|
87
|
-
*
|
|
88
|
-
* A scaffold made by a CLI still at this version pins a range the registry
|
|
89
|
-
* cannot serve, so the emitted README keeps its yalc note and `init` keeps its
|
|
90
|
-
* install warning while — and only while — it holds: from a published CLI the
|
|
91
|
-
* derived pins resolve and either note would be false as printed.
|
|
92
|
-
*/
|
|
93
|
-
export const UNPUBLISHED_FRAMEWORK_VERSION = '0.0.0';
|
|
94
|
-
|
|
95
85
|
/**
|
|
96
86
|
* The version `init` pins every `@hydranium/*` dependency at: the scaffolding
|
|
97
87
|
* CLI's own.
|
|
@@ -177,7 +167,7 @@ function columnsFor(composition: InitComposition): number {
|
|
|
177
167
|
|
|
178
168
|
/**
|
|
179
169
|
* A named import on one line while it fits the column budget, else one symbol
|
|
180
|
-
* per line. Emitted files are supposed to pass a
|
|
170
|
+
* per line. Emitted files are supposed to pass a format check as-is, and the
|
|
181
171
|
* generated-module import grows by one symbol per grammar.
|
|
182
172
|
*/
|
|
183
173
|
function importList(symbols: readonly string[], from: string, columns: number, typeOnly = false): string {
|
|
@@ -228,7 +218,12 @@ const HEAD_FRAMEWORK_DEPENDENCIES: Record<InitHead, readonly string[]> = {
|
|
|
228
218
|
const HEAD_THIRD_PARTY_DEPENDENCIES: Record<InitHead, Readonly<Record<string, string>>> = {
|
|
229
219
|
lsp: {
|
|
230
220
|
langium: '4.3.1',
|
|
231
|
-
|
|
221
|
+
// Exact, as are the protocol and transport it pins, so Langium and the
|
|
222
|
+
// framework share their copies; across copies, an error a handler throws
|
|
223
|
+
// or returns loses its code, and a later patch can pin another protocol.
|
|
224
|
+
'vscode-languageserver': '10.0.1',
|
|
225
|
+
'vscode-jsonrpc': '9.0.0',
|
|
226
|
+
'vscode-languageserver-protocol': '3.18.1'
|
|
232
227
|
},
|
|
233
228
|
data: {},
|
|
234
229
|
glsp: {
|
|
@@ -241,6 +236,37 @@ const HEAD_THIRD_PARTY_DEPENDENCIES: Record<InitHead, Readonly<Record<string, st
|
|
|
241
236
|
}
|
|
242
237
|
};
|
|
243
238
|
|
|
239
|
+
/**
|
|
240
|
+
* The exact pins of `heads`, for a workspace root to declare too. npm installs a
|
|
241
|
+
* root's own dependencies at the top of the tree, so another member's version of
|
|
242
|
+
* one nests beneath that member instead of taking the place of the copy this
|
|
243
|
+
* package shares with the framework.
|
|
244
|
+
*/
|
|
245
|
+
export function workspaceRootPins(heads: readonly InitHead[]): ReadonlyArray<readonly [string, string]> {
|
|
246
|
+
return heads
|
|
247
|
+
.flatMap(head => Object.entries(HEAD_THIRD_PARTY_DEPENDENCIES[head]))
|
|
248
|
+
.filter(([, version]) => /^\d+\.\d+\.\d+$/.test(version))
|
|
249
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Whether an npm version is 11.6.0 or later, the first that installs `vitest` 4.1's peer set; `11.6.0-rc.1` is earlier. */
|
|
253
|
+
function installsVitest41(npmVersion: string | undefined): boolean {
|
|
254
|
+
const [release, prerelease] = (npmVersion ?? '0.0.0').split('-', 2);
|
|
255
|
+
const [major, minor, patch] = release.split('.').map(Number);
|
|
256
|
+
return major > 11 || (major === 11 && (minor > 6 || (minor === 6 && (patch > 0 || prerelease === undefined))));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Whether the scaffold holds `vitest` below 4.1, whose peer set crashes npm
|
|
261
|
+
* before 11.6 (every npm a Node 22 release bundles) on the first install. Only a
|
|
262
|
+
* workspace root declaring a newer npm lifts it, and only when `init` runs under
|
|
263
|
+
* an npm known to be newer: npm does not enforce `packageManager`.
|
|
264
|
+
*/
|
|
265
|
+
function pinsVitest(composition: InitComposition): boolean {
|
|
266
|
+
const workspace = composition.packaging.workspace;
|
|
267
|
+
return !(installsVitest41(workspace?.packageManagerNpm) && installsVitest41(workspace?.npmVersion));
|
|
268
|
+
}
|
|
269
|
+
|
|
244
270
|
/** The dependency block for a head set, merged and sorted as npm writes it. */
|
|
245
271
|
function dependencyBlock(composition: InitComposition, indent: string): string {
|
|
246
272
|
const merged: Record<string, string> = {};
|
|
@@ -260,7 +286,7 @@ function dependencyBlock(composition: InitComposition, indent: string): string {
|
|
|
260
286
|
// npm convention, and `npm install` rewrites `package.json` at two spaces
|
|
261
287
|
// regardless — so three would not survive the adopter's first install.
|
|
262
288
|
//
|
|
263
|
-
// The KEY ORDER is
|
|
289
|
+
// The KEY ORDER is the one Oxfmt's `sortPackageJson` canonicalises to, `private`
|
|
264
290
|
// before `license` included. Any other order is rewritten by the scaffolded
|
|
265
291
|
// project's own first `format` run — a diff on a file the adopter never touched
|
|
266
292
|
// — and it makes a byte-compare against a formatted copy of this emission
|
|
@@ -291,16 +317,9 @@ const PACKAGE_JSON = `{
|
|
|
291
317
|
"version": "0.0.0",
|
|
292
318
|
__PRIVATE__ "description": "__NAME__ language server, built with the Hydranium framework.",
|
|
293
319
|
"keywords": [
|
|
294
|
-
|
|
295
|
-
"langium",
|
|
296
|
-
"language-server",
|
|
297
|
-
"__PROJECT_ID__"
|
|
298
|
-
],
|
|
320
|
+
__KEYWORDS__ ],
|
|
299
321
|
"license": "UNLICENSED",
|
|
300
322
|
"author": "",
|
|
301
|
-
"type": "module",
|
|
302
|
-
"main": "lib/index.js",
|
|
303
|
-
"types": "lib/index.d.ts",
|
|
304
323
|
"bin": {
|
|
305
324
|
__BIN__ },
|
|
306
325
|
"files": [
|
|
@@ -308,11 +327,14 @@ __BIN__ },
|
|
|
308
327
|
"src",
|
|
309
328
|
"syntaxes"
|
|
310
329
|
],
|
|
330
|
+
"type": "module",
|
|
331
|
+
"main": "lib/index.js",
|
|
332
|
+
"types": "lib/index.d.ts",
|
|
311
333
|
"scripts": {
|
|
312
334
|
"build": "npm run generate && tsc",
|
|
313
|
-
"clean": "rimraf lib syntaxes src/language-server/generated src/language-server/generated-
|
|
335
|
+
"clean": "rimraf lib syntaxes src/language-server/generated src/language-server/generated-hydranium tsconfig.tsbuildinfo",
|
|
314
336
|
"generate": "npm run langium:generate && npm run generate:transfer-model",
|
|
315
|
-
"generate:transfer-model": "hydranium-cli generate-transfer-model --ast-file src/language-server/generated/ast.ts --augmentation-file src/language-server/ast.ts --out-file src/language-server/generated-
|
|
337
|
+
"generate:transfer-model": "hydranium-cli generate-transfer-model --ast-file src/language-server/generated/ast.ts --augmentation-file src/language-server/ast.ts --out-file src/language-server/generated-hydranium/transfer-model.ts --ast-builder-file src/language-server/generated-hydranium/ast-builder.ts --element-type-name __NAME__Element --terminals-name __NAME__Terminals --regen-command \\"Run: __NPM_RUN__ generate:transfer-model\\"",
|
|
316
338
|
"langium:generate": "langium generate",
|
|
317
339
|
"langium:watch": "langium generate --watch",
|
|
318
340
|
__LINT__ "start": "node lib/main.js --stdio",
|
|
@@ -328,8 +350,8 @@ __DEPENDENCIES__
|
|
|
328
350
|
"@types/node": "^22.0.0",
|
|
329
351
|
"langium-cli": "4.3.0",
|
|
330
352
|
"rimraf": "^5.0.0",
|
|
331
|
-
"typescript": "^
|
|
332
|
-
"vitest": "
|
|
353
|
+
"typescript": "^7.0.2",
|
|
354
|
+
"vitest": "__VITEST__"
|
|
333
355
|
},
|
|
334
356
|
"engines": {
|
|
335
357
|
"node": ">=22.13"
|
|
@@ -359,7 +381,7 @@ function npmRun(composition: InitComposition): string {
|
|
|
359
381
|
* name. Without the second key a scaffolded project has no command line that
|
|
360
382
|
* reaches its own data head at all.
|
|
361
383
|
*
|
|
362
|
-
* Alphabetical, because `
|
|
384
|
+
* Alphabetical, because Oxfmt's `sortPackageJson` sorts `bin` and any other
|
|
363
385
|
* order is rewritten by the scaffolded project's first `format` run.
|
|
364
386
|
*/
|
|
365
387
|
function binBlock(composition: InitComposition): string {
|
|
@@ -374,15 +396,32 @@ function binBlock(composition: InitComposition): string {
|
|
|
374
396
|
function packageJson(composition: InitComposition): string {
|
|
375
397
|
const { scope } = composition.packaging;
|
|
376
398
|
const packageName = scope === undefined ? composition.projectId : `${scope}/${composition.projectId}`;
|
|
377
|
-
|
|
399
|
+
const keywords =
|
|
400
|
+
['hydranium', 'langium', 'language-server', composition.projectId]
|
|
401
|
+
.sort()
|
|
402
|
+
.map(keyword => ` ${JSON.stringify(keyword)}`)
|
|
403
|
+
.join(',\n') + '\n';
|
|
404
|
+
// Emitted only where a root lint config says the repo lints. The absence is
|
|
378
405
|
// the dangerous direction: a task runner runs a script only where one is
|
|
379
406
|
// declared, so a package with no `lint` is SKIPPED rather than reported, and
|
|
380
407
|
// that reads as a clean lint. A wrong invocation is one visible line to edit.
|
|
381
|
-
const
|
|
408
|
+
const workspace = composition.packaging.workspace;
|
|
409
|
+
const oxlintConfig =
|
|
410
|
+
workspace?.oxlintConfig === undefined
|
|
411
|
+
? undefined
|
|
412
|
+
: `${'../'.repeat(workspace.targetPath.split('/').length)}${workspace.oxlintConfig}`;
|
|
413
|
+
const lint =
|
|
414
|
+
workspace?.oxlintConfig !== undefined
|
|
415
|
+
? ` "lint": "oxlint --report-unused-disable-directives --format stylish --config ${oxlintConfig} src test --max-warnings 0",\n`
|
|
416
|
+
: workspace?.eslintConfig !== undefined
|
|
417
|
+
? ' "lint": "eslint src test --max-warnings 0",\n'
|
|
418
|
+
: '';
|
|
382
419
|
return project(PACKAGE_JSON, composition)
|
|
383
420
|
.replace('__PACKAGE_NAME__', packageName)
|
|
421
|
+
.replace('__KEYWORDS__', keywords)
|
|
384
422
|
.replace('__BIN__', binBlock(composition))
|
|
385
423
|
.replace('__LINT__', lint)
|
|
424
|
+
.replace('__VITEST__', pinsVitest(composition) ? '~4.0.18' : '^4.0.0')
|
|
386
425
|
.replace('__PRIVATE__', composition.packaging.private ? ' "private": true,\n' : '')
|
|
387
426
|
.replace('__NPM_RUN__', npmRun(composition))
|
|
388
427
|
.replace('__FRAMEWORK_PIN__', frameworkPin(composition))
|
|
@@ -439,6 +478,11 @@ const TSCONFIG_COMPILER_OPTIONS: ReadonlyArray<readonly [string, JsonValue]> = [
|
|
|
439
478
|
['outDir', 'lib'],
|
|
440
479
|
['strict', true],
|
|
441
480
|
['esModuleInterop', true],
|
|
481
|
+
// A server that renders its own user-facing messages loads a catalogue, and
|
|
482
|
+
// a catalogue is JSON. Without this the import does not resolve at all, and
|
|
483
|
+
// without the `include` glob below a composite project rejects it with
|
|
484
|
+
// TS6307 naming neither cause.
|
|
485
|
+
['resolveJsonModule', true],
|
|
442
486
|
['skipLibCheck', true],
|
|
443
487
|
['declaration', true],
|
|
444
488
|
['experimentalDecorators', true],
|
|
@@ -455,9 +499,20 @@ const TSCONFIG_COMPILER_OPTIONS: ReadonlyArray<readonly [string, JsonValue]> = [
|
|
|
455
499
|
*/
|
|
456
500
|
const PACKAGE_LOCAL_OPTIONS: readonly string[] = ['rootDir', 'outDir'];
|
|
457
501
|
|
|
458
|
-
/**
|
|
459
|
-
|
|
460
|
-
|
|
502
|
+
/**
|
|
503
|
+
* The enum-valued options of {@link TSCONFIG_COMPILER_OPTIONS}, whose values
|
|
504
|
+
* `tsc` reads case-insensitively, so `nodenext` and `NodeNext` are one value.
|
|
505
|
+
* An enum-valued option added there belongs here too.
|
|
506
|
+
*/
|
|
507
|
+
const CASE_INSENSITIVE_OPTIONS: readonly string[] = ['target', 'lib', 'module', 'moduleResolution'];
|
|
508
|
+
|
|
509
|
+
/** Compare two option values. Sound here because compiler options are scalars and flat arrays. */
|
|
510
|
+
function optionEquals(key: string, left: JsonValue | undefined, right: JsonValue): boolean {
|
|
511
|
+
const normalise = (value: JsonValue | undefined) => {
|
|
512
|
+
const text = JSON.stringify(value);
|
|
513
|
+
return CASE_INSENSITIVE_OPTIONS.includes(key) ? text?.toLowerCase() : text;
|
|
514
|
+
};
|
|
515
|
+
return normalise(left) === normalise(right);
|
|
461
516
|
}
|
|
462
517
|
|
|
463
518
|
/**
|
|
@@ -476,20 +531,23 @@ function tsconfigJson(composition: InitComposition): string {
|
|
|
476
531
|
? TSCONFIG_COMPILER_OPTIONS
|
|
477
532
|
: [
|
|
478
533
|
...TSCONFIG_COMPILER_OPTIONS.filter(([key]) => PACKAGE_LOCAL_OPTIONS.includes(key)),
|
|
479
|
-
...TSCONFIG_COMPILER_OPTIONS.filter(
|
|
534
|
+
...TSCONFIG_COMPILER_OPTIONS.filter(
|
|
535
|
+
([key, value]) => !PACKAGE_LOCAL_OPTIONS.includes(key) && !optionEquals(key, base[key], value)
|
|
536
|
+
)
|
|
480
537
|
];
|
|
481
538
|
const extendsLine = workspace?.baseTsconfig === undefined ? '' : ` "extends": "${workspace.baseTsconfig}",\n`;
|
|
482
539
|
const body = options.map(([key, value]) => ` "${key}": ${JSON.stringify(value)}`).join(',\n');
|
|
483
|
-
return `{\n${extendsLine} "compilerOptions": {\n${body}\n },\n "include": ["src"]\n}\n`;
|
|
540
|
+
return `{\n${extendsLine} "compilerOptions": {\n${body}\n },\n "include": ["src", "src/**/*.json"]\n}\n`;
|
|
484
541
|
}
|
|
485
542
|
|
|
486
543
|
// `isolatedModules` is what makes this check agree with the transform that
|
|
487
544
|
// actually runs the tests. `npm test` is `typecheck:test && vitest run`, and
|
|
488
|
-
// vitest compiles
|
|
545
|
+
// vitest compiles each file on its own — a transform with no type
|
|
489
546
|
// information, which cannot tell a re-exported TYPE from a re-exported value.
|
|
490
|
-
// Without the flag `tsc` accepts `export { SomeType } from './x'` that
|
|
491
|
-
// then emits as a real import of a symbol that does not exist at
|
|
492
|
-
// the typecheck passes and the suite fails with a confusing
|
|
547
|
+
// Without the flag `tsc` accepts `export { SomeType } from './x'` that the
|
|
548
|
+
// transform then emits as a real import of a symbol that does not exist at
|
|
549
|
+
// runtime, so the typecheck passes and the suite fails with a confusing
|
|
550
|
+
// missing-export.
|
|
493
551
|
const TSCONFIG_TEST = `{
|
|
494
552
|
"extends": "./tsconfig.json",
|
|
495
553
|
"compilerOptions": {
|
|
@@ -498,7 +556,16 @@ const TSCONFIG_TEST = `{
|
|
|
498
556
|
"isolatedModules": true,
|
|
499
557
|
"types": ["node"]
|
|
500
558
|
},
|
|
501
|
-
"include": ["src", "test"]
|
|
559
|
+
"include": ["src", "test", "src/**/*.json"]
|
|
560
|
+
}
|
|
561
|
+
`;
|
|
562
|
+
|
|
563
|
+
// Found by name, unlike `tsconfig.test.json`. Under vite 8, vitest's transform
|
|
564
|
+
// applies a tsconfig only to the files its own `include` lists, so without this
|
|
565
|
+
// a test declaring a decorated class fails to load.
|
|
566
|
+
const TEST_DIR_TSCONFIG = `{
|
|
567
|
+
"extends": "../tsconfig.test.json",
|
|
568
|
+
"include": ["../src", ".", "../src/**/*.json"]
|
|
502
569
|
}
|
|
503
570
|
`;
|
|
504
571
|
|
|
@@ -507,7 +574,7 @@ const VITEST_CONFIG = `import { defineConfig } from 'vitest/config';
|
|
|
507
574
|
// Vitest transforms TypeScript itself and resolves \`.js\` specifiers to their
|
|
508
575
|
// \`.ts\` source, so tests import from \`../src/\` with the same specifiers the
|
|
509
576
|
// compiled output uses. \`include\` is scoped to \`test/\` so \`lib/\` is never
|
|
510
|
-
// scanned; \`npm test\` runs \`typecheck:test\` first, because
|
|
577
|
+
// scanned; \`npm test\` runs \`typecheck:test\` first, because vitest's
|
|
511
578
|
// transform strips types without checking them.
|
|
512
579
|
|
|
513
580
|
export default defineConfig({
|
|
@@ -518,6 +585,11 @@ export default defineConfig({
|
|
|
518
585
|
});
|
|
519
586
|
`;
|
|
520
587
|
|
|
588
|
+
/**
|
|
589
|
+
* The `.gitignore`, written for a workspace member too: its root may not ignore
|
|
590
|
+
* `lib/` (a `yo langium` root ignores `out/`), and a rule the root repeats
|
|
591
|
+
* costs nothing.
|
|
592
|
+
*/
|
|
521
593
|
const GITIGNORE = `node_modules/
|
|
522
594
|
lib/
|
|
523
595
|
*.tsbuildinfo
|
|
@@ -526,27 +598,6 @@ lib/
|
|
|
526
598
|
syntaxes/
|
|
527
599
|
`;
|
|
528
600
|
|
|
529
|
-
/**
|
|
530
|
-
* The workspace-member `.gitignore` — one entry, and the one entry a root
|
|
531
|
-
* cannot be assumed to have.
|
|
532
|
-
*
|
|
533
|
-
* A member inherits the root's rules, and `node_modules/`, `lib/` and
|
|
534
|
-
* `*.tsbuildinfo` are in every monorepo root already, so repeating them here
|
|
535
|
-
* would be three lines to keep in step for no coverage. `syntaxes/` is not like
|
|
536
|
-
* them: it is a Langium artefact, so a root that has never held a Langium
|
|
537
|
-
* package has no rule for it, and the first `langium generate` then offers
|
|
538
|
-
* generated output up for commit with nothing to warn the adopter. Dropping the
|
|
539
|
-
* whole file rather than this subset is the mistake this template exists to
|
|
540
|
-
* undo.
|
|
541
|
-
*/
|
|
542
|
-
const GITIGNORE_WORKSPACE_MEMBER = `# The workspace root already covers \`node_modules/\`, \`lib/\` and \`*.tsbuildinfo\`.
|
|
543
|
-
# It has no reason to know about this one: the TextMate grammar is a Langium
|
|
544
|
-
# artefact, rewritten by every \`langium generate\`, so a root that has never held
|
|
545
|
-
# a Langium package ignores nothing here and the generated file is offered for
|
|
546
|
-
# commit.
|
|
547
|
-
syntaxes/
|
|
548
|
-
`;
|
|
549
|
-
|
|
550
601
|
/** The terminals every grammar needs, shared verbatim by the fragment and the lone-grammar case. */
|
|
551
602
|
const TERMINALS = `hidden terminal WS: /\\s+/;
|
|
552
603
|
terminal ID: /[_a-zA-Z][\\w_]*/;
|
|
@@ -814,9 +865,9 @@ const SERIALIZER = `// Concrete-syntax emitter for __GRAMMAR__ — the parser's
|
|
|
814
865
|
//
|
|
815
866
|
// Without it every structured write fails: the framework's default binding at
|
|
816
867
|
// \`services.serializer.Serializer\` THROWS, because a concrete syntax is
|
|
817
|
-
// language knowledge no framework can derive. \`
|
|
818
|
-
// the data head's \`saveModelDocument\` and a GLSP \`SaveModelAction\`
|
|
819
|
-
// it.
|
|
868
|
+
// language knowledge no framework can derive. A client session's \`update\` /
|
|
869
|
+
// \`save\`, the data head's \`saveModelDocument\` and a GLSP \`SaveModelAction\`
|
|
870
|
+
// all reach it.
|
|
820
871
|
//
|
|
821
872
|
// \`AbstractSerializer\`'s generic property walk lays out FORMAT-structured
|
|
822
873
|
// output — its YAML and JSON subclasses are what it exists for — and cannot
|
|
@@ -1003,7 +1054,7 @@ import { startGlspServer } from '@hydranium/glsp-server/node';
|
|
|
1003
1054
|
// \`TransferElement\` structurally, so naming the AST type here compiles fine and
|
|
1004
1055
|
// silently tells every typed client that a reference is a resolvable object
|
|
1005
1056
|
// rather than a name.
|
|
1006
|
-
${importList(roots, './language-server/generated-
|
|
1057
|
+
${importList(roots, './language-server/generated-hydranium/transfer-model.js', columns, true)}
|
|
1007
1058
|
`
|
|
1008
1059
|
: '';
|
|
1009
1060
|
|
|
@@ -1027,19 +1078,22 @@ const dataServer = startSocketServer({ port: 0, logTag: 'ModelServer', logger: s
|
|
|
1027
1078
|
return { dispose: () => undefined };
|
|
1028
1079
|
});
|
|
1029
1080
|
dataServer.started
|
|
1030
|
-
.then(
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1081
|
+
.then(
|
|
1082
|
+
() => {
|
|
1083
|
+
const { port } = dataServer;
|
|
1084
|
+
if (port === undefined) {
|
|
1085
|
+
// \`started\` resolves only once the address is resolved, so this is
|
|
1086
|
+
// unreachable; a non-null assertion in its place would publish
|
|
1087
|
+
// \`undefined\`, which the host cannot tell from an unreachable port.
|
|
1088
|
+
throw new Error('the data head started without a resolved port');
|
|
1089
|
+
}
|
|
1090
|
+
publishPortOnLspConnection(shared.lsp.Connection, ${upper}_DATA_SERVER_PORT_COMMAND, port);
|
|
1091
|
+
},
|
|
1092
|
+
// The launcher logged its own bind failure; the rejection is control flow.
|
|
1093
|
+
() => undefined
|
|
1094
|
+
)
|
|
1040
1095
|
.catch((error: unknown) => {
|
|
1041
|
-
|
|
1042
|
-
shared.Logger.error(\`[ModelServer] Could not publish the data-server port: \${reason}\`);
|
|
1096
|
+
shared.Logger.error(\`[ModelServer] Could not publish the data-server port: \${describeErrorWithStack(error)}\`);
|
|
1043
1097
|
});
|
|
1044
1098
|
`
|
|
1045
1099
|
: '';
|
|
@@ -1076,9 +1130,9 @@ void glspServer;
|
|
|
1076
1130
|
data
|
|
1077
1131
|
? `
|
|
1078
1132
|
//
|
|
1079
|
-
// NOT the entry
|
|
1080
|
-
//
|
|
1081
|
-
//
|
|
1133
|
+
// NOT the entry the data-server subcommands of \`hydranium-cli\` speak to: stdio
|
|
1134
|
+
// here carries LSP, and the data head is a socket whose port is published over
|
|
1135
|
+
// the LSP connection. Those subcommands spawn \`data-server-main.js\`.`
|
|
1082
1136
|
: ''
|
|
1083
1137
|
}
|
|
1084
1138
|
//
|
|
@@ -1086,11 +1140,17 @@ void glspServer;
|
|
|
1086
1140
|
// than a library entry — import \`./index.js\` instead to compose the language.
|
|
1087
1141
|
|
|
1088
1142
|
${reflectImport}${glspImports}${importList(coreNodeSymbols, '@hydranium/core/node', columns)}
|
|
1089
|
-
${data ? "import { DataServer } from '@hydranium/data-server';\n" : ''}
|
|
1143
|
+
${data ? "import { DataServer } from '@hydranium/data-server';\nimport { describeErrorWithStack } from '@hydranium/protocol';\n" : ''}// The framework's entry point, NOT Langium's \`@hydranium/langium/lsp\` one. It is
|
|
1144
|
+
// signature-compatible and delegates straight through; what it adds first is
|
|
1145
|
+
// \`assertLspHeadComposed\`, which fails the start if the LSP head's SHARED module
|
|
1146
|
+
// was never composed. Reaching for Langium's is the natural mistake and it is
|
|
1147
|
+
// silent: the server boots, links and completes, while echo suppression, the
|
|
1148
|
+
// didChangeContent debounce and the last-client-close rebuild are all inert.
|
|
1149
|
+
import { startLanguageServer, withHydraniumLspFeatures } from '@hydranium/core/lsp';
|
|
1090
1150
|
import { ProposedFeatures, createConnection } from 'vscode-languageserver/node';
|
|
1091
1151
|
${diagramImports}${transferImports}import { create__NAME__Services } from './language-server/__PROJECT_ID__-module.js';
|
|
1092
1152
|
${portCommands === '' ? '' : '\n' + portCommands}
|
|
1093
|
-
const connection = createConnection(ProposedFeatures.all);
|
|
1153
|
+
const connection = createConnection(withHydraniumLspFeatures(ProposedFeatures.all));
|
|
1094
1154
|
const { shared } = create__NAME__Services({ connection, ...NodeFileSystem });
|
|
1095
1155
|
startLanguageServer(shared);
|
|
1096
1156
|
${dataBlock}${glspBlock}`;
|
|
@@ -1103,11 +1163,11 @@ ${dataBlock}${glspBlock}`;
|
|
|
1103
1163
|
* Emitted with the `data` head because it is the ONLY command line that reaches
|
|
1104
1164
|
* that head from outside the editor. `main.ts` gives stdio to LSP and publishes
|
|
1105
1165
|
* the data head's socket port over the LSP connection, so a client that is not
|
|
1106
|
-
* an LSP client cannot discover it — which is every
|
|
1107
|
-
* `
|
|
1108
|
-
*
|
|
1109
|
-
*
|
|
1110
|
-
*
|
|
1166
|
+
* an LSP client cannot discover it — which is every data-server subcommand of
|
|
1167
|
+
* `hydranium-cli`, since each drives the data protocol over a spawned child's
|
|
1168
|
+
* stdin/stdout. Without this file a scaffolded project has no `--server` value,
|
|
1169
|
+
* and the closest guess — the LSP entry — fails as an unrecognised method
|
|
1170
|
+
* rather than as a missing head.
|
|
1111
1171
|
*
|
|
1112
1172
|
* A separate process rather than a flag on `main.ts`: only one protocol can own
|
|
1113
1173
|
* stdio, so the two entries are mutually exclusive by construction.
|
|
@@ -1118,10 +1178,9 @@ function dataServerMainFile(composition: InitComposition): string {
|
|
|
1118
1178
|
const plural = roots.length > 1 ? 's' : '';
|
|
1119
1179
|
|
|
1120
1180
|
const text = `${SHEBANG}// Standalone entry point: a data-server head on **stdio**, with no LSP head in
|
|
1121
|
-
// the process. This is the transport \`hydranium-cli\` speaks —
|
|
1122
|
-
//
|
|
1123
|
-
//
|
|
1124
|
-
// language.
|
|
1181
|
+
// the process. This is the transport \`hydranium-cli\` speaks — its data-server
|
|
1182
|
+
// subcommands spawn a server command and drive JSON-RPC over its stdin/stdout —
|
|
1183
|
+
// so it is what makes those subcommands usable against this language.
|
|
1125
1184
|
//
|
|
1126
1185
|
// Invocation: \`node lib/data-server-main.js [<workspace-path>]\`, or the
|
|
1127
1186
|
// package's \`__PROJECT_ID__-data-server\` bin script. The workspace path defaults to
|
|
@@ -1140,7 +1199,7 @@ function dataServerMainFile(composition: InitComposition): string {
|
|
|
1140
1199
|
${importList(['NodeFileSystem', 'startStdioServer'], '@hydranium/core/node', columns)}
|
|
1141
1200
|
import { DataServer } from '@hydranium/data-server';
|
|
1142
1201
|
// The TRANSFER root${plural}, not the AST one${plural} — same reasoning as \`main.ts\`.
|
|
1143
|
-
${importList(roots, './language-server/generated-
|
|
1202
|
+
${importList(roots, './language-server/generated-hydranium/transfer-model.js', columns, true)}
|
|
1144
1203
|
import { create__NAME__Services } from './language-server/__PROJECT_ID__-module.js';
|
|
1145
1204
|
|
|
1146
1205
|
const { shared } = create__NAME__Services({ ...NodeFileSystem });
|
|
@@ -1259,8 +1318,8 @@ function serializationTest(composition: InitComposition): string {
|
|
|
1259
1318
|
const transferTypes = composition.grammars.map(grammar => `${grammar.entryRule} as Transfer${grammar.entryRule}`);
|
|
1260
1319
|
const text = `// Round-trips each grammar through its serializer: parse the source, serialize
|
|
1261
1320
|
// the model, compare the text. The serializer is the parser's inverse, so this
|
|
1262
|
-
// is the assertion that keeps \`
|
|
1263
|
-
// language server can read back.
|
|
1321
|
+
// is the assertion that keeps a client session's \`update\` / \`save\` writing
|
|
1322
|
+
// files the language server can read back.
|
|
1264
1323
|
//
|
|
1265
1324
|
// The transfer case is the one that catches the mistake worth catching.
|
|
1266
1325
|
// \`ModelService.modelToText\` short-circuits only a RAW STRING, so a typed model
|
|
@@ -1270,7 +1329,7 @@ function serializationTest(composition: InitComposition): string {
|
|
|
1270
1329
|
import { parseHelper } from '@hydranium/core/testing';
|
|
1271
1330
|
import { describe, expect, it } from 'vitest';
|
|
1272
1331
|
${importList(astTypes, '../src/language-server/ast.js', columns, true)}
|
|
1273
|
-
${importList(transferTypes, '../src/language-server/generated-
|
|
1332
|
+
${importList(transferTypes, '../src/language-server/generated-hydranium/transfer-model.js', columns, true)}
|
|
1274
1333
|
import { createServices } from '../src/services.js';
|
|
1275
1334
|
|
|
1276
1335
|
${composition.grammars.map(grammar => render(SERIALIZATION_SUITE, composition, grammar)).join('\n\n')}
|
|
@@ -1439,20 +1498,6 @@ function readme(composition: InitComposition): string {
|
|
|
1439
1498
|
'',
|
|
1440
1499
|
'## Getting started',
|
|
1441
1500
|
'',
|
|
1442
|
-
// Conditional on the pin this scaffold actually carries — see
|
|
1443
|
-
// `UNPUBLISHED_FRAMEWORK_VERSION`.
|
|
1444
|
-
...(composition.frameworkVersion === UNPUBLISHED_FRAMEWORK_VERSION
|
|
1445
|
-
? [
|
|
1446
|
-
'> **Pre-publish note.** `@hydranium/*` is not on npm yet, so the `0.0.0`',
|
|
1447
|
-
'> pins below are placeholders and `npm install` will fail with a 404 until',
|
|
1448
|
-
'> the framework is released. Until then, supply the packages from a local',
|
|
1449
|
-
'> framework checkout with [yalc](https://github.com/wclr/yalc) — a plain',
|
|
1450
|
-
'> `file:` path or `npm link` is not enough, because the framework packages',
|
|
1451
|
-
'> depend on each other by version and npm would try to fetch those from the',
|
|
1452
|
-
'> registry too.',
|
|
1453
|
-
''
|
|
1454
|
-
]
|
|
1455
|
-
: []),
|
|
1456
1501
|
'```bash',
|
|
1457
1502
|
'npm install',
|
|
1458
1503
|
'npm run langium:generate # generate the AST from the grammar',
|
|
@@ -1566,6 +1611,24 @@ function readme(composition: InitComposition): string {
|
|
|
1566
1611
|
'atomic set and depends on a single physical copy, so a floating range can',
|
|
1567
1612
|
'silently resolve a second one.',
|
|
1568
1613
|
'',
|
|
1614
|
+
'`vscode-languageserver` is pinned to `10.0.1`, and',
|
|
1615
|
+
'`vscode-languageserver-protocol` and `vscode-jsonrpc` to the `3.18.1` and',
|
|
1616
|
+
'`9.0.0` it pins, so Langium, the LSP connection and the framework share one',
|
|
1617
|
+
'copy of each. With two, an error a Langium handler returns, such as a request',
|
|
1618
|
+
'for a document the server does not have, reaches the client as a result, and a',
|
|
1619
|
+
'framework error your own LSP handler throws loses its code. The Langium 4.4',
|
|
1620
|
+
'upgrade',
|
|
1621
|
+
'([eclipse-emfcloud/hydranium#142](https://github.com/eclipse-emfcloud/hydranium/issues/142))',
|
|
1622
|
+
'replaces these pins.',
|
|
1623
|
+
'',
|
|
1624
|
+
...(pinsVitest(composition)
|
|
1625
|
+
? [
|
|
1626
|
+
'`vitest` is held below 4.1, whose peer set crashes the npm every Node 22',
|
|
1627
|
+
'release bundles. Raise it once every npm that installs this project is',
|
|
1628
|
+
'11.6 or later.',
|
|
1629
|
+
''
|
|
1630
|
+
]
|
|
1631
|
+
: []),
|
|
1569
1632
|
'## If your repo gates license headers',
|
|
1570
1633
|
'',
|
|
1571
1634
|
'The emitted `.ts` files carry no copyright header — the scaffold cannot know',
|
|
@@ -1585,10 +1648,9 @@ function readme(composition: InitComposition): string {
|
|
|
1585
1648
|
/**
|
|
1586
1649
|
* The GLSP files for ONE grammar's diagram.
|
|
1587
1650
|
*
|
|
1588
|
-
* Per grammar rather than per project because
|
|
1589
|
-
* grammar
|
|
1590
|
-
*
|
|
1591
|
-
* grammar with no diagram can sit on the same server.
|
|
1651
|
+
* Per grammar rather than per project because the state, storage and GModel
|
|
1652
|
+
* factory are typed over that grammar's model, and a grammar with no diagram
|
|
1653
|
+
* can sit on the same server.
|
|
1592
1654
|
*
|
|
1593
1655
|
* The index and the computed-bounds handler are framework classes used
|
|
1594
1656
|
* unmodified; storage and the submission handler are thin typed subclasses,
|
|
@@ -1656,8 +1718,11 @@ export class __GRAMMAR__GlspState extends FullTextHydraniumGlspState<__ENTRY_RUL
|
|
|
1656
1718
|
|
|
1657
1719
|
const storage = `// Source-model storage for the __GRAMMAR__ diagram, inheriting both framework
|
|
1658
1720
|
// defaults: \`loadSourceModel\` (open + settle + \`setSourceRoot\`) and
|
|
1659
|
-
// \`saveSourceModel\` (
|
|
1660
|
-
//
|
|
1721
|
+
// \`saveSourceModel\` (flush the store's text for the primary and every tracked
|
|
1722
|
+
// secondary through the diagram's client session).
|
|
1723
|
+
//
|
|
1724
|
+
// No serializer runs on the save path — a save persists what the diagram's
|
|
1725
|
+
// operations already wrote to the store.
|
|
1661
1726
|
//
|
|
1662
1727
|
// The subclass exists so a bespoke load or save has a stable place to land.
|
|
1663
1728
|
|
|
@@ -1821,27 +1886,28 @@ export class __GRAMMAR__DiagramConfiguration implements DiagramConfiguration {
|
|
|
1821
1886
|
// read-only is a deletion the compiler checks, while read-only → editable is
|
|
1822
1887
|
// authoring against a seam you have not used yet.
|
|
1823
1888
|
//
|
|
1824
|
-
// **It
|
|
1825
|
-
// \`
|
|
1826
|
-
//
|
|
1827
|
-
//
|
|
1828
|
-
//
|
|
1829
|
-
//
|
|
1830
|
-
//
|
|
1831
|
-
// \`HydraniumGlspRecordingCommand\` over \`state.sourceModel\` instead — the same
|
|
1832
|
-
// binding the diagram's own save action needs.
|
|
1889
|
+
// **It edits the operation's working copy.** A handler runs inside an operation,
|
|
1890
|
+
// where \`sourceRoot\` is a copy of the built root: the recording command's
|
|
1891
|
+
// runnable appends to the copy, and the operation writes the copy's text through
|
|
1892
|
+
// the grammar's \`Serializer\` once, gated on the version the copy was made at,
|
|
1893
|
+
// with undo and redo applying that one change. A command that writes the
|
|
1894
|
+
// document itself instead reads no text off the copy, which has no \`$document\`,
|
|
1895
|
+
// and the operation's own write then overwrites it.
|
|
1833
1896
|
//
|
|
1834
1897
|
// **The drop location is discarded.** \`needsClientLayout\` is \`true\` and the
|
|
1835
1898
|
// starter grammar persists no bounds, so there is nowhere to put a coordinate: the
|
|
1836
1899
|
// node is appended at the end of the document and the client places it.
|
|
1837
1900
|
//
|
|
1838
1901
|
// Like \`types.ts\`, \`gmodel-factory.ts\` and \`diagram-configuration.ts\`, this file
|
|
1839
|
-
// knows the starter grammar's
|
|
1840
|
-
//
|
|
1902
|
+
// knows the starter grammar's shape — the \`nodes\` list and the \`__NODE_RULE__\`
|
|
1903
|
+
// it holds. Replacing the grammar means replacing these four together.
|
|
1841
1904
|
|
|
1842
1905
|
import { type Command, type CreateNodeOperation, JsonCreateNodeOperationHandler, type MaybePromise } from '@eclipse-glsp/server';
|
|
1906
|
+
import { appendChild } from '@hydranium/core';
|
|
1907
|
+
import { HydraniumGlspRecordingCommand } from '@hydranium/glsp-server';
|
|
1843
1908
|
import { findNextUnique } from '@hydranium/protocol';
|
|
1844
1909
|
import { injectable } from 'inversify';
|
|
1910
|
+
import type { __NODE_RULE__ } from '../../language-server/ast.js';
|
|
1845
1911
|
import { ${upper}_NODE_TYPE } from './types.js';
|
|
1846
1912
|
import type { __GRAMMAR__GlspState } from './state.js';
|
|
1847
1913
|
|
|
@@ -1860,46 +1926,20 @@ export class __GRAMMAR__CreateNodeOperationHandler extends JsonCreateNodeOperati
|
|
|
1860
1926
|
if (!this.elementTypeIds.includes(operation.elementTypeId)) {
|
|
1861
1927
|
return undefined;
|
|
1862
1928
|
}
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
before,
|
|
1867
|
-
findNextUnique(
|
|
1929
|
+
return new HydraniumGlspRecordingCommand(this.modelState, this.label, () => {
|
|
1930
|
+
const root = this.modelState.sourceRoot;
|
|
1931
|
+
const name = findNextUnique(
|
|
1868
1932
|
NODE_NAME_STEM,
|
|
1869
|
-
|
|
1870
|
-
)
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
// has exactly one field, so there is nothing to merge a concurrent edit into.
|
|
1874
|
-
return {
|
|
1875
|
-
execute: () => state.updateSourceModel({ text: after }),
|
|
1876
|
-
undo: () => state.updateSourceModel({ text: before }),
|
|
1877
|
-
redo: () => state.updateSourceModel({ text: after })
|
|
1878
|
-
};
|
|
1879
|
-
}
|
|
1880
|
-
|
|
1881
|
-
/** The text the captured source root was parsed from — the baseline an edit appends to. */
|
|
1882
|
-
protected documentText(): string {
|
|
1883
|
-
return this.modelState.sourceRoot.$document?.textDocument.getText() ?? '';
|
|
1884
|
-
}
|
|
1885
|
-
|
|
1886
|
-
/** \`text\` with one more node declaration, under exactly one trailing newline. */
|
|
1887
|
-
protected withNode(text: string, name: string): string {
|
|
1888
|
-
const body = text.trimEnd();
|
|
1889
|
-
const declaration = \`node \${name}\`;
|
|
1890
|
-
return body.length === 0 ? \`\${declaration}\\n\` : \`\${body}\\n\${declaration}\\n\`;
|
|
1933
|
+
root.nodes.map(node => node.name)
|
|
1934
|
+
);
|
|
1935
|
+
appendChild(root, 'nodes', root.nodes, { $type: '__NODE_RULE__', name } as __NODE_RULE__);
|
|
1936
|
+
});
|
|
1891
1937
|
}
|
|
1892
1938
|
}
|
|
1893
1939
|
`;
|
|
1894
1940
|
|
|
1895
1941
|
const module = `// GLSP diagram module for __GRAMMAR__ — the DI wiring of one diagram type.
|
|
1896
1942
|
//
|
|
1897
|
-
// \`declareLanguage\` is the multi-grammar seam: it binds the grammar this diagram
|
|
1898
|
-
// edits on the SESSION container, so \`modelState.diagramLanguage\` and the
|
|
1899
|
-
// per-language lookups resolve to __LANGUAGE_ID__. That is what stops one
|
|
1900
|
-
// diagram type from fighting over a process-wide binding when a project holds
|
|
1901
|
-
// several grammars — a grammar with no diagram is registered on the same server.
|
|
1902
|
-
//
|
|
1903
1943
|
// \`configureActionHandlers\` REBINDS rather than adds: GLSP's own
|
|
1904
1944
|
// \`DiagramModule\` already registers \`ComputedBoundsActionHandler\`, and two
|
|
1905
1945
|
// handlers for one action would both run.
|
|
@@ -1926,13 +1966,11 @@ ${importList(
|
|
|
1926
1966
|
true
|
|
1927
1967
|
)}
|
|
1928
1968
|
import { ComputedBoundsActionHandler } from '@eclipse-glsp/server';
|
|
1929
|
-
import type { LanguageMetaData } from '@hydranium/langium';
|
|
1930
1969
|
${importList(
|
|
1931
1970
|
['HydraniumGlspComputedBoundsActionHandler', 'AbstractHydraniumGlspDiagramModule', 'HydraniumGlspIndex'],
|
|
1932
1971
|
'@hydranium/glsp-server',
|
|
1933
1972
|
columns
|
|
1934
1973
|
)}
|
|
1935
|
-
import { __GRAMMAR__LanguageMetaData } from '../../language-server/generated/module.js';
|
|
1936
1974
|
import { __GRAMMAR__CreateNodeOperationHandler } from './create-node-operation-handler.js';
|
|
1937
1975
|
import { __GRAMMAR__DiagramConfiguration } from './diagram-configuration.js';
|
|
1938
1976
|
import { __GRAMMAR__GModelFactory } from './gmodel-factory.js';
|
|
@@ -1944,10 +1982,6 @@ import { ${upper}_DIAGRAM_TYPE } from './types.js';
|
|
|
1944
1982
|
export class __GRAMMAR__DiagramModule extends AbstractHydraniumGlspDiagramModule {
|
|
1945
1983
|
readonly diagramType = ${upper}_DIAGRAM_TYPE;
|
|
1946
1984
|
|
|
1947
|
-
protected override declareLanguage(): LanguageMetaData {
|
|
1948
|
-
return __GRAMMAR__LanguageMetaData;
|
|
1949
|
-
}
|
|
1950
|
-
|
|
1951
1985
|
protected override bindModelState(): BindingTarget<ModelState> {
|
|
1952
1986
|
return { service: __GRAMMAR__GlspState };
|
|
1953
1987
|
}
|
|
@@ -1983,6 +2017,76 @@ export class __GRAMMAR__DiagramModule extends AbstractHydraniumGlspDiagramModule
|
|
|
1983
2017
|
binding.add(__GRAMMAR__CreateNodeOperationHandler);
|
|
1984
2018
|
}
|
|
1985
2019
|
}
|
|
2020
|
+
`;
|
|
2021
|
+
|
|
2022
|
+
const diagramTest = `// The starter create-node handler, run the way a client runs it: an in-process
|
|
2023
|
+
// GLSP server opens a document, a create-node operation adds a node, and an undo
|
|
2024
|
+
// and a redo take it out and put it back. Every step must keep the nodes the
|
|
2025
|
+
// document already had, and its comments: the diagram writes the document from
|
|
2026
|
+
// its AST, which carries none.
|
|
2027
|
+
|
|
2028
|
+
import 'reflect-metadata';
|
|
2029
|
+
import { type Action, CreateNodeOperation, RedoAction, ServerModule, SetDirtyStateAction, UndoAction } from '@eclipse-glsp/server';
|
|
2030
|
+
import { initializeWorkspaceProgrammatically } from '@hydranium/core';
|
|
2031
|
+
import { makeScratchWorkspace, type ScratchWorkspace } from '@hydranium/core/testing/node';
|
|
2032
|
+
import { HydraniumGlspAppModule } from '@hydranium/glsp-server';
|
|
2033
|
+
import { type GlspHarness, makeGlspHarness } from '@hydranium/glsp-server/testing';
|
|
2034
|
+
import { waitFor } from '@hydranium/protocol/testing';
|
|
2035
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
2036
|
+
import { __GRAMMAR__DiagramModule } from '../src/glsp/__GRAMMAR_ID__/diagram-module.js';
|
|
2037
|
+
import type { __GRAMMAR__GlspState } from '../src/glsp/__GRAMMAR_ID__/state.js';
|
|
2038
|
+
import { ${upper}_DIAGRAM_TYPE, ${upper}_NODE_TYPE } from '../src/glsp/__GRAMMAR_ID__/types.js';
|
|
2039
|
+
import { createServices } from '../src/services.js';
|
|
2040
|
+
|
|
2041
|
+
let workspace: ScratchWorkspace | undefined;
|
|
2042
|
+
let diagram: GlspHarness<__GRAMMAR__GlspState> | undefined;
|
|
2043
|
+
|
|
2044
|
+
afterEach(() => {
|
|
2045
|
+
diagram?.dispose();
|
|
2046
|
+
workspace?.dispose();
|
|
2047
|
+
diagram = undefined;
|
|
2048
|
+
workspace = undefined;
|
|
2049
|
+
});
|
|
2050
|
+
|
|
2051
|
+
/** Send \`action\` and wait for the dirty state the server answers it with, which it sends once the edit is written. */
|
|
2052
|
+
async function send(harness: GlspHarness<__GRAMMAR__GlspState>, action: Action, reason: string): Promise<void> {
|
|
2053
|
+
const before = harness.actions.length;
|
|
2054
|
+
harness.dispatch(action);
|
|
2055
|
+
await waitFor(() => harness.actions.slice(before).some(sent => SetDirtyStateAction.is(sent) && sent.reason === reason), {
|
|
2056
|
+
message: \`no '\${reason}' dirty state\`
|
|
2057
|
+
});
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
describe('__GRAMMAR__ diagram', () => {
|
|
2061
|
+
it('creates a node after the existing ones, keeping their comments, and undoes and redoes it', async () => {
|
|
2062
|
+
workspace = makeScratchWorkspace({ prefix: '__GRAMMAR_ID__-diagram-' });
|
|
2063
|
+
const original = '// keep this comment\\nnode first -> second\\n// about second\\nnode second';
|
|
2064
|
+
const file = workspace.write('model.__EXTENSION__', original);
|
|
2065
|
+
const { shared } = createServices();
|
|
2066
|
+
await initializeWorkspaceProgrammatically(shared, workspace.root);
|
|
2067
|
+
diagram = makeGlspHarness<__GRAMMAR__GlspState>({
|
|
2068
|
+
serverModule: new ServerModule().configureDiagramModule(new __GRAMMAR__DiagramModule()),
|
|
2069
|
+
diagramType: ${upper}_DIAGRAM_TYPE,
|
|
2070
|
+
appModules: [new HydraniumGlspAppModule({ shared })]
|
|
2071
|
+
});
|
|
2072
|
+
await diagram.start();
|
|
2073
|
+
await diagram.openDocument(file);
|
|
2074
|
+
const uri = workspace.uri('model.__EXTENSION__');
|
|
2075
|
+
const text = (): string | undefined => shared.workspace.TextDocuments.get(uri)?.getText();
|
|
2076
|
+
|
|
2077
|
+
await send(diagram, CreateNodeOperation.create(${upper}_NODE_TYPE), 'operation');
|
|
2078
|
+
const created = text();
|
|
2079
|
+
await send(diagram, UndoAction.create(), 'undo');
|
|
2080
|
+
const undone = text();
|
|
2081
|
+
await send(diagram, RedoAction.create(), 'redo');
|
|
2082
|
+
|
|
2083
|
+
expect({ created, undone, redone: text() }).toEqual({
|
|
2084
|
+
created: \`\${original}\\nnode Node\`,
|
|
2085
|
+
undone: original,
|
|
2086
|
+
redone: \`\${original}\\nnode Node\`
|
|
2087
|
+
});
|
|
2088
|
+
});
|
|
2089
|
+
});
|
|
1986
2090
|
`;
|
|
1987
2091
|
|
|
1988
2092
|
return [
|
|
@@ -1993,7 +2097,8 @@ export class __GRAMMAR__DiagramModule extends AbstractHydraniumGlspDiagramModule
|
|
|
1993
2097
|
{ path: `${dir}/gmodel-factory.ts`, content: render(factory) },
|
|
1994
2098
|
{ path: `${dir}/diagram-configuration.ts`, content: render(configuration) },
|
|
1995
2099
|
{ path: `${dir}/create-node-operation-handler.ts`, content: render(createHandler) },
|
|
1996
|
-
{ path: `${dir}/diagram-module.ts`, content: render(module) }
|
|
2100
|
+
{ path: `${dir}/diagram-module.ts`, content: render(module) },
|
|
2101
|
+
{ path: `test/${grammar.grammarId}-diagram.test.ts`, content: render(diagramTest) }
|
|
1997
2102
|
];
|
|
1998
2103
|
}
|
|
1999
2104
|
|
|
@@ -2021,11 +2126,9 @@ export function buildInitTemplates(composition: InitComposition): InitFile[] {
|
|
|
2021
2126
|
{ path: 'langium-config.json', content: langiumConfig(composition) },
|
|
2022
2127
|
{ path: 'tsconfig.json', content: tsconfigJson(composition) },
|
|
2023
2128
|
{ path: 'tsconfig.test.json', content: TSCONFIG_TEST },
|
|
2129
|
+
{ path: 'test/tsconfig.json', content: TEST_DIR_TSCONFIG },
|
|
2024
2130
|
{ path: 'vitest.config.ts', content: VITEST_CONFIG },
|
|
2025
|
-
{
|
|
2026
|
-
path: '.gitignore',
|
|
2027
|
-
content: composition.packaging.workspace === undefined ? GITIGNORE : GITIGNORE_WORKSPACE_MEMBER
|
|
2028
|
-
},
|
|
2131
|
+
{ path: '.gitignore', content: GITIGNORE },
|
|
2029
2132
|
{ path: 'README.md', content: readme(composition) },
|
|
2030
2133
|
...grammarFiles,
|
|
2031
2134
|
{ path: `src/language-server/${composition.projectId}-module.ts`, content: moduleFile(composition) },
|