@hydranium/cli 1.0.0-next.30 → 1.0.0-next.300
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
|
@@ -71,15 +71,6 @@
|
|
|
71
71
|
* over; the adopter's own formatter settles it.
|
|
72
72
|
*/
|
|
73
73
|
import { readCliVersion } from '../cli-version.js';
|
|
74
|
-
/**
|
|
75
|
-
* The version every framework package carries before the first release.
|
|
76
|
-
*
|
|
77
|
-
* A scaffold made by a CLI still at this version pins a range the registry
|
|
78
|
-
* cannot serve, so the emitted README keeps its yalc note and `init` keeps its
|
|
79
|
-
* install warning while — and only while — it holds: from a published CLI the
|
|
80
|
-
* derived pins resolve and either note would be false as printed.
|
|
81
|
-
*/
|
|
82
|
-
export const UNPUBLISHED_FRAMEWORK_VERSION = '0.0.0';
|
|
83
74
|
/**
|
|
84
75
|
* The version `init` pins every `@hydranium/*` dependency at: the scaffolding
|
|
85
76
|
* CLI's own.
|
|
@@ -156,7 +147,7 @@ function columnsFor(composition) {
|
|
|
156
147
|
}
|
|
157
148
|
/**
|
|
158
149
|
* A named import on one line while it fits the column budget, else one symbol
|
|
159
|
-
* per line. Emitted files are supposed to pass a
|
|
150
|
+
* per line. Emitted files are supposed to pass a format check as-is, and the
|
|
160
151
|
* generated-module import grows by one symbol per grammar.
|
|
161
152
|
*/
|
|
162
153
|
function importList(symbols, from, columns, typeOnly = false) {
|
|
@@ -204,7 +195,12 @@ const HEAD_FRAMEWORK_DEPENDENCIES = {
|
|
|
204
195
|
const HEAD_THIRD_PARTY_DEPENDENCIES = {
|
|
205
196
|
lsp: {
|
|
206
197
|
langium: '4.3.1',
|
|
207
|
-
|
|
198
|
+
// Exact, as are the protocol and transport it pins, so Langium and the
|
|
199
|
+
// framework share their copies; across copies, an error a handler throws
|
|
200
|
+
// or returns loses its code, and a later patch can pin another protocol.
|
|
201
|
+
'vscode-languageserver': '10.0.1',
|
|
202
|
+
'vscode-jsonrpc': '9.0.0',
|
|
203
|
+
'vscode-languageserver-protocol': '3.18.1'
|
|
208
204
|
},
|
|
209
205
|
data: {},
|
|
210
206
|
glsp: {
|
|
@@ -216,6 +212,34 @@ const HEAD_THIRD_PARTY_DEPENDENCIES = {
|
|
|
216
212
|
'reflect-metadata': '~0.2.2'
|
|
217
213
|
}
|
|
218
214
|
};
|
|
215
|
+
/**
|
|
216
|
+
* The exact pins of `heads`, for a workspace root to declare too. npm installs a
|
|
217
|
+
* root's own dependencies at the top of the tree, so another member's version of
|
|
218
|
+
* one nests beneath that member instead of taking the place of the copy this
|
|
219
|
+
* package shares with the framework.
|
|
220
|
+
*/
|
|
221
|
+
export function workspaceRootPins(heads) {
|
|
222
|
+
return heads
|
|
223
|
+
.flatMap(head => Object.entries(HEAD_THIRD_PARTY_DEPENDENCIES[head]))
|
|
224
|
+
.filter(([, version]) => /^\d+\.\d+\.\d+$/.test(version))
|
|
225
|
+
.sort(([left], [right]) => left.localeCompare(right));
|
|
226
|
+
}
|
|
227
|
+
/** 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. */
|
|
228
|
+
function installsVitest41(npmVersion) {
|
|
229
|
+
const [release, prerelease] = (npmVersion ?? '0.0.0').split('-', 2);
|
|
230
|
+
const [major, minor, patch] = release.split('.').map(Number);
|
|
231
|
+
return major > 11 || (major === 11 && (minor > 6 || (minor === 6 && (patch > 0 || prerelease === undefined))));
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Whether the scaffold holds `vitest` below 4.1, whose peer set crashes npm
|
|
235
|
+
* before 11.6 (every npm a Node 22 release bundles) on the first install. Only a
|
|
236
|
+
* workspace root declaring a newer npm lifts it, and only when `init` runs under
|
|
237
|
+
* an npm known to be newer: npm does not enforce `packageManager`.
|
|
238
|
+
*/
|
|
239
|
+
function pinsVitest(composition) {
|
|
240
|
+
const workspace = composition.packaging.workspace;
|
|
241
|
+
return !(installsVitest41(workspace?.packageManagerNpm) && installsVitest41(workspace?.npmVersion));
|
|
242
|
+
}
|
|
219
243
|
/** The dependency block for a head set, merged and sorted as npm writes it. */
|
|
220
244
|
function dependencyBlock(composition, indent) {
|
|
221
245
|
const merged = {};
|
|
@@ -234,7 +258,7 @@ function dependencyBlock(composition, indent) {
|
|
|
234
258
|
// npm convention, and `npm install` rewrites `package.json` at two spaces
|
|
235
259
|
// regardless — so three would not survive the adopter's first install.
|
|
236
260
|
//
|
|
237
|
-
// The KEY ORDER is
|
|
261
|
+
// The KEY ORDER is the one Oxfmt's `sortPackageJson` canonicalises to, `private`
|
|
238
262
|
// before `license` included. Any other order is rewritten by the scaffolded
|
|
239
263
|
// project's own first `format` run — a diff on a file the adopter never touched
|
|
240
264
|
// — and it makes a byte-compare against a formatted copy of this emission
|
|
@@ -265,16 +289,9 @@ const PACKAGE_JSON = `{
|
|
|
265
289
|
"version": "0.0.0",
|
|
266
290
|
__PRIVATE__ "description": "__NAME__ language server, built with the Hydranium framework.",
|
|
267
291
|
"keywords": [
|
|
268
|
-
|
|
269
|
-
"langium",
|
|
270
|
-
"language-server",
|
|
271
|
-
"__PROJECT_ID__"
|
|
272
|
-
],
|
|
292
|
+
__KEYWORDS__ ],
|
|
273
293
|
"license": "UNLICENSED",
|
|
274
294
|
"author": "",
|
|
275
|
-
"type": "module",
|
|
276
|
-
"main": "lib/index.js",
|
|
277
|
-
"types": "lib/index.d.ts",
|
|
278
295
|
"bin": {
|
|
279
296
|
__BIN__ },
|
|
280
297
|
"files": [
|
|
@@ -282,11 +299,14 @@ __BIN__ },
|
|
|
282
299
|
"src",
|
|
283
300
|
"syntaxes"
|
|
284
301
|
],
|
|
302
|
+
"type": "module",
|
|
303
|
+
"main": "lib/index.js",
|
|
304
|
+
"types": "lib/index.d.ts",
|
|
285
305
|
"scripts": {
|
|
286
306
|
"build": "npm run generate && tsc",
|
|
287
|
-
"clean": "rimraf lib syntaxes src/language-server/generated src/language-server/generated-
|
|
307
|
+
"clean": "rimraf lib syntaxes src/language-server/generated src/language-server/generated-hydranium tsconfig.tsbuildinfo",
|
|
288
308
|
"generate": "npm run langium:generate && npm run generate:transfer-model",
|
|
289
|
-
"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-
|
|
309
|
+
"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\\"",
|
|
290
310
|
"langium:generate": "langium generate",
|
|
291
311
|
"langium:watch": "langium generate --watch",
|
|
292
312
|
__LINT__ "start": "node lib/main.js --stdio",
|
|
@@ -302,8 +322,8 @@ __DEPENDENCIES__
|
|
|
302
322
|
"@types/node": "^22.0.0",
|
|
303
323
|
"langium-cli": "4.3.0",
|
|
304
324
|
"rimraf": "^5.0.0",
|
|
305
|
-
"typescript": "^
|
|
306
|
-
"vitest": "
|
|
325
|
+
"typescript": "^7.0.2",
|
|
326
|
+
"vitest": "__VITEST__"
|
|
307
327
|
},
|
|
308
328
|
"engines": {
|
|
309
329
|
"node": ">=22.13"
|
|
@@ -331,7 +351,7 @@ function npmRun(composition) {
|
|
|
331
351
|
* name. Without the second key a scaffolded project has no command line that
|
|
332
352
|
* reaches its own data head at all.
|
|
333
353
|
*
|
|
334
|
-
* Alphabetical, because `
|
|
354
|
+
* Alphabetical, because Oxfmt's `sortPackageJson` sorts `bin` and any other
|
|
335
355
|
* order is rewritten by the scaffolded project's first `format` run.
|
|
336
356
|
*/
|
|
337
357
|
function binBlock(composition) {
|
|
@@ -345,15 +365,29 @@ function binBlock(composition) {
|
|
|
345
365
|
function packageJson(composition) {
|
|
346
366
|
const { scope } = composition.packaging;
|
|
347
367
|
const packageName = scope === undefined ? composition.projectId : `${scope}/${composition.projectId}`;
|
|
348
|
-
|
|
368
|
+
const keywords = ['hydranium', 'langium', 'language-server', composition.projectId]
|
|
369
|
+
.sort()
|
|
370
|
+
.map(keyword => ` ${JSON.stringify(keyword)}`)
|
|
371
|
+
.join(',\n') + '\n';
|
|
372
|
+
// Emitted only where a root lint config says the repo lints. The absence is
|
|
349
373
|
// the dangerous direction: a task runner runs a script only where one is
|
|
350
374
|
// declared, so a package with no `lint` is SKIPPED rather than reported, and
|
|
351
375
|
// that reads as a clean lint. A wrong invocation is one visible line to edit.
|
|
352
|
-
const
|
|
376
|
+
const workspace = composition.packaging.workspace;
|
|
377
|
+
const oxlintConfig = workspace?.oxlintConfig === undefined
|
|
378
|
+
? undefined
|
|
379
|
+
: `${'../'.repeat(workspace.targetPath.split('/').length)}${workspace.oxlintConfig}`;
|
|
380
|
+
const lint = workspace?.oxlintConfig !== undefined
|
|
381
|
+
? ` "lint": "oxlint --report-unused-disable-directives --format stylish --config ${oxlintConfig} src test --max-warnings 0",\n`
|
|
382
|
+
: workspace?.eslintConfig !== undefined
|
|
383
|
+
? ' "lint": "eslint src test --max-warnings 0",\n'
|
|
384
|
+
: '';
|
|
353
385
|
return project(PACKAGE_JSON, composition)
|
|
354
386
|
.replace('__PACKAGE_NAME__', packageName)
|
|
387
|
+
.replace('__KEYWORDS__', keywords)
|
|
355
388
|
.replace('__BIN__', binBlock(composition))
|
|
356
389
|
.replace('__LINT__', lint)
|
|
390
|
+
.replace('__VITEST__', pinsVitest(composition) ? '~4.0.18' : '^4.0.0')
|
|
357
391
|
.replace('__PRIVATE__', composition.packaging.private ? ' "private": true,\n' : '')
|
|
358
392
|
.replace('__NPM_RUN__', npmRun(composition))
|
|
359
393
|
.replace('__FRAMEWORK_PIN__', frameworkPin(composition))
|
|
@@ -406,6 +440,11 @@ const TSCONFIG_COMPILER_OPTIONS = [
|
|
|
406
440
|
['outDir', 'lib'],
|
|
407
441
|
['strict', true],
|
|
408
442
|
['esModuleInterop', true],
|
|
443
|
+
// A server that renders its own user-facing messages loads a catalogue, and
|
|
444
|
+
// a catalogue is JSON. Without this the import does not resolve at all, and
|
|
445
|
+
// without the `include` glob below a composite project rejects it with
|
|
446
|
+
// TS6307 naming neither cause.
|
|
447
|
+
['resolveJsonModule', true],
|
|
409
448
|
['skipLibCheck', true],
|
|
410
449
|
['declaration', true],
|
|
411
450
|
['experimentalDecorators', true],
|
|
@@ -420,9 +459,19 @@ const TSCONFIG_COMPILER_OPTIONS = [
|
|
|
420
459
|
* base is wrong rather than that the member is redundant.
|
|
421
460
|
*/
|
|
422
461
|
const PACKAGE_LOCAL_OPTIONS = ['rootDir', 'outDir'];
|
|
423
|
-
/**
|
|
424
|
-
|
|
425
|
-
|
|
462
|
+
/**
|
|
463
|
+
* The enum-valued options of {@link TSCONFIG_COMPILER_OPTIONS}, whose values
|
|
464
|
+
* `tsc` reads case-insensitively, so `nodenext` and `NodeNext` are one value.
|
|
465
|
+
* An enum-valued option added there belongs here too.
|
|
466
|
+
*/
|
|
467
|
+
const CASE_INSENSITIVE_OPTIONS = ['target', 'lib', 'module', 'moduleResolution'];
|
|
468
|
+
/** Compare two option values. Sound here because compiler options are scalars and flat arrays. */
|
|
469
|
+
function optionEquals(key, left, right) {
|
|
470
|
+
const normalise = (value) => {
|
|
471
|
+
const text = JSON.stringify(value);
|
|
472
|
+
return CASE_INSENSITIVE_OPTIONS.includes(key) ? text?.toLowerCase() : text;
|
|
473
|
+
};
|
|
474
|
+
return normalise(left) === normalise(right);
|
|
426
475
|
}
|
|
427
476
|
/**
|
|
428
477
|
* `tsconfig.json`, extending the workspace's base config when there is one.
|
|
@@ -439,19 +488,20 @@ function tsconfigJson(composition) {
|
|
|
439
488
|
? TSCONFIG_COMPILER_OPTIONS
|
|
440
489
|
: [
|
|
441
490
|
...TSCONFIG_COMPILER_OPTIONS.filter(([key]) => PACKAGE_LOCAL_OPTIONS.includes(key)),
|
|
442
|
-
...TSCONFIG_COMPILER_OPTIONS.filter(([key, value]) => !PACKAGE_LOCAL_OPTIONS.includes(key) && !
|
|
491
|
+
...TSCONFIG_COMPILER_OPTIONS.filter(([key, value]) => !PACKAGE_LOCAL_OPTIONS.includes(key) && !optionEquals(key, base[key], value))
|
|
443
492
|
];
|
|
444
493
|
const extendsLine = workspace?.baseTsconfig === undefined ? '' : ` "extends": "${workspace.baseTsconfig}",\n`;
|
|
445
494
|
const body = options.map(([key, value]) => ` "${key}": ${JSON.stringify(value)}`).join(',\n');
|
|
446
|
-
return `{\n${extendsLine} "compilerOptions": {\n${body}\n },\n "include": ["src"]\n}\n`;
|
|
495
|
+
return `{\n${extendsLine} "compilerOptions": {\n${body}\n },\n "include": ["src", "src/**/*.json"]\n}\n`;
|
|
447
496
|
}
|
|
448
497
|
// `isolatedModules` is what makes this check agree with the transform that
|
|
449
498
|
// actually runs the tests. `npm test` is `typecheck:test && vitest run`, and
|
|
450
|
-
// vitest compiles
|
|
499
|
+
// vitest compiles each file on its own — a transform with no type
|
|
451
500
|
// information, which cannot tell a re-exported TYPE from a re-exported value.
|
|
452
|
-
// Without the flag `tsc` accepts `export { SomeType } from './x'` that
|
|
453
|
-
// then emits as a real import of a symbol that does not exist at
|
|
454
|
-
// the typecheck passes and the suite fails with a confusing
|
|
501
|
+
// Without the flag `tsc` accepts `export { SomeType } from './x'` that the
|
|
502
|
+
// transform then emits as a real import of a symbol that does not exist at
|
|
503
|
+
// runtime, so the typecheck passes and the suite fails with a confusing
|
|
504
|
+
// missing-export.
|
|
455
505
|
const TSCONFIG_TEST = `{
|
|
456
506
|
"extends": "./tsconfig.json",
|
|
457
507
|
"compilerOptions": {
|
|
@@ -460,7 +510,15 @@ const TSCONFIG_TEST = `{
|
|
|
460
510
|
"isolatedModules": true,
|
|
461
511
|
"types": ["node"]
|
|
462
512
|
},
|
|
463
|
-
"include": ["src", "test"]
|
|
513
|
+
"include": ["src", "test", "src/**/*.json"]
|
|
514
|
+
}
|
|
515
|
+
`;
|
|
516
|
+
// Found by name, unlike `tsconfig.test.json`. Under vite 8, vitest's transform
|
|
517
|
+
// applies a tsconfig only to the files its own `include` lists, so without this
|
|
518
|
+
// a test declaring a decorated class fails to load.
|
|
519
|
+
const TEST_DIR_TSCONFIG = `{
|
|
520
|
+
"extends": "../tsconfig.test.json",
|
|
521
|
+
"include": ["../src", ".", "../src/**/*.json"]
|
|
464
522
|
}
|
|
465
523
|
`;
|
|
466
524
|
const VITEST_CONFIG = `import { defineConfig } from 'vitest/config';
|
|
@@ -468,7 +526,7 @@ const VITEST_CONFIG = `import { defineConfig } from 'vitest/config';
|
|
|
468
526
|
// Vitest transforms TypeScript itself and resolves \`.js\` specifiers to their
|
|
469
527
|
// \`.ts\` source, so tests import from \`../src/\` with the same specifiers the
|
|
470
528
|
// compiled output uses. \`include\` is scoped to \`test/\` so \`lib/\` is never
|
|
471
|
-
// scanned; \`npm test\` runs \`typecheck:test\` first, because
|
|
529
|
+
// scanned; \`npm test\` runs \`typecheck:test\` first, because vitest's
|
|
472
530
|
// transform strips types without checking them.
|
|
473
531
|
|
|
474
532
|
export default defineConfig({
|
|
@@ -478,6 +536,11 @@ export default defineConfig({
|
|
|
478
536
|
}
|
|
479
537
|
});
|
|
480
538
|
`;
|
|
539
|
+
/**
|
|
540
|
+
* The `.gitignore`, written for a workspace member too: its root may not ignore
|
|
541
|
+
* `lib/` (a `yo langium` root ignores `out/`), and a rule the root repeats
|
|
542
|
+
* costs nothing.
|
|
543
|
+
*/
|
|
481
544
|
const GITIGNORE = `node_modules/
|
|
482
545
|
lib/
|
|
483
546
|
*.tsbuildinfo
|
|
@@ -485,26 +548,6 @@ lib/
|
|
|
485
548
|
# Langium-generated TextMate grammar (rewritten by every \`langium generate\`).
|
|
486
549
|
syntaxes/
|
|
487
550
|
`;
|
|
488
|
-
/**
|
|
489
|
-
* The workspace-member `.gitignore` — one entry, and the one entry a root
|
|
490
|
-
* cannot be assumed to have.
|
|
491
|
-
*
|
|
492
|
-
* A member inherits the root's rules, and `node_modules/`, `lib/` and
|
|
493
|
-
* `*.tsbuildinfo` are in every monorepo root already, so repeating them here
|
|
494
|
-
* would be three lines to keep in step for no coverage. `syntaxes/` is not like
|
|
495
|
-
* them: it is a Langium artefact, so a root that has never held a Langium
|
|
496
|
-
* package has no rule for it, and the first `langium generate` then offers
|
|
497
|
-
* generated output up for commit with nothing to warn the adopter. Dropping the
|
|
498
|
-
* whole file rather than this subset is the mistake this template exists to
|
|
499
|
-
* undo.
|
|
500
|
-
*/
|
|
501
|
-
const GITIGNORE_WORKSPACE_MEMBER = `# The workspace root already covers \`node_modules/\`, \`lib/\` and \`*.tsbuildinfo\`.
|
|
502
|
-
# It has no reason to know about this one: the TextMate grammar is a Langium
|
|
503
|
-
# artefact, rewritten by every \`langium generate\`, so a root that has never held
|
|
504
|
-
# a Langium package ignores nothing here and the generated file is offered for
|
|
505
|
-
# commit.
|
|
506
|
-
syntaxes/
|
|
507
|
-
`;
|
|
508
551
|
/** The terminals every grammar needs, shared verbatim by the fragment and the lone-grammar case. */
|
|
509
552
|
const TERMINALS = `hidden terminal WS: /\\s+/;
|
|
510
553
|
terminal ID: /[_a-zA-Z][\\w_]*/;
|
|
@@ -747,9 +790,9 @@ const SERIALIZER = `// Concrete-syntax emitter for __GRAMMAR__ — the parser's
|
|
|
747
790
|
//
|
|
748
791
|
// Without it every structured write fails: the framework's default binding at
|
|
749
792
|
// \`services.serializer.Serializer\` THROWS, because a concrete syntax is
|
|
750
|
-
// language knowledge no framework can derive. \`
|
|
751
|
-
// the data head's \`saveModelDocument\` and a GLSP \`SaveModelAction\`
|
|
752
|
-
// it.
|
|
793
|
+
// language knowledge no framework can derive. A client session's \`update\` /
|
|
794
|
+
// \`save\`, the data head's \`saveModelDocument\` and a GLSP \`SaveModelAction\`
|
|
795
|
+
// all reach it.
|
|
753
796
|
//
|
|
754
797
|
// \`AbstractSerializer\`'s generic property walk lays out FORMAT-structured
|
|
755
798
|
// output — its YAML and JSON subclasses are what it exists for — and cannot
|
|
@@ -925,7 +968,7 @@ import { startGlspServer } from '@hydranium/glsp-server/node';
|
|
|
925
968
|
// \`TransferElement\` structurally, so naming the AST type here compiles fine and
|
|
926
969
|
// silently tells every typed client that a reference is a resolvable object
|
|
927
970
|
// rather than a name.
|
|
928
|
-
${importList(roots, './language-server/generated-
|
|
971
|
+
${importList(roots, './language-server/generated-hydranium/transfer-model.js', columns, true)}
|
|
929
972
|
`
|
|
930
973
|
: '';
|
|
931
974
|
// The commands live in `head-ports.ts`, not here: a host shell has to name
|
|
@@ -947,19 +990,22 @@ const dataServer = startSocketServer({ port: 0, logTag: 'ModelServer', logger: s
|
|
|
947
990
|
return { dispose: () => undefined };
|
|
948
991
|
});
|
|
949
992
|
dataServer.started
|
|
950
|
-
.then(
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
993
|
+
.then(
|
|
994
|
+
() => {
|
|
995
|
+
const { port } = dataServer;
|
|
996
|
+
if (port === undefined) {
|
|
997
|
+
// \`started\` resolves only once the address is resolved, so this is
|
|
998
|
+
// unreachable; a non-null assertion in its place would publish
|
|
999
|
+
// \`undefined\`, which the host cannot tell from an unreachable port.
|
|
1000
|
+
throw new Error('the data head started without a resolved port');
|
|
1001
|
+
}
|
|
1002
|
+
publishPortOnLspConnection(shared.lsp.Connection, ${upper}_DATA_SERVER_PORT_COMMAND, port);
|
|
1003
|
+
},
|
|
1004
|
+
// The launcher logged its own bind failure; the rejection is control flow.
|
|
1005
|
+
() => undefined
|
|
1006
|
+
)
|
|
960
1007
|
.catch((error: unknown) => {
|
|
961
|
-
|
|
962
|
-
shared.Logger.error(\`[ModelServer] Could not publish the data-server port: \${reason}\`);
|
|
1008
|
+
shared.Logger.error(\`[ModelServer] Could not publish the data-server port: \${describeErrorWithStack(error)}\`);
|
|
963
1009
|
});
|
|
964
1010
|
`
|
|
965
1011
|
: '';
|
|
@@ -993,20 +1039,26 @@ void glspServer;
|
|
|
993
1039
|
// script.${data
|
|
994
1040
|
? `
|
|
995
1041
|
//
|
|
996
|
-
// NOT the entry
|
|
997
|
-
//
|
|
998
|
-
//
|
|
1042
|
+
// NOT the entry the data-server subcommands of \`hydranium-cli\` speak to: stdio
|
|
1043
|
+
// here carries LSP, and the data head is a socket whose port is published over
|
|
1044
|
+
// the LSP connection. Those subcommands spawn \`data-server-main.js\`.`
|
|
999
1045
|
: ''}
|
|
1000
1046
|
//
|
|
1001
1047
|
// Everything here runs at module scope, so this file is an executable rather
|
|
1002
1048
|
// than a library entry — import \`./index.js\` instead to compose the language.
|
|
1003
1049
|
|
|
1004
1050
|
${reflectImport}${glspImports}${importList(coreNodeSymbols, '@hydranium/core/node', columns)}
|
|
1005
|
-
${data ? "import { DataServer } from '@hydranium/data-server';\n" : ''}
|
|
1051
|
+
${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
|
|
1052
|
+
// signature-compatible and delegates straight through; what it adds first is
|
|
1053
|
+
// \`assertLspHeadComposed\`, which fails the start if the LSP head's SHARED module
|
|
1054
|
+
// was never composed. Reaching for Langium's is the natural mistake and it is
|
|
1055
|
+
// silent: the server boots, links and completes, while echo suppression, the
|
|
1056
|
+
// didChangeContent debounce and the last-client-close rebuild are all inert.
|
|
1057
|
+
import { startLanguageServer, withHydraniumLspFeatures } from '@hydranium/core/lsp';
|
|
1006
1058
|
import { ProposedFeatures, createConnection } from 'vscode-languageserver/node';
|
|
1007
1059
|
${diagramImports}${transferImports}import { create__NAME__Services } from './language-server/__PROJECT_ID__-module.js';
|
|
1008
1060
|
${portCommands === '' ? '' : '\n' + portCommands}
|
|
1009
|
-
const connection = createConnection(ProposedFeatures.all);
|
|
1061
|
+
const connection = createConnection(withHydraniumLspFeatures(ProposedFeatures.all));
|
|
1010
1062
|
const { shared } = create__NAME__Services({ connection, ...NodeFileSystem });
|
|
1011
1063
|
startLanguageServer(shared);
|
|
1012
1064
|
${dataBlock}${glspBlock}`;
|
|
@@ -1018,11 +1070,11 @@ ${dataBlock}${glspBlock}`;
|
|
|
1018
1070
|
* Emitted with the `data` head because it is the ONLY command line that reaches
|
|
1019
1071
|
* that head from outside the editor. `main.ts` gives stdio to LSP and publishes
|
|
1020
1072
|
* the data head's socket port over the LSP connection, so a client that is not
|
|
1021
|
-
* an LSP client cannot discover it — which is every
|
|
1022
|
-
* `
|
|
1023
|
-
*
|
|
1024
|
-
*
|
|
1025
|
-
*
|
|
1073
|
+
* an LSP client cannot discover it — which is every data-server subcommand of
|
|
1074
|
+
* `hydranium-cli`, since each drives the data protocol over a spawned child's
|
|
1075
|
+
* stdin/stdout. Without this file a scaffolded project has no `--server` value,
|
|
1076
|
+
* and the closest guess — the LSP entry — fails as an unrecognised method
|
|
1077
|
+
* rather than as a missing head.
|
|
1026
1078
|
*
|
|
1027
1079
|
* A separate process rather than a flag on `main.ts`: only one protocol can own
|
|
1028
1080
|
* stdio, so the two entries are mutually exclusive by construction.
|
|
@@ -1032,10 +1084,9 @@ function dataServerMainFile(composition) {
|
|
|
1032
1084
|
const roots = [...composition.grammars.map(grammar => grammar.entryRule)].sort();
|
|
1033
1085
|
const plural = roots.length > 1 ? 's' : '';
|
|
1034
1086
|
const text = `${SHEBANG}// Standalone entry point: a data-server head on **stdio**, with no LSP head in
|
|
1035
|
-
// the process. This is the transport \`hydranium-cli\` speaks —
|
|
1036
|
-
//
|
|
1037
|
-
//
|
|
1038
|
-
// language.
|
|
1087
|
+
// the process. This is the transport \`hydranium-cli\` speaks — its data-server
|
|
1088
|
+
// subcommands spawn a server command and drive JSON-RPC over its stdin/stdout —
|
|
1089
|
+
// so it is what makes those subcommands usable against this language.
|
|
1039
1090
|
//
|
|
1040
1091
|
// Invocation: \`node lib/data-server-main.js [<workspace-path>]\`, or the
|
|
1041
1092
|
// package's \`__PROJECT_ID__-data-server\` bin script. The workspace path defaults to
|
|
@@ -1054,7 +1105,7 @@ function dataServerMainFile(composition) {
|
|
|
1054
1105
|
${importList(['NodeFileSystem', 'startStdioServer'], '@hydranium/core/node', columns)}
|
|
1055
1106
|
import { DataServer } from '@hydranium/data-server';
|
|
1056
1107
|
// The TRANSFER root${plural}, not the AST one${plural} — same reasoning as \`main.ts\`.
|
|
1057
|
-
${importList(roots, './language-server/generated-
|
|
1108
|
+
${importList(roots, './language-server/generated-hydranium/transfer-model.js', columns, true)}
|
|
1058
1109
|
import { create__NAME__Services } from './language-server/__PROJECT_ID__-module.js';
|
|
1059
1110
|
|
|
1060
1111
|
const { shared } = create__NAME__Services({ ...NodeFileSystem });
|
|
@@ -1170,8 +1221,8 @@ function serializationTest(composition) {
|
|
|
1170
1221
|
const transferTypes = composition.grammars.map(grammar => `${grammar.entryRule} as Transfer${grammar.entryRule}`);
|
|
1171
1222
|
const text = `// Round-trips each grammar through its serializer: parse the source, serialize
|
|
1172
1223
|
// the model, compare the text. The serializer is the parser's inverse, so this
|
|
1173
|
-
// is the assertion that keeps \`
|
|
1174
|
-
// language server can read back.
|
|
1224
|
+
// is the assertion that keeps a client session's \`update\` / \`save\` writing
|
|
1225
|
+
// files the language server can read back.
|
|
1175
1226
|
//
|
|
1176
1227
|
// The transfer case is the one that catches the mistake worth catching.
|
|
1177
1228
|
// \`ModelService.modelToText\` short-circuits only a RAW STRING, so a typed model
|
|
@@ -1181,7 +1232,7 @@ function serializationTest(composition) {
|
|
|
1181
1232
|
import { parseHelper } from '@hydranium/core/testing';
|
|
1182
1233
|
import { describe, expect, it } from 'vitest';
|
|
1183
1234
|
${importList(astTypes, '../src/language-server/ast.js', columns, true)}
|
|
1184
|
-
${importList(transferTypes, '../src/language-server/generated-
|
|
1235
|
+
${importList(transferTypes, '../src/language-server/generated-hydranium/transfer-model.js', columns, true)}
|
|
1185
1236
|
import { createServices } from '../src/services.js';
|
|
1186
1237
|
|
|
1187
1238
|
${composition.grammars.map(grammar => render(SERIALIZATION_SUITE, composition, grammar)).join('\n\n')}
|
|
@@ -1340,20 +1391,6 @@ function readme(composition) {
|
|
|
1340
1391
|
'',
|
|
1341
1392
|
'## Getting started',
|
|
1342
1393
|
'',
|
|
1343
|
-
// Conditional on the pin this scaffold actually carries — see
|
|
1344
|
-
// `UNPUBLISHED_FRAMEWORK_VERSION`.
|
|
1345
|
-
...(composition.frameworkVersion === UNPUBLISHED_FRAMEWORK_VERSION
|
|
1346
|
-
? [
|
|
1347
|
-
'> **Pre-publish note.** `@hydranium/*` is not on npm yet, so the `0.0.0`',
|
|
1348
|
-
'> pins below are placeholders and `npm install` will fail with a 404 until',
|
|
1349
|
-
'> the framework is released. Until then, supply the packages from a local',
|
|
1350
|
-
'> framework checkout with [yalc](https://github.com/wclr/yalc) — a plain',
|
|
1351
|
-
'> `file:` path or `npm link` is not enough, because the framework packages',
|
|
1352
|
-
'> depend on each other by version and npm would try to fetch those from the',
|
|
1353
|
-
'> registry too.',
|
|
1354
|
-
''
|
|
1355
|
-
]
|
|
1356
|
-
: []),
|
|
1357
1394
|
'```bash',
|
|
1358
1395
|
'npm install',
|
|
1359
1396
|
'npm run langium:generate # generate the AST from the grammar',
|
|
@@ -1461,6 +1498,24 @@ function readme(composition) {
|
|
|
1461
1498
|
'atomic set and depends on a single physical copy, so a floating range can',
|
|
1462
1499
|
'silently resolve a second one.',
|
|
1463
1500
|
'',
|
|
1501
|
+
'`vscode-languageserver` is pinned to `10.0.1`, and',
|
|
1502
|
+
'`vscode-languageserver-protocol` and `vscode-jsonrpc` to the `3.18.1` and',
|
|
1503
|
+
'`9.0.0` it pins, so Langium, the LSP connection and the framework share one',
|
|
1504
|
+
'copy of each. With two, an error a Langium handler returns, such as a request',
|
|
1505
|
+
'for a document the server does not have, reaches the client as a result, and a',
|
|
1506
|
+
'framework error your own LSP handler throws loses its code. The Langium 4.4',
|
|
1507
|
+
'upgrade',
|
|
1508
|
+
'([eclipse-emfcloud/hydranium#142](https://github.com/eclipse-emfcloud/hydranium/issues/142))',
|
|
1509
|
+
'replaces these pins.',
|
|
1510
|
+
'',
|
|
1511
|
+
...(pinsVitest(composition)
|
|
1512
|
+
? [
|
|
1513
|
+
'`vitest` is held below 4.1, whose peer set crashes the npm every Node 22',
|
|
1514
|
+
'release bundles. Raise it once every npm that installs this project is',
|
|
1515
|
+
'11.6 or later.',
|
|
1516
|
+
''
|
|
1517
|
+
]
|
|
1518
|
+
: []),
|
|
1464
1519
|
'## If your repo gates license headers',
|
|
1465
1520
|
'',
|
|
1466
1521
|
'The emitted `.ts` files carry no copyright header — the scaffold cannot know',
|
|
@@ -1479,10 +1534,9 @@ function readme(composition) {
|
|
|
1479
1534
|
/**
|
|
1480
1535
|
* The GLSP files for ONE grammar's diagram.
|
|
1481
1536
|
*
|
|
1482
|
-
* Per grammar rather than per project because
|
|
1483
|
-
* grammar
|
|
1484
|
-
*
|
|
1485
|
-
* grammar with no diagram can sit on the same server.
|
|
1537
|
+
* Per grammar rather than per project because the state, storage and GModel
|
|
1538
|
+
* factory are typed over that grammar's model, and a grammar with no diagram
|
|
1539
|
+
* can sit on the same server.
|
|
1486
1540
|
*
|
|
1487
1541
|
* The index and the computed-bounds handler are framework classes used
|
|
1488
1542
|
* unmodified; storage and the submission handler are thin typed subclasses,
|
|
@@ -1547,8 +1601,11 @@ export class __GRAMMAR__GlspState extends FullTextHydraniumGlspState<__ENTRY_RUL
|
|
|
1547
1601
|
`;
|
|
1548
1602
|
const storage = `// Source-model storage for the __GRAMMAR__ diagram, inheriting both framework
|
|
1549
1603
|
// defaults: \`loadSourceModel\` (open + settle + \`setSourceRoot\`) and
|
|
1550
|
-
// \`saveSourceModel\` (
|
|
1551
|
-
//
|
|
1604
|
+
// \`saveSourceModel\` (flush the store's text for the primary and every tracked
|
|
1605
|
+
// secondary through the diagram's client session).
|
|
1606
|
+
//
|
|
1607
|
+
// No serializer runs on the save path — a save persists what the diagram's
|
|
1608
|
+
// operations already wrote to the store.
|
|
1552
1609
|
//
|
|
1553
1610
|
// The subclass exists so a bespoke load or save has a stable place to land.
|
|
1554
1611
|
|
|
@@ -1704,27 +1761,28 @@ export class __GRAMMAR__DiagramConfiguration implements DiagramConfiguration {
|
|
|
1704
1761
|
// read-only is a deletion the compiler checks, while read-only → editable is
|
|
1705
1762
|
// authoring against a seam you have not used yet.
|
|
1706
1763
|
//
|
|
1707
|
-
// **It
|
|
1708
|
-
// \`
|
|
1709
|
-
//
|
|
1710
|
-
//
|
|
1711
|
-
//
|
|
1712
|
-
//
|
|
1713
|
-
//
|
|
1714
|
-
// \`HydraniumGlspRecordingCommand\` over \`state.sourceModel\` instead — the same
|
|
1715
|
-
// binding the diagram's own save action needs.
|
|
1764
|
+
// **It edits the operation's working copy.** A handler runs inside an operation,
|
|
1765
|
+
// where \`sourceRoot\` is a copy of the built root: the recording command's
|
|
1766
|
+
// runnable appends to the copy, and the operation writes the copy's text through
|
|
1767
|
+
// the grammar's \`Serializer\` once, gated on the version the copy was made at,
|
|
1768
|
+
// with undo and redo applying that one change. A command that writes the
|
|
1769
|
+
// document itself instead reads no text off the copy, which has no \`$document\`,
|
|
1770
|
+
// and the operation's own write then overwrites it.
|
|
1716
1771
|
//
|
|
1717
1772
|
// **The drop location is discarded.** \`needsClientLayout\` is \`true\` and the
|
|
1718
1773
|
// starter grammar persists no bounds, so there is nowhere to put a coordinate: the
|
|
1719
1774
|
// node is appended at the end of the document and the client places it.
|
|
1720
1775
|
//
|
|
1721
1776
|
// Like \`types.ts\`, \`gmodel-factory.ts\` and \`diagram-configuration.ts\`, this file
|
|
1722
|
-
// knows the starter grammar's
|
|
1723
|
-
//
|
|
1777
|
+
// knows the starter grammar's shape — the \`nodes\` list and the \`__NODE_RULE__\`
|
|
1778
|
+
// it holds. Replacing the grammar means replacing these four together.
|
|
1724
1779
|
|
|
1725
1780
|
import { type Command, type CreateNodeOperation, JsonCreateNodeOperationHandler, type MaybePromise } from '@eclipse-glsp/server';
|
|
1781
|
+
import { appendChild } from '@hydranium/core';
|
|
1782
|
+
import { HydraniumGlspRecordingCommand } from '@hydranium/glsp-server';
|
|
1726
1783
|
import { findNextUnique } from '@hydranium/protocol';
|
|
1727
1784
|
import { injectable } from 'inversify';
|
|
1785
|
+
import type { __NODE_RULE__ } from '../../language-server/ast.js';
|
|
1728
1786
|
import { ${upper}_NODE_TYPE } from './types.js';
|
|
1729
1787
|
import type { __GRAMMAR__GlspState } from './state.js';
|
|
1730
1788
|
|
|
@@ -1743,45 +1801,19 @@ export class __GRAMMAR__CreateNodeOperationHandler extends JsonCreateNodeOperati
|
|
|
1743
1801
|
if (!this.elementTypeIds.includes(operation.elementTypeId)) {
|
|
1744
1802
|
return undefined;
|
|
1745
1803
|
}
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
before,
|
|
1750
|
-
findNextUnique(
|
|
1804
|
+
return new HydraniumGlspRecordingCommand(this.modelState, this.label, () => {
|
|
1805
|
+
const root = this.modelState.sourceRoot;
|
|
1806
|
+
const name = findNextUnique(
|
|
1751
1807
|
NODE_NAME_STEM,
|
|
1752
|
-
|
|
1753
|
-
)
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
// has exactly one field, so there is nothing to merge a concurrent edit into.
|
|
1757
|
-
return {
|
|
1758
|
-
execute: () => state.updateSourceModel({ text: after }),
|
|
1759
|
-
undo: () => state.updateSourceModel({ text: before }),
|
|
1760
|
-
redo: () => state.updateSourceModel({ text: after })
|
|
1761
|
-
};
|
|
1762
|
-
}
|
|
1763
|
-
|
|
1764
|
-
/** The text the captured source root was parsed from — the baseline an edit appends to. */
|
|
1765
|
-
protected documentText(): string {
|
|
1766
|
-
return this.modelState.sourceRoot.$document?.textDocument.getText() ?? '';
|
|
1767
|
-
}
|
|
1768
|
-
|
|
1769
|
-
/** \`text\` with one more node declaration, under exactly one trailing newline. */
|
|
1770
|
-
protected withNode(text: string, name: string): string {
|
|
1771
|
-
const body = text.trimEnd();
|
|
1772
|
-
const declaration = \`node \${name}\`;
|
|
1773
|
-
return body.length === 0 ? \`\${declaration}\\n\` : \`\${body}\\n\${declaration}\\n\`;
|
|
1808
|
+
root.nodes.map(node => node.name)
|
|
1809
|
+
);
|
|
1810
|
+
appendChild(root, 'nodes', root.nodes, { $type: '__NODE_RULE__', name } as __NODE_RULE__);
|
|
1811
|
+
});
|
|
1774
1812
|
}
|
|
1775
1813
|
}
|
|
1776
1814
|
`;
|
|
1777
1815
|
const module = `// GLSP diagram module for __GRAMMAR__ — the DI wiring of one diagram type.
|
|
1778
1816
|
//
|
|
1779
|
-
// \`declareLanguage\` is the multi-grammar seam: it binds the grammar this diagram
|
|
1780
|
-
// edits on the SESSION container, so \`modelState.diagramLanguage\` and the
|
|
1781
|
-
// per-language lookups resolve to __LANGUAGE_ID__. That is what stops one
|
|
1782
|
-
// diagram type from fighting over a process-wide binding when a project holds
|
|
1783
|
-
// several grammars — a grammar with no diagram is registered on the same server.
|
|
1784
|
-
//
|
|
1785
1817
|
// \`configureActionHandlers\` REBINDS rather than adds: GLSP's own
|
|
1786
1818
|
// \`DiagramModule\` already registers \`ComputedBoundsActionHandler\`, and two
|
|
1787
1819
|
// handlers for one action would both run.
|
|
@@ -1803,9 +1835,7 @@ ${importList([
|
|
|
1803
1835
|
'SourceModelStorage'
|
|
1804
1836
|
], '@eclipse-glsp/server', columns, true)}
|
|
1805
1837
|
import { ComputedBoundsActionHandler } from '@eclipse-glsp/server';
|
|
1806
|
-
import type { LanguageMetaData } from '@hydranium/langium';
|
|
1807
1838
|
${importList(['HydraniumGlspComputedBoundsActionHandler', 'AbstractHydraniumGlspDiagramModule', 'HydraniumGlspIndex'], '@hydranium/glsp-server', columns)}
|
|
1808
|
-
import { __GRAMMAR__LanguageMetaData } from '../../language-server/generated/module.js';
|
|
1809
1839
|
import { __GRAMMAR__CreateNodeOperationHandler } from './create-node-operation-handler.js';
|
|
1810
1840
|
import { __GRAMMAR__DiagramConfiguration } from './diagram-configuration.js';
|
|
1811
1841
|
import { __GRAMMAR__GModelFactory } from './gmodel-factory.js';
|
|
@@ -1817,10 +1847,6 @@ import { ${upper}_DIAGRAM_TYPE } from './types.js';
|
|
|
1817
1847
|
export class __GRAMMAR__DiagramModule extends AbstractHydraniumGlspDiagramModule {
|
|
1818
1848
|
readonly diagramType = ${upper}_DIAGRAM_TYPE;
|
|
1819
1849
|
|
|
1820
|
-
protected override declareLanguage(): LanguageMetaData {
|
|
1821
|
-
return __GRAMMAR__LanguageMetaData;
|
|
1822
|
-
}
|
|
1823
|
-
|
|
1824
1850
|
protected override bindModelState(): BindingTarget<ModelState> {
|
|
1825
1851
|
return { service: __GRAMMAR__GlspState };
|
|
1826
1852
|
}
|
|
@@ -1856,6 +1882,75 @@ export class __GRAMMAR__DiagramModule extends AbstractHydraniumGlspDiagramModule
|
|
|
1856
1882
|
binding.add(__GRAMMAR__CreateNodeOperationHandler);
|
|
1857
1883
|
}
|
|
1858
1884
|
}
|
|
1885
|
+
`;
|
|
1886
|
+
const diagramTest = `// The starter create-node handler, run the way a client runs it: an in-process
|
|
1887
|
+
// GLSP server opens a document, a create-node operation adds a node, and an undo
|
|
1888
|
+
// and a redo take it out and put it back. Every step must keep the nodes the
|
|
1889
|
+
// document already had, and its comments: the diagram writes the document from
|
|
1890
|
+
// its AST, which carries none.
|
|
1891
|
+
|
|
1892
|
+
import 'reflect-metadata';
|
|
1893
|
+
import { type Action, CreateNodeOperation, RedoAction, ServerModule, SetDirtyStateAction, UndoAction } from '@eclipse-glsp/server';
|
|
1894
|
+
import { initializeWorkspaceProgrammatically } from '@hydranium/core';
|
|
1895
|
+
import { makeScratchWorkspace, type ScratchWorkspace } from '@hydranium/core/testing/node';
|
|
1896
|
+
import { HydraniumGlspAppModule } from '@hydranium/glsp-server';
|
|
1897
|
+
import { type GlspHarness, makeGlspHarness } from '@hydranium/glsp-server/testing';
|
|
1898
|
+
import { waitFor } from '@hydranium/protocol/testing';
|
|
1899
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
1900
|
+
import { __GRAMMAR__DiagramModule } from '../src/glsp/__GRAMMAR_ID__/diagram-module.js';
|
|
1901
|
+
import type { __GRAMMAR__GlspState } from '../src/glsp/__GRAMMAR_ID__/state.js';
|
|
1902
|
+
import { ${upper}_DIAGRAM_TYPE, ${upper}_NODE_TYPE } from '../src/glsp/__GRAMMAR_ID__/types.js';
|
|
1903
|
+
import { createServices } from '../src/services.js';
|
|
1904
|
+
|
|
1905
|
+
let workspace: ScratchWorkspace | undefined;
|
|
1906
|
+
let diagram: GlspHarness<__GRAMMAR__GlspState> | undefined;
|
|
1907
|
+
|
|
1908
|
+
afterEach(() => {
|
|
1909
|
+
diagram?.dispose();
|
|
1910
|
+
workspace?.dispose();
|
|
1911
|
+
diagram = undefined;
|
|
1912
|
+
workspace = undefined;
|
|
1913
|
+
});
|
|
1914
|
+
|
|
1915
|
+
/** Send \`action\` and wait for the dirty state the server answers it with, which it sends once the edit is written. */
|
|
1916
|
+
async function send(harness: GlspHarness<__GRAMMAR__GlspState>, action: Action, reason: string): Promise<void> {
|
|
1917
|
+
const before = harness.actions.length;
|
|
1918
|
+
harness.dispatch(action);
|
|
1919
|
+
await waitFor(() => harness.actions.slice(before).some(sent => SetDirtyStateAction.is(sent) && sent.reason === reason), {
|
|
1920
|
+
message: \`no '\${reason}' dirty state\`
|
|
1921
|
+
});
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
describe('__GRAMMAR__ diagram', () => {
|
|
1925
|
+
it('creates a node after the existing ones, keeping their comments, and undoes and redoes it', async () => {
|
|
1926
|
+
workspace = makeScratchWorkspace({ prefix: '__GRAMMAR_ID__-diagram-' });
|
|
1927
|
+
const original = '// keep this comment\\nnode first -> second\\n// about second\\nnode second';
|
|
1928
|
+
const file = workspace.write('model.__EXTENSION__', original);
|
|
1929
|
+
const { shared } = createServices();
|
|
1930
|
+
await initializeWorkspaceProgrammatically(shared, workspace.root);
|
|
1931
|
+
diagram = makeGlspHarness<__GRAMMAR__GlspState>({
|
|
1932
|
+
serverModule: new ServerModule().configureDiagramModule(new __GRAMMAR__DiagramModule()),
|
|
1933
|
+
diagramType: ${upper}_DIAGRAM_TYPE,
|
|
1934
|
+
appModules: [new HydraniumGlspAppModule({ shared })]
|
|
1935
|
+
});
|
|
1936
|
+
await diagram.start();
|
|
1937
|
+
await diagram.openDocument(file);
|
|
1938
|
+
const uri = workspace.uri('model.__EXTENSION__');
|
|
1939
|
+
const text = (): string | undefined => shared.workspace.TextDocuments.get(uri)?.getText();
|
|
1940
|
+
|
|
1941
|
+
await send(diagram, CreateNodeOperation.create(${upper}_NODE_TYPE), 'operation');
|
|
1942
|
+
const created = text();
|
|
1943
|
+
await send(diagram, UndoAction.create(), 'undo');
|
|
1944
|
+
const undone = text();
|
|
1945
|
+
await send(diagram, RedoAction.create(), 'redo');
|
|
1946
|
+
|
|
1947
|
+
expect({ created, undone, redone: text() }).toEqual({
|
|
1948
|
+
created: \`\${original}\\nnode Node\`,
|
|
1949
|
+
undone: original,
|
|
1950
|
+
redone: \`\${original}\\nnode Node\`
|
|
1951
|
+
});
|
|
1952
|
+
});
|
|
1953
|
+
});
|
|
1859
1954
|
`;
|
|
1860
1955
|
return [
|
|
1861
1956
|
{ path: `${dir}/types.ts`, content: render(types) },
|
|
@@ -1865,7 +1960,8 @@ export class __GRAMMAR__DiagramModule extends AbstractHydraniumGlspDiagramModule
|
|
|
1865
1960
|
{ path: `${dir}/gmodel-factory.ts`, content: render(factory) },
|
|
1866
1961
|
{ path: `${dir}/diagram-configuration.ts`, content: render(configuration) },
|
|
1867
1962
|
{ path: `${dir}/create-node-operation-handler.ts`, content: render(createHandler) },
|
|
1868
|
-
{ path: `${dir}/diagram-module.ts`, content: render(module) }
|
|
1963
|
+
{ path: `${dir}/diagram-module.ts`, content: render(module) },
|
|
1964
|
+
{ path: `test/${grammar.grammarId}-diagram.test.ts`, content: render(diagramTest) }
|
|
1869
1965
|
];
|
|
1870
1966
|
}
|
|
1871
1967
|
/**
|
|
@@ -1890,11 +1986,9 @@ export function buildInitTemplates(composition) {
|
|
|
1890
1986
|
{ path: 'langium-config.json', content: langiumConfig(composition) },
|
|
1891
1987
|
{ path: 'tsconfig.json', content: tsconfigJson(composition) },
|
|
1892
1988
|
{ path: 'tsconfig.test.json', content: TSCONFIG_TEST },
|
|
1989
|
+
{ path: 'test/tsconfig.json', content: TEST_DIR_TSCONFIG },
|
|
1893
1990
|
{ path: 'vitest.config.ts', content: VITEST_CONFIG },
|
|
1894
|
-
{
|
|
1895
|
-
path: '.gitignore',
|
|
1896
|
-
content: composition.packaging.workspace === undefined ? GITIGNORE : GITIGNORE_WORKSPACE_MEMBER
|
|
1897
|
-
},
|
|
1991
|
+
{ path: '.gitignore', content: GITIGNORE },
|
|
1898
1992
|
{ path: 'README.md', content: readme(composition) },
|
|
1899
1993
|
...grammarFiles,
|
|
1900
1994
|
{ path: `src/language-server/${composition.projectId}-module.ts`, content: moduleFile(composition) },
|