@hydranium/cli 1.0.0-next.27 → 1.0.0-next.270

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.
Files changed (196) hide show
  1. package/README.md +73 -204
  2. package/bin/hydranium-cli.js +13 -0
  3. package/lib/cli-version.js.map +1 -1
  4. package/lib/cli.js +2 -1
  5. package/lib/cli.js.map +1 -1
  6. package/lib/commands/analyze-heap-args.js.map +1 -1
  7. package/lib/commands/analyze-heap.d.ts +3 -0
  8. package/lib/commands/analyze-heap.d.ts.map +1 -1
  9. package/lib/commands/analyze-heap.js +2 -1
  10. package/lib/commands/analyze-heap.js.map +1 -1
  11. package/lib/commands/ast-ground-truth-args.d.ts +3 -2
  12. package/lib/commands/ast-ground-truth-args.d.ts.map +1 -1
  13. package/lib/commands/ast-ground-truth-args.js +11 -9
  14. package/lib/commands/ast-ground-truth-args.js.map +1 -1
  15. package/lib/commands/ast-ground-truth-driver.js +2 -1
  16. package/lib/commands/ast-ground-truth-driver.js.map +1 -1
  17. package/lib/commands/ast-ground-truth.js +1 -1
  18. package/lib/commands/ast-ground-truth.js.map +1 -1
  19. package/lib/commands/generate-ast-builder.d.ts +49 -0
  20. package/lib/commands/generate-ast-builder.d.ts.map +1 -0
  21. package/lib/commands/generate-ast-builder.js +200 -0
  22. package/lib/commands/generate-ast-builder.js.map +1 -0
  23. package/lib/commands/generate-transfer-model-args.d.ts.map +1 -1
  24. package/lib/commands/generate-transfer-model-args.js +11 -1
  25. package/lib/commands/generate-transfer-model-args.js.map +1 -1
  26. package/lib/commands/generate-transfer-model-config.d.ts.map +1 -1
  27. package/lib/commands/generate-transfer-model-config.js +6 -3
  28. package/lib/commands/generate-transfer-model-config.js.map +1 -1
  29. package/lib/commands/generate-transfer-model.d.ts +9 -0
  30. package/lib/commands/generate-transfer-model.d.ts.map +1 -1
  31. package/lib/commands/generate-transfer-model.js +18 -4
  32. package/lib/commands/generate-transfer-model.js.map +1 -1
  33. package/lib/commands/harness-args.d.ts +37 -42
  34. package/lib/commands/harness-args.d.ts.map +1 -1
  35. package/lib/commands/harness-args.js +91 -27
  36. package/lib/commands/harness-args.js.map +1 -1
  37. package/lib/commands/headless-harness.d.ts +37 -20
  38. package/lib/commands/headless-harness.d.ts.map +1 -1
  39. package/lib/commands/headless-harness.js +68 -17
  40. package/lib/commands/headless-harness.js.map +1 -1
  41. package/lib/commands/init-args.js +6 -6
  42. package/lib/commands/init-args.js.map +1 -1
  43. package/lib/commands/init-prompt.d.ts.map +1 -1
  44. package/lib/commands/init-templates.d.ts +8 -10
  45. package/lib/commands/init-templates.d.ts.map +1 -1
  46. package/lib/commands/init-templates.js +260 -152
  47. package/lib/commands/init-templates.js.map +1 -1
  48. package/lib/commands/init-wizard.js +4 -4
  49. package/lib/commands/init-wizard.js.map +1 -1
  50. package/lib/commands/init-workspace.d.ts +13 -3
  51. package/lib/commands/init-workspace.d.ts.map +1 -1
  52. package/lib/commands/init-workspace.js +21 -6
  53. package/lib/commands/init-workspace.js.map +1 -1
  54. package/lib/commands/init.d.ts +16 -10
  55. package/lib/commands/init.d.ts.map +1 -1
  56. package/lib/commands/init.js +17 -15
  57. package/lib/commands/init.js.map +1 -1
  58. package/lib/commands/lint-grammar-args.d.ts +3 -2
  59. package/lib/commands/lint-grammar-args.d.ts.map +1 -1
  60. package/lib/commands/lint-grammar-args.js +13 -10
  61. package/lib/commands/lint-grammar-args.js.map +1 -1
  62. package/lib/commands/lint-grammar-driver.js +2 -1
  63. package/lib/commands/lint-grammar-driver.js.map +1 -1
  64. package/lib/commands/lint-grammar-report.js.map +1 -1
  65. package/lib/commands/lint-grammar.js +1 -1
  66. package/lib/commands/lint-grammar.js.map +1 -1
  67. package/lib/commands/measure-memory-args.d.ts +3 -2
  68. package/lib/commands/measure-memory-args.d.ts.map +1 -1
  69. package/lib/commands/measure-memory-args.js +22 -11
  70. package/lib/commands/measure-memory-args.js.map +1 -1
  71. package/lib/commands/measure-memory-driver.js +5 -3
  72. package/lib/commands/measure-memory-driver.js.map +1 -1
  73. package/lib/commands/measure-memory.js +1 -1
  74. package/lib/commands/measure-memory.js.map +1 -1
  75. package/lib/commands/model-docs-args.d.ts +4 -3
  76. package/lib/commands/model-docs-args.d.ts.map +1 -1
  77. package/lib/commands/model-docs-args.js +12 -10
  78. package/lib/commands/model-docs-args.js.map +1 -1
  79. package/lib/commands/model-docs-driver.js +2 -1
  80. package/lib/commands/model-docs-driver.js.map +1 -1
  81. package/lib/commands/model-docs.js +1 -1
  82. package/lib/commands/model-docs.js.map +1 -1
  83. package/lib/commands/projects-args.d.ts.map +1 -1
  84. package/lib/commands/projects-args.js +5 -5
  85. package/lib/commands/projects-args.js.map +1 -1
  86. package/lib/commands/projects.d.ts +3 -4
  87. package/lib/commands/projects.d.ts.map +1 -1
  88. package/lib/commands/projects.js +2 -2
  89. package/lib/commands/projects.js.map +1 -1
  90. package/lib/commands/query-args.d.ts.map +1 -1
  91. package/lib/commands/query-args.js +5 -5
  92. package/lib/commands/query-args.js.map +1 -1
  93. package/lib/commands/query.d.ts +3 -4
  94. package/lib/commands/query.d.ts.map +1 -1
  95. package/lib/commands/query.js +2 -2
  96. package/lib/commands/query.js.map +1 -1
  97. package/lib/commands/reflect-args.d.ts +3 -2
  98. package/lib/commands/reflect-args.d.ts.map +1 -1
  99. package/lib/commands/reflect-args.js +13 -10
  100. package/lib/commands/reflect-args.js.map +1 -1
  101. package/lib/commands/reflect-driver.js +2 -1
  102. package/lib/commands/reflect-driver.js.map +1 -1
  103. package/lib/commands/reflect-report.js.map +1 -1
  104. package/lib/commands/reflect.js +1 -1
  105. package/lib/commands/reflect.js.map +1 -1
  106. package/lib/commands/save-args.d.ts.map +1 -1
  107. package/lib/commands/save-args.js +6 -4
  108. package/lib/commands/save-args.js.map +1 -1
  109. package/lib/commands/save.d.ts +8 -4
  110. package/lib/commands/save.d.ts.map +1 -1
  111. package/lib/commands/save.js +31 -6
  112. package/lib/commands/save.js.map +1 -1
  113. package/lib/commands/validate-args.d.ts +3 -2
  114. package/lib/commands/validate-args.d.ts.map +1 -1
  115. package/lib/commands/validate-args.js +13 -10
  116. package/lib/commands/validate-args.js.map +1 -1
  117. package/lib/commands/validate-driver.js +2 -1
  118. package/lib/commands/validate-driver.js.map +1 -1
  119. package/lib/commands/validate-report.js.map +1 -1
  120. package/lib/commands/validate.js +1 -1
  121. package/lib/commands/validate.js.map +1 -1
  122. package/lib/commands/watch-args.d.ts.map +1 -1
  123. package/lib/commands/watch-args.js +6 -4
  124. package/lib/commands/watch-args.js.map +1 -1
  125. package/lib/commands/watch.d.ts +3 -4
  126. package/lib/commands/watch.d.ts.map +1 -1
  127. package/lib/commands/watch.js +20 -2
  128. package/lib/commands/watch.js.map +1 -1
  129. package/lib/driver-heap.d.ts +52 -0
  130. package/lib/driver-heap.d.ts.map +1 -0
  131. package/lib/driver-heap.js +51 -0
  132. package/lib/driver-heap.js.map +1 -0
  133. package/lib/index.d.ts +1 -0
  134. package/lib/index.d.ts.map +1 -1
  135. package/lib/index.js +1 -0
  136. package/lib/index.js.map +1 -1
  137. package/lib/log-level.d.ts +21 -11
  138. package/lib/log-level.d.ts.map +1 -1
  139. package/lib/log-level.js +25 -14
  140. package/lib/log-level.js.map +1 -1
  141. package/lib/spawn-data-server.d.ts +6 -1
  142. package/lib/spawn-data-server.d.ts.map +1 -1
  143. package/lib/spawn-data-server.js +2 -1
  144. package/lib/spawn-data-server.js.map +1 -1
  145. package/lib/stderr-connection-logger.d.ts +12 -0
  146. package/lib/stderr-connection-logger.d.ts.map +1 -0
  147. package/lib/stderr-connection-logger.js +16 -0
  148. package/lib/stderr-connection-logger.js.map +1 -0
  149. package/lib/testing/echo-server.js +9 -1
  150. package/lib/testing/echo-server.js.map +1 -1
  151. package/package.json +50 -39
  152. package/src/cli.ts +2 -1
  153. package/src/commands/analyze-heap.ts +4 -1
  154. package/src/commands/ast-ground-truth-args.ts +15 -12
  155. package/src/commands/ast-ground-truth-driver.ts +2 -1
  156. package/src/commands/ast-ground-truth.ts +1 -1
  157. package/src/commands/generate-ast-builder.ts +246 -0
  158. package/src/commands/generate-transfer-model-args.ts +11 -1
  159. package/src/commands/generate-transfer-model-config.ts +6 -3
  160. package/src/commands/generate-transfer-model.ts +27 -4
  161. package/src/commands/harness-args.ts +76 -31
  162. package/src/commands/headless-harness.ts +86 -26
  163. package/src/commands/init-args.ts +6 -6
  164. package/src/commands/init-templates.ts +270 -154
  165. package/src/commands/init-wizard.ts +4 -4
  166. package/src/commands/init-workspace.ts +31 -7
  167. package/src/commands/init.ts +36 -26
  168. package/src/commands/lint-grammar-args.ts +17 -13
  169. package/src/commands/lint-grammar-driver.ts +2 -1
  170. package/src/commands/lint-grammar.ts +1 -1
  171. package/src/commands/measure-memory-args.ts +27 -14
  172. package/src/commands/measure-memory-driver.ts +5 -3
  173. package/src/commands/measure-memory.ts +1 -1
  174. package/src/commands/model-docs-args.ts +16 -13
  175. package/src/commands/model-docs-driver.ts +2 -1
  176. package/src/commands/model-docs.ts +1 -1
  177. package/src/commands/projects-args.ts +5 -4
  178. package/src/commands/projects.ts +4 -6
  179. package/src/commands/query-args.ts +5 -4
  180. package/src/commands/query.ts +4 -6
  181. package/src/commands/reflect-args.ts +17 -13
  182. package/src/commands/reflect-driver.ts +2 -1
  183. package/src/commands/reflect.ts +1 -1
  184. package/src/commands/save-args.ts +6 -3
  185. package/src/commands/save.ts +47 -10
  186. package/src/commands/validate-args.ts +17 -13
  187. package/src/commands/validate-driver.ts +2 -1
  188. package/src/commands/validate.ts +1 -1
  189. package/src/commands/watch-args.ts +6 -3
  190. package/src/commands/watch.ts +28 -7
  191. package/src/driver-heap.ts +65 -0
  192. package/src/index.ts +1 -0
  193. package/src/log-level.ts +42 -14
  194. package/src/spawn-data-server.ts +14 -2
  195. package/src/stderr-connection-logger.ts +18 -0
  196. 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 prettier check as-is, and the
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
- 'vscode-languageserver': '~10.0.1'
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 `prettier-plugin-packagejson`'s canonical one, `private`
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
- "hydranium",
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-transfer tsconfig.tsbuildinfo",
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-transfer/transfer-model.ts --element-type-name __NAME__Element --terminals-name __NAME__Terminals --regen-command \\"Run: __NPM_RUN__ generate:transfer-model\\"",
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": "^5.8.0",
306
- "vitest": "^4.0.0"
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 `prettier-plugin-packagejson` sorts `bin` and any other
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
- // Emitted only where a root eslint config says the repo lints. The absence is
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 lint = composition.packaging.workspace?.eslintConfig === undefined ? '' : ' "lint": "eslint src test --max-warnings 0",\n';
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
- /** Compare two JSON option values. Sound here because compiler options are scalars and flat arrays. */
424
- function jsonEquals(left, right) {
425
- return JSON.stringify(left) === JSON.stringify(right);
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,11 +488,11 @@ 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) && !jsonEquals(base[key], value))
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
@@ -460,7 +509,7 @@ const TSCONFIG_TEST = `{
460
509
  "isolatedModules": true,
461
510
  "types": ["node"]
462
511
  },
463
- "include": ["src", "test"]
512
+ "include": ["src", "test", "src/**/*.json"]
464
513
  }
465
514
  `;
466
515
  const VITEST_CONFIG = `import { defineConfig } from 'vitest/config';
@@ -478,6 +527,11 @@ export default defineConfig({
478
527
  }
479
528
  });
480
529
  `;
530
+ /**
531
+ * The `.gitignore`, written for a workspace member too: its root may not ignore
532
+ * `lib/` (a `yo langium` root ignores `out/`), and a rule the root repeats
533
+ * costs nothing.
534
+ */
481
535
  const GITIGNORE = `node_modules/
482
536
  lib/
483
537
  *.tsbuildinfo
@@ -485,26 +539,6 @@ lib/
485
539
  # Langium-generated TextMate grammar (rewritten by every \`langium generate\`).
486
540
  syntaxes/
487
541
  `;
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
542
  /** The terminals every grammar needs, shared verbatim by the fragment and the lone-grammar case. */
509
543
  const TERMINALS = `hidden terminal WS: /\\s+/;
510
544
  terminal ID: /[_a-zA-Z][\\w_]*/;
@@ -644,13 +678,35 @@ export type __NAME__Services = LangiumServices &
644
678
  LspServerAddedServices & {
645
679
  shared: __NAME__SharedServices;
646
680
  };
681
+
682
+ /** What a host or a test may vary about this composition. */
683
+ export interface __NAME__Options {
684
+ /**
685
+ * Shared modules layered in after the framework's own bindings.
686
+ *
687
+ * The framework constructs most shared services with no options —
688
+ * \`DocumentBuilder: services => new HydraniumDocumentBuilder(services)\` — so
689
+ * rebinding the slot is the only way to boot one configured differently, and
690
+ * a factory that hard-codes its composition leaves a test nowhere to do it.
691
+ * That is what this is for: pass a module binding \`workspace.DocumentBuilder\`
692
+ * to exercise a builder option, or to substitute a subclass.
693
+ *
694
+ * Composed LAST, after \`__NAME__SharedModule\`, so it wins over every other
695
+ * tier including this file's own bindings — which is what makes it usable for
696
+ * a slot the adopter overrides. Production code should not reach for it.
697
+ */
698
+ readonly extraSharedModules?: ReadonlyArray<Module<__NAME__SharedServices, DeepPartial<__NAME__SharedServices>>>;
699
+ }
647
700
  ${configurationRoot}
648
701
  ${sharedModule}
649
702
 
650
703
  ${languageModules}
651
704
 
652
705
  /** Compose the Langium DI tree for __NAME__ — returns the shared + language services. */
653
- export function create__NAME__Services(context: Partial<ServerModuleContext> = EmptyFileSystem): {
706
+ export function create__NAME__Services(
707
+ context: Partial<ServerModuleContext> = EmptyFileSystem,
708
+ options: __NAME__Options = {}
709
+ ): {
654
710
  shared: __NAME__SharedServices;
655
711
  ${returnType}
656
712
  } {
@@ -660,7 +716,8 @@ ${returnType}
660
716
  sharedModules: {
661
717
  generated: __NAME__GeneratedSharedModule,
662
718
  adopter: __NAME__SharedModule,
663
- extra: [createLspServerSharedModule(fullContext)]
719
+ extra: [createLspServerSharedModule(fullContext)],
720
+ overrides: options.extraSharedModules
664
721
  },
665
722
  languageModules: {
666
723
  generated: ${primary.grammar}GeneratedModule,
@@ -724,9 +781,9 @@ const SERIALIZER = `// Concrete-syntax emitter for __GRAMMAR__ — the parser's
724
781
  //
725
782
  // Without it every structured write fails: the framework's default binding at
726
783
  // \`services.serializer.Serializer\` THROWS, because a concrete syntax is
727
- // language knowledge no framework can derive. \`ModelService.update\` / \`save\`,
728
- // the data head's \`saveModelDocument\` and a GLSP \`SaveModelAction\` all reach
729
- // it.
784
+ // language knowledge no framework can derive. A client session's \`update\` /
785
+ // \`save\`, the data head's \`saveModelDocument\` and a GLSP \`SaveModelAction\`
786
+ // all reach it.
730
787
  //
731
788
  // \`AbstractSerializer\`'s generic property walk lays out FORMAT-structured
732
789
  // output — its YAML and JSON subclasses are what it exists for — and cannot
@@ -902,7 +959,7 @@ import { startGlspServer } from '@hydranium/glsp-server/node';
902
959
  // \`TransferElement\` structurally, so naming the AST type here compiles fine and
903
960
  // silently tells every typed client that a reference is a resolvable object
904
961
  // rather than a name.
905
- ${importList(roots, './language-server/generated-transfer/transfer-model.js', columns, true)}
962
+ ${importList(roots, './language-server/generated-hydranium/transfer-model.js', columns, true)}
906
963
  `
907
964
  : '';
908
965
  // The commands live in `head-ports.ts`, not here: a host shell has to name
@@ -924,19 +981,22 @@ const dataServer = startSocketServer({ port: 0, logTag: 'ModelServer', logger: s
924
981
  return { dispose: () => undefined };
925
982
  });
926
983
  dataServer.started
927
- .then(() => {
928
- const { port } = dataServer;
929
- if (port === undefined) {
930
- // \`started\` resolves only once the address is resolved, so this is
931
- // unreachable; a non-null assertion in its place would publish
932
- // \`undefined\`, which the host cannot tell from an unreachable port.
933
- throw new Error('the data head started without a resolved port');
934
- }
935
- publishPortOnLspConnection(shared.lsp.Connection, ${upper}_DATA_SERVER_PORT_COMMAND, port);
936
- })
984
+ .then(
985
+ () => {
986
+ const { port } = dataServer;
987
+ if (port === undefined) {
988
+ // \`started\` resolves only once the address is resolved, so this is
989
+ // unreachable; a non-null assertion in its place would publish
990
+ // \`undefined\`, which the host cannot tell from an unreachable port.
991
+ throw new Error('the data head started without a resolved port');
992
+ }
993
+ publishPortOnLspConnection(shared.lsp.Connection, ${upper}_DATA_SERVER_PORT_COMMAND, port);
994
+ },
995
+ // The launcher logged its own bind failure; the rejection is control flow.
996
+ () => undefined
997
+ )
937
998
  .catch((error: unknown) => {
938
- const reason = error instanceof Error ? error.message : String(error);
939
- shared.Logger.error(\`[ModelServer] Could not publish the data-server port: \${reason}\`);
999
+ shared.Logger.error(\`[ModelServer] Could not publish the data-server port: \${describeErrorWithStack(error)}\`);
940
1000
  });
941
1001
  `
942
1002
  : '';
@@ -979,11 +1039,17 @@ void glspServer;
979
1039
  // than a library entry — import \`./index.js\` instead to compose the language.
980
1040
 
981
1041
  ${reflectImport}${glspImports}${importList(coreNodeSymbols, '@hydranium/core/node', columns)}
982
- ${data ? "import { DataServer } from '@hydranium/data-server';\n" : ''}import { startLanguageServer } from '@hydranium/langium/lsp';
1042
+ ${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
1043
+ // signature-compatible and delegates straight through; what it adds first is
1044
+ // \`assertLspHeadComposed\`, which fails the start if the LSP head's SHARED module
1045
+ // was never composed. Reaching for Langium's is the natural mistake and it is
1046
+ // silent: the server boots, links and completes, while echo suppression, the
1047
+ // didChangeContent debounce and the last-client-close rebuild are all inert.
1048
+ import { startLanguageServer, withHydraniumLspFeatures } from '@hydranium/core/lsp';
983
1049
  import { ProposedFeatures, createConnection } from 'vscode-languageserver/node';
984
1050
  ${diagramImports}${transferImports}import { create__NAME__Services } from './language-server/__PROJECT_ID__-module.js';
985
1051
  ${portCommands === '' ? '' : '\n' + portCommands}
986
- const connection = createConnection(ProposedFeatures.all);
1052
+ const connection = createConnection(withHydraniumLspFeatures(ProposedFeatures.all));
987
1053
  const { shared } = create__NAME__Services({ connection, ...NodeFileSystem });
988
1054
  startLanguageServer(shared);
989
1055
  ${dataBlock}${glspBlock}`;
@@ -1031,7 +1097,7 @@ function dataServerMainFile(composition) {
1031
1097
  ${importList(['NodeFileSystem', 'startStdioServer'], '@hydranium/core/node', columns)}
1032
1098
  import { DataServer } from '@hydranium/data-server';
1033
1099
  // The TRANSFER root${plural}, not the AST one${plural} — same reasoning as \`main.ts\`.
1034
- ${importList(roots, './language-server/generated-transfer/transfer-model.js', columns, true)}
1100
+ ${importList(roots, './language-server/generated-hydranium/transfer-model.js', columns, true)}
1035
1101
  import { create__NAME__Services } from './language-server/__PROJECT_ID__-module.js';
1036
1102
 
1037
1103
  const { shared } = create__NAME__Services({ ...NodeFileSystem });
@@ -1147,8 +1213,8 @@ function serializationTest(composition) {
1147
1213
  const transferTypes = composition.grammars.map(grammar => `${grammar.entryRule} as Transfer${grammar.entryRule}`);
1148
1214
  const text = `// Round-trips each grammar through its serializer: parse the source, serialize
1149
1215
  // the model, compare the text. The serializer is the parser's inverse, so this
1150
- // is the assertion that keeps \`ModelService.update\` / \`save\` writing files the
1151
- // language server can read back.
1216
+ // is the assertion that keeps a client session's \`update\` / \`save\` writing
1217
+ // files the language server can read back.
1152
1218
  //
1153
1219
  // The transfer case is the one that catches the mistake worth catching.
1154
1220
  // \`ModelService.modelToText\` short-circuits only a RAW STRING, so a typed model
@@ -1158,7 +1224,7 @@ function serializationTest(composition) {
1158
1224
  import { parseHelper } from '@hydranium/core/testing';
1159
1225
  import { describe, expect, it } from 'vitest';
1160
1226
  ${importList(astTypes, '../src/language-server/ast.js', columns, true)}
1161
- ${importList(transferTypes, '../src/language-server/generated-transfer/transfer-model.js', columns, true)}
1227
+ ${importList(transferTypes, '../src/language-server/generated-hydranium/transfer-model.js', columns, true)}
1162
1228
  import { createServices } from '../src/services.js';
1163
1229
 
1164
1230
  ${composition.grammars.map(grammar => render(SERIALIZATION_SUITE, composition, grammar)).join('\n\n')}
@@ -1317,20 +1383,6 @@ function readme(composition) {
1317
1383
  '',
1318
1384
  '## Getting started',
1319
1385
  '',
1320
- // Conditional on the pin this scaffold actually carries — see
1321
- // `UNPUBLISHED_FRAMEWORK_VERSION`.
1322
- ...(composition.frameworkVersion === UNPUBLISHED_FRAMEWORK_VERSION
1323
- ? [
1324
- '> **Pre-publish note.** `@hydranium/*` is not on npm yet, so the `0.0.0`',
1325
- '> pins below are placeholders and `npm install` will fail with a 404 until',
1326
- '> the framework is released. Until then, supply the packages from a local',
1327
- '> framework checkout with [yalc](https://github.com/wclr/yalc) — a plain',
1328
- '> `file:` path or `npm link` is not enough, because the framework packages',
1329
- '> depend on each other by version and npm would try to fetch those from the',
1330
- '> registry too.',
1331
- ''
1332
- ]
1333
- : []),
1334
1386
  '```bash',
1335
1387
  'npm install',
1336
1388
  'npm run langium:generate # generate the AST from the grammar',
@@ -1438,6 +1490,24 @@ function readme(composition) {
1438
1490
  'atomic set and depends on a single physical copy, so a floating range can',
1439
1491
  'silently resolve a second one.',
1440
1492
  '',
1493
+ '`vscode-languageserver` is pinned to `10.0.1`, and',
1494
+ '`vscode-languageserver-protocol` and `vscode-jsonrpc` to the `3.18.1` and',
1495
+ '`9.0.0` it pins, so Langium, the LSP connection and the framework share one',
1496
+ 'copy of each. With two, an error a Langium handler returns, such as a request',
1497
+ 'for a document the server does not have, reaches the client as a result, and a',
1498
+ 'framework error your own LSP handler throws loses its code. The Langium 4.4',
1499
+ 'upgrade',
1500
+ '([eclipse-emfcloud/hydranium#142](https://github.com/eclipse-emfcloud/hydranium/issues/142))',
1501
+ 'replaces these pins.',
1502
+ '',
1503
+ ...(pinsVitest(composition)
1504
+ ? [
1505
+ '`vitest` is held below 4.1, whose peer set crashes the npm every Node 22',
1506
+ 'release bundles. Raise it once every npm that installs this project is',
1507
+ '11.6 or later.',
1508
+ ''
1509
+ ]
1510
+ : []),
1441
1511
  '## If your repo gates license headers',
1442
1512
  '',
1443
1513
  'The emitted `.ts` files carry no copyright header — the scaffold cannot know',
@@ -1456,10 +1526,9 @@ function readme(composition) {
1456
1526
  /**
1457
1527
  * The GLSP files for ONE grammar's diagram.
1458
1528
  *
1459
- * Per grammar rather than per project because a diagram type has exactly one
1460
- * grammar — `AbstractHydraniumGlspDiagramModule.declareLanguage()` returns that
1461
- * grammar's `LanguageMetaData`, and it is bound on the SESSION container so a
1462
- * grammar with no diagram can sit on the same server.
1529
+ * Per grammar rather than per project because the state, storage and GModel
1530
+ * factory are typed over that grammar's model, and a grammar with no diagram
1531
+ * can sit on the same server.
1463
1532
  *
1464
1533
  * The index and the computed-bounds handler are framework classes used
1465
1534
  * unmodified; storage and the submission handler are thin typed subclasses,
@@ -1524,8 +1593,11 @@ export class __GRAMMAR__GlspState extends FullTextHydraniumGlspState<__ENTRY_RUL
1524
1593
  `;
1525
1594
  const storage = `// Source-model storage for the __GRAMMAR__ diagram, inheriting both framework
1526
1595
  // defaults: \`loadSourceModel\` (open + settle + \`setSourceRoot\`) and
1527
- // \`saveSourceModel\` (through \`ModelService.save\` → the per-URI \`Serializer\` →
1528
- // the multi-client text store → \`WritableFileSystemProvider\`).
1596
+ // \`saveSourceModel\` (flush the store's text for the primary and every tracked
1597
+ // secondary through the diagram's client session).
1598
+ //
1599
+ // No serializer runs on the save path — a save persists what the diagram's
1600
+ // operations already wrote to the store.
1529
1601
  //
1530
1602
  // The subclass exists so a bespoke load or save has a stable place to land.
1531
1603
 
@@ -1681,27 +1753,28 @@ export class __GRAMMAR__DiagramConfiguration implements DiagramConfiguration {
1681
1753
  // read-only is a deletion the compiler checks, while read-only → editable is
1682
1754
  // authoring against a seam you have not used yet.
1683
1755
  //
1684
- // **It composes TEXT rather than mutating the AST.** The source model of
1685
- // \`FullTextHydraniumGlspState\` is the document text, and reading it back through
1686
- // \`state.sourceModel\` serialises the AST through the per-URI \`Serializer\`, which
1687
- // this scaffold does not bind — so that getter throws until you do. Appending a
1688
- // declaration to the text the parser last read needs no serializer, which is what
1689
- // makes a scaffolded diagram editable on day one. Bind a \`Serializer\` at
1690
- // \`services.serializer.Serializer\` and this becomes a
1691
- // \`HydraniumGlspRecordingCommand\` over \`state.sourceModel\` instead — the same
1692
- // binding the diagram's own save action needs.
1756
+ // **It edits the operation's working copy.** A handler runs inside an operation,
1757
+ // where \`sourceRoot\` is a copy of the built root: the recording command's
1758
+ // runnable appends to the copy, and the operation writes the copy's text through
1759
+ // the grammar's \`Serializer\` once, gated on the version the copy was made at,
1760
+ // with undo and redo applying that one change. A command that writes the
1761
+ // document itself instead reads no text off the copy, which has no \`$document\`,
1762
+ // and the operation's own write then overwrites it.
1693
1763
  //
1694
1764
  // **The drop location is discarded.** \`needsClientLayout\` is \`true\` and the
1695
1765
  // starter grammar persists no bounds, so there is nowhere to put a coordinate: the
1696
1766
  // node is appended at the end of the document and the client places it.
1697
1767
  //
1698
1768
  // Like \`types.ts\`, \`gmodel-factory.ts\` and \`diagram-configuration.ts\`, this file
1699
- // knows the starter grammar's concrete syntax — the \`node\` keyword below is that
1700
- // grammar's. Replacing the grammar means replacing these four together.
1769
+ // knows the starter grammar's shape — the \`nodes\` list and the \`__NODE_RULE__\`
1770
+ // it holds. Replacing the grammar means replacing these four together.
1701
1771
 
1702
1772
  import { type Command, type CreateNodeOperation, JsonCreateNodeOperationHandler, type MaybePromise } from '@eclipse-glsp/server';
1773
+ import { appendChild } from '@hydranium/core';
1774
+ import { HydraniumGlspRecordingCommand } from '@hydranium/glsp-server';
1703
1775
  import { findNextUnique } from '@hydranium/protocol';
1704
1776
  import { injectable } from 'inversify';
1777
+ import type { __NODE_RULE__ } from '../../language-server/ast.js';
1705
1778
  import { ${upper}_NODE_TYPE } from './types.js';
1706
1779
  import type { __GRAMMAR__GlspState } from './state.js';
1707
1780
 
@@ -1720,45 +1793,19 @@ export class __GRAMMAR__CreateNodeOperationHandler extends JsonCreateNodeOperati
1720
1793
  if (!this.elementTypeIds.includes(operation.elementTypeId)) {
1721
1794
  return undefined;
1722
1795
  }
1723
- const state = this.modelState;
1724
- const before = this.documentText();
1725
- const after = this.withNode(
1726
- before,
1727
- findNextUnique(
1796
+ return new HydraniumGlspRecordingCommand(this.modelState, this.label, () => {
1797
+ const root = this.modelState.sourceRoot;
1798
+ const name = findNextUnique(
1728
1799
  NODE_NAME_STEM,
1729
- state.sourceRoot.nodes.map(node => node.name)
1730
- )
1731
- );
1732
- // Whole-document undo, which is all a full-text source model can offer: it
1733
- // has exactly one field, so there is nothing to merge a concurrent edit into.
1734
- return {
1735
- execute: () => state.updateSourceModel({ text: after }),
1736
- undo: () => state.updateSourceModel({ text: before }),
1737
- redo: () => state.updateSourceModel({ text: after })
1738
- };
1739
- }
1740
-
1741
- /** The text the captured source root was parsed from — the baseline an edit appends to. */
1742
- protected documentText(): string {
1743
- return this.modelState.sourceRoot.$document?.textDocument.getText() ?? '';
1744
- }
1745
-
1746
- /** \`text\` with one more node declaration, under exactly one trailing newline. */
1747
- protected withNode(text: string, name: string): string {
1748
- const body = text.trimEnd();
1749
- const declaration = \`node \${name}\`;
1750
- return body.length === 0 ? \`\${declaration}\\n\` : \`\${body}\\n\${declaration}\\n\`;
1800
+ root.nodes.map(node => node.name)
1801
+ );
1802
+ appendChild(root, 'nodes', root.nodes, { $type: '__NODE_RULE__', name } as __NODE_RULE__);
1803
+ });
1751
1804
  }
1752
1805
  }
1753
1806
  `;
1754
1807
  const module = `// GLSP diagram module for __GRAMMAR__ — the DI wiring of one diagram type.
1755
1808
  //
1756
- // \`declareLanguage\` is the multi-grammar seam: it binds the grammar this diagram
1757
- // edits on the SESSION container, so \`modelState.diagramLanguage\` and the
1758
- // per-language lookups resolve to __LANGUAGE_ID__. That is what stops one
1759
- // diagram type from fighting over a process-wide binding when a project holds
1760
- // several grammars — a grammar with no diagram is registered on the same server.
1761
- //
1762
1809
  // \`configureActionHandlers\` REBINDS rather than adds: GLSP's own
1763
1810
  // \`DiagramModule\` already registers \`ComputedBoundsActionHandler\`, and two
1764
1811
  // handlers for one action would both run.
@@ -1780,9 +1827,7 @@ ${importList([
1780
1827
  'SourceModelStorage'
1781
1828
  ], '@eclipse-glsp/server', columns, true)}
1782
1829
  import { ComputedBoundsActionHandler } from '@eclipse-glsp/server';
1783
- import type { LanguageMetaData } from '@hydranium/langium';
1784
1830
  ${importList(['HydraniumGlspComputedBoundsActionHandler', 'AbstractHydraniumGlspDiagramModule', 'HydraniumGlspIndex'], '@hydranium/glsp-server', columns)}
1785
- import { __GRAMMAR__LanguageMetaData } from '../../language-server/generated/module.js';
1786
1831
  import { __GRAMMAR__CreateNodeOperationHandler } from './create-node-operation-handler.js';
1787
1832
  import { __GRAMMAR__DiagramConfiguration } from './diagram-configuration.js';
1788
1833
  import { __GRAMMAR__GModelFactory } from './gmodel-factory.js';
@@ -1794,10 +1839,6 @@ import { ${upper}_DIAGRAM_TYPE } from './types.js';
1794
1839
  export class __GRAMMAR__DiagramModule extends AbstractHydraniumGlspDiagramModule {
1795
1840
  readonly diagramType = ${upper}_DIAGRAM_TYPE;
1796
1841
 
1797
- protected override declareLanguage(): LanguageMetaData {
1798
- return __GRAMMAR__LanguageMetaData;
1799
- }
1800
-
1801
1842
  protected override bindModelState(): BindingTarget<ModelState> {
1802
1843
  return { service: __GRAMMAR__GlspState };
1803
1844
  }
@@ -1833,6 +1874,75 @@ export class __GRAMMAR__DiagramModule extends AbstractHydraniumGlspDiagramModule
1833
1874
  binding.add(__GRAMMAR__CreateNodeOperationHandler);
1834
1875
  }
1835
1876
  }
1877
+ `;
1878
+ const diagramTest = `// The starter create-node handler, run the way a client runs it: an in-process
1879
+ // GLSP server opens a document, a create-node operation adds a node, and an undo
1880
+ // and a redo take it out and put it back. Every step must keep the nodes the
1881
+ // document already had, and its comments: the diagram writes the document from
1882
+ // its AST, which carries none.
1883
+
1884
+ import 'reflect-metadata';
1885
+ import { type Action, CreateNodeOperation, RedoAction, ServerModule, SetDirtyStateAction, UndoAction } from '@eclipse-glsp/server';
1886
+ import { initializeWorkspaceProgrammatically } from '@hydranium/core';
1887
+ import { makeScratchWorkspace, type ScratchWorkspace } from '@hydranium/core/testing/node';
1888
+ import { HydraniumGlspAppModule } from '@hydranium/glsp-server';
1889
+ import { type GlspHarness, makeGlspHarness } from '@hydranium/glsp-server/testing';
1890
+ import { waitFor } from '@hydranium/protocol/testing';
1891
+ import { afterEach, describe, expect, it } from 'vitest';
1892
+ import { __GRAMMAR__DiagramModule } from '../src/glsp/__GRAMMAR_ID__/diagram-module.js';
1893
+ import type { __GRAMMAR__GlspState } from '../src/glsp/__GRAMMAR_ID__/state.js';
1894
+ import { ${upper}_DIAGRAM_TYPE, ${upper}_NODE_TYPE } from '../src/glsp/__GRAMMAR_ID__/types.js';
1895
+ import { createServices } from '../src/services.js';
1896
+
1897
+ let workspace: ScratchWorkspace | undefined;
1898
+ let diagram: GlspHarness<__GRAMMAR__GlspState> | undefined;
1899
+
1900
+ afterEach(() => {
1901
+ diagram?.dispose();
1902
+ workspace?.dispose();
1903
+ diagram = undefined;
1904
+ workspace = undefined;
1905
+ });
1906
+
1907
+ /** Send \`action\` and wait for the dirty state the server answers it with, which it sends once the edit is written. */
1908
+ async function send(harness: GlspHarness<__GRAMMAR__GlspState>, action: Action, reason: string): Promise<void> {
1909
+ const before = harness.actions.length;
1910
+ harness.dispatch(action);
1911
+ await waitFor(() => harness.actions.slice(before).some(sent => SetDirtyStateAction.is(sent) && sent.reason === reason), {
1912
+ message: \`no '\${reason}' dirty state\`
1913
+ });
1914
+ }
1915
+
1916
+ describe('__GRAMMAR__ diagram', () => {
1917
+ it('creates a node after the existing ones, keeping their comments, and undoes and redoes it', async () => {
1918
+ workspace = makeScratchWorkspace({ prefix: '__GRAMMAR_ID__-diagram-' });
1919
+ const original = '// keep this comment\\nnode first -> second\\n// about second\\nnode second';
1920
+ const file = workspace.write('model.__EXTENSION__', original);
1921
+ const { shared } = createServices();
1922
+ await initializeWorkspaceProgrammatically(shared, workspace.root);
1923
+ diagram = makeGlspHarness<__GRAMMAR__GlspState>({
1924
+ serverModule: new ServerModule().configureDiagramModule(new __GRAMMAR__DiagramModule()),
1925
+ diagramType: ${upper}_DIAGRAM_TYPE,
1926
+ appModules: [new HydraniumGlspAppModule({ shared })]
1927
+ });
1928
+ await diagram.start();
1929
+ await diagram.openDocument(file);
1930
+ const uri = workspace.uri('model.__EXTENSION__');
1931
+ const text = (): string | undefined => shared.workspace.TextDocuments.get(uri)?.getText();
1932
+
1933
+ await send(diagram, CreateNodeOperation.create(${upper}_NODE_TYPE), 'operation');
1934
+ const created = text();
1935
+ await send(diagram, UndoAction.create(), 'undo');
1936
+ const undone = text();
1937
+ await send(diagram, RedoAction.create(), 'redo');
1938
+
1939
+ expect({ created, undone, redone: text() }).toEqual({
1940
+ created: \`\${original}\\nnode Node\`,
1941
+ undone: original,
1942
+ redone: \`\${original}\\nnode Node\`
1943
+ });
1944
+ });
1945
+ });
1836
1946
  `;
1837
1947
  return [
1838
1948
  { path: `${dir}/types.ts`, content: render(types) },
@@ -1842,7 +1952,8 @@ export class __GRAMMAR__DiagramModule extends AbstractHydraniumGlspDiagramModule
1842
1952
  { path: `${dir}/gmodel-factory.ts`, content: render(factory) },
1843
1953
  { path: `${dir}/diagram-configuration.ts`, content: render(configuration) },
1844
1954
  { path: `${dir}/create-node-operation-handler.ts`, content: render(createHandler) },
1845
- { path: `${dir}/diagram-module.ts`, content: render(module) }
1955
+ { path: `${dir}/diagram-module.ts`, content: render(module) },
1956
+ { path: `test/${grammar.grammarId}-diagram.test.ts`, content: render(diagramTest) }
1846
1957
  ];
1847
1958
  }
1848
1959
  /**
@@ -1868,10 +1979,7 @@ export function buildInitTemplates(composition) {
1868
1979
  { path: 'tsconfig.json', content: tsconfigJson(composition) },
1869
1980
  { path: 'tsconfig.test.json', content: TSCONFIG_TEST },
1870
1981
  { path: 'vitest.config.ts', content: VITEST_CONFIG },
1871
- {
1872
- path: '.gitignore',
1873
- content: composition.packaging.workspace === undefined ? GITIGNORE : GITIGNORE_WORKSPACE_MEMBER
1874
- },
1982
+ { path: '.gitignore', content: GITIGNORE },
1875
1983
  { path: 'README.md', content: readme(composition) },
1876
1984
  ...grammarFiles,
1877
1985
  { path: `src/language-server/${composition.projectId}-module.ts`, content: moduleFile(composition) },