@hydranium/cli 1.0.0-next.10

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 (267) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +240 -0
  3. package/heap-analysis/analyze-heap.mjs +798 -0
  4. package/heap-analysis/heap/core/aggregate.mjs +152 -0
  5. package/heap-analysis/heap/core/bucket-label.mjs +23 -0
  6. package/heap-analysis/heap/core/cutpoints.mjs +51 -0
  7. package/heap-analysis/heap/core/diff.mjs +56 -0
  8. package/heap-analysis/heap/core/format.mjs +47 -0
  9. package/heap-analysis/heap/core/holders.mjs +80 -0
  10. package/heap-analysis/heap/core/load.mjs +46 -0
  11. package/heap-analysis/heap/core/report.mjs +68 -0
  12. package/heap-analysis/heap/core/strings.mjs +80 -0
  13. package/heap-analysis/heap/langium/concepts.mjs +230 -0
  14. package/heap-analysis/heap/langium/glsp.classifier.mjs +80 -0
  15. package/heap-analysis/heap/langium/langium.classifier.mjs +203 -0
  16. package/heap-analysis/heap/renderer/renderer.mjs +122 -0
  17. package/lib/cli-version.d.ts +21 -0
  18. package/lib/cli-version.d.ts.map +1 -0
  19. package/lib/cli-version.js +30 -0
  20. package/lib/cli-version.js.map +1 -0
  21. package/lib/cli.d.ts +11 -0
  22. package/lib/cli.d.ts.map +1 -0
  23. package/lib/cli.js +135 -0
  24. package/lib/cli.js.map +1 -0
  25. package/lib/commands/analyze-heap-args.d.ts +46 -0
  26. package/lib/commands/analyze-heap-args.d.ts.map +1 -0
  27. package/lib/commands/analyze-heap-args.js +174 -0
  28. package/lib/commands/analyze-heap-args.js.map +1 -0
  29. package/lib/commands/analyze-heap.d.ts +37 -0
  30. package/lib/commands/analyze-heap.d.ts.map +1 -0
  31. package/lib/commands/analyze-heap.js +63 -0
  32. package/lib/commands/analyze-heap.js.map +1 -0
  33. package/lib/commands/ast-ground-truth-args.d.ts +23 -0
  34. package/lib/commands/ast-ground-truth-args.d.ts.map +1 -0
  35. package/lib/commands/ast-ground-truth-args.js +47 -0
  36. package/lib/commands/ast-ground-truth-args.js.map +1 -0
  37. package/lib/commands/ast-ground-truth-driver.d.ts +10 -0
  38. package/lib/commands/ast-ground-truth-driver.d.ts.map +1 -0
  39. package/lib/commands/ast-ground-truth-driver.js +72 -0
  40. package/lib/commands/ast-ground-truth-driver.js.map +1 -0
  41. package/lib/commands/ast-ground-truth.d.ts +31 -0
  42. package/lib/commands/ast-ground-truth.d.ts.map +1 -0
  43. package/lib/commands/ast-ground-truth.js +36 -0
  44. package/lib/commands/ast-ground-truth.js.map +1 -0
  45. package/lib/commands/generate-transfer-model-args.d.ts +38 -0
  46. package/lib/commands/generate-transfer-model-args.d.ts.map +1 -0
  47. package/lib/commands/generate-transfer-model-args.js +183 -0
  48. package/lib/commands/generate-transfer-model-args.js.map +1 -0
  49. package/lib/commands/generate-transfer-model-config.d.ts +32 -0
  50. package/lib/commands/generate-transfer-model-config.d.ts.map +1 -0
  51. package/lib/commands/generate-transfer-model-config.js +122 -0
  52. package/lib/commands/generate-transfer-model-config.js.map +1 -0
  53. package/lib/commands/generate-transfer-model.d.ts +110 -0
  54. package/lib/commands/generate-transfer-model.d.ts.map +1 -0
  55. package/lib/commands/generate-transfer-model.js +587 -0
  56. package/lib/commands/generate-transfer-model.js.map +1 -0
  57. package/lib/commands/harness-args.d.ts +198 -0
  58. package/lib/commands/harness-args.d.ts.map +1 -0
  59. package/lib/commands/harness-args.js +339 -0
  60. package/lib/commands/harness-args.js.map +1 -0
  61. package/lib/commands/headless-harness.d.ts +97 -0
  62. package/lib/commands/headless-harness.d.ts.map +1 -0
  63. package/lib/commands/headless-harness.js +121 -0
  64. package/lib/commands/headless-harness.js.map +1 -0
  65. package/lib/commands/init-args.d.ts +81 -0
  66. package/lib/commands/init-args.d.ts.map +1 -0
  67. package/lib/commands/init-args.js +250 -0
  68. package/lib/commands/init-args.js.map +1 -0
  69. package/lib/commands/init-prompt.d.ts +66 -0
  70. package/lib/commands/init-prompt.d.ts.map +1 -0
  71. package/lib/commands/init-prompt.js +71 -0
  72. package/lib/commands/init-prompt.js.map +1 -0
  73. package/lib/commands/init-templates.d.ts +42 -0
  74. package/lib/commands/init-templates.d.ts.map +1 -0
  75. package/lib/commands/init-templates.js +1902 -0
  76. package/lib/commands/init-templates.js.map +1 -0
  77. package/lib/commands/init-wizard.d.ts +65 -0
  78. package/lib/commands/init-wizard.d.ts.map +1 -0
  79. package/lib/commands/init-wizard.js +302 -0
  80. package/lib/commands/init-wizard.js.map +1 -0
  81. package/lib/commands/init-workspace.d.ts +168 -0
  82. package/lib/commands/init-workspace.d.ts.map +1 -0
  83. package/lib/commands/init-workspace.js +372 -0
  84. package/lib/commands/init-workspace.js.map +1 -0
  85. package/lib/commands/init.d.ts +273 -0
  86. package/lib/commands/init.d.ts.map +1 -0
  87. package/lib/commands/init.js +303 -0
  88. package/lib/commands/init.js.map +1 -0
  89. package/lib/commands/lint-grammar-args.d.ts +23 -0
  90. package/lib/commands/lint-grammar-args.d.ts.map +1 -0
  91. package/lib/commands/lint-grammar-args.js +63 -0
  92. package/lib/commands/lint-grammar-args.js.map +1 -0
  93. package/lib/commands/lint-grammar-driver.d.ts +10 -0
  94. package/lib/commands/lint-grammar-driver.d.ts.map +1 -0
  95. package/lib/commands/lint-grammar-driver.js +72 -0
  96. package/lib/commands/lint-grammar-driver.js.map +1 -0
  97. package/lib/commands/lint-grammar-report.d.ts +24 -0
  98. package/lib/commands/lint-grammar-report.d.ts.map +1 -0
  99. package/lib/commands/lint-grammar-report.js +65 -0
  100. package/lib/commands/lint-grammar-report.js.map +1 -0
  101. package/lib/commands/lint-grammar.d.ts +33 -0
  102. package/lib/commands/lint-grammar.d.ts.map +1 -0
  103. package/lib/commands/lint-grammar.js +42 -0
  104. package/lib/commands/lint-grammar.js.map +1 -0
  105. package/lib/commands/measure-memory-args.d.ts +23 -0
  106. package/lib/commands/measure-memory-args.d.ts.map +1 -0
  107. package/lib/commands/measure-memory-args.js +106 -0
  108. package/lib/commands/measure-memory-args.js.map +1 -0
  109. package/lib/commands/measure-memory-driver.d.ts +10 -0
  110. package/lib/commands/measure-memory-driver.d.ts.map +1 -0
  111. package/lib/commands/measure-memory-driver.js +110 -0
  112. package/lib/commands/measure-memory-driver.js.map +1 -0
  113. package/lib/commands/measure-memory.d.ts +68 -0
  114. package/lib/commands/measure-memory.d.ts.map +1 -0
  115. package/lib/commands/measure-memory.js +100 -0
  116. package/lib/commands/measure-memory.js.map +1 -0
  117. package/lib/commands/model-docs-args.d.ts +23 -0
  118. package/lib/commands/model-docs-args.d.ts.map +1 -0
  119. package/lib/commands/model-docs-args.js +53 -0
  120. package/lib/commands/model-docs-args.js.map +1 -0
  121. package/lib/commands/model-docs-driver.d.ts +10 -0
  122. package/lib/commands/model-docs-driver.d.ts.map +1 -0
  123. package/lib/commands/model-docs-driver.js +61 -0
  124. package/lib/commands/model-docs-driver.js.map +1 -0
  125. package/lib/commands/model-docs-report.d.ts +18 -0
  126. package/lib/commands/model-docs-report.d.ts.map +1 -0
  127. package/lib/commands/model-docs-report.js +123 -0
  128. package/lib/commands/model-docs-report.js.map +1 -0
  129. package/lib/commands/model-docs.d.ts +30 -0
  130. package/lib/commands/model-docs.d.ts.map +1 -0
  131. package/lib/commands/model-docs.js +37 -0
  132. package/lib/commands/model-docs.js.map +1 -0
  133. package/lib/commands/projects-args.d.ts +25 -0
  134. package/lib/commands/projects-args.d.ts.map +1 -0
  135. package/lib/commands/projects-args.js +49 -0
  136. package/lib/commands/projects-args.js.map +1 -0
  137. package/lib/commands/projects.d.ts +45 -0
  138. package/lib/commands/projects.d.ts.map +1 -0
  139. package/lib/commands/projects.js +39 -0
  140. package/lib/commands/projects.js.map +1 -0
  141. package/lib/commands/query-args.d.ts +25 -0
  142. package/lib/commands/query-args.d.ts.map +1 -0
  143. package/lib/commands/query-args.js +46 -0
  144. package/lib/commands/query-args.js.map +1 -0
  145. package/lib/commands/query.d.ts +32 -0
  146. package/lib/commands/query.d.ts.map +1 -0
  147. package/lib/commands/query.js +29 -0
  148. package/lib/commands/query.js.map +1 -0
  149. package/lib/commands/reflect-args.d.ts +23 -0
  150. package/lib/commands/reflect-args.d.ts.map +1 -0
  151. package/lib/commands/reflect-args.js +59 -0
  152. package/lib/commands/reflect-args.js.map +1 -0
  153. package/lib/commands/reflect-driver.d.ts +10 -0
  154. package/lib/commands/reflect-driver.d.ts.map +1 -0
  155. package/lib/commands/reflect-driver.js +66 -0
  156. package/lib/commands/reflect-driver.js.map +1 -0
  157. package/lib/commands/reflect-report.d.ts +20 -0
  158. package/lib/commands/reflect-report.d.ts.map +1 -0
  159. package/lib/commands/reflect-report.js +127 -0
  160. package/lib/commands/reflect-report.js.map +1 -0
  161. package/lib/commands/reflect.d.ts +30 -0
  162. package/lib/commands/reflect.d.ts.map +1 -0
  163. package/lib/commands/reflect.js +38 -0
  164. package/lib/commands/reflect.js.map +1 -0
  165. package/lib/commands/save-args.d.ts +26 -0
  166. package/lib/commands/save-args.d.ts.map +1 -0
  167. package/lib/commands/save-args.js +59 -0
  168. package/lib/commands/save-args.js.map +1 -0
  169. package/lib/commands/save.d.ts +38 -0
  170. package/lib/commands/save.d.ts.map +1 -0
  171. package/lib/commands/save.js +48 -0
  172. package/lib/commands/save.js.map +1 -0
  173. package/lib/commands/validate-args.d.ts +23 -0
  174. package/lib/commands/validate-args.d.ts.map +1 -0
  175. package/lib/commands/validate-args.js +60 -0
  176. package/lib/commands/validate-args.js.map +1 -0
  177. package/lib/commands/validate-driver.d.ts +10 -0
  178. package/lib/commands/validate-driver.d.ts.map +1 -0
  179. package/lib/commands/validate-driver.js +72 -0
  180. package/lib/commands/validate-driver.js.map +1 -0
  181. package/lib/commands/validate-report.d.ts +30 -0
  182. package/lib/commands/validate-report.d.ts.map +1 -0
  183. package/lib/commands/validate-report.js +87 -0
  184. package/lib/commands/validate-report.js.map +1 -0
  185. package/lib/commands/validate.d.ts +34 -0
  186. package/lib/commands/validate.d.ts.map +1 -0
  187. package/lib/commands/validate.js +41 -0
  188. package/lib/commands/validate.js.map +1 -0
  189. package/lib/commands/watch-args.d.ts +26 -0
  190. package/lib/commands/watch-args.d.ts.map +1 -0
  191. package/lib/commands/watch-args.js +59 -0
  192. package/lib/commands/watch-args.js.map +1 -0
  193. package/lib/commands/watch.d.ts +75 -0
  194. package/lib/commands/watch.d.ts.map +1 -0
  195. package/lib/commands/watch.js +112 -0
  196. package/lib/commands/watch.js.map +1 -0
  197. package/lib/index.d.ts +16 -0
  198. package/lib/index.d.ts.map +1 -0
  199. package/lib/index.js +16 -0
  200. package/lib/index.js.map +1 -0
  201. package/lib/log-level.d.ts +39 -0
  202. package/lib/log-level.d.ts.map +1 -0
  203. package/lib/log-level.js +47 -0
  204. package/lib/log-level.js.map +1 -0
  205. package/lib/package-name.d.ts +33 -0
  206. package/lib/package-name.d.ts.map +1 -0
  207. package/lib/package-name.js +37 -0
  208. package/lib/package-name.js.map +1 -0
  209. package/lib/spawn-data-server.d.ts +102 -0
  210. package/lib/spawn-data-server.d.ts.map +1 -0
  211. package/lib/spawn-data-server.js +154 -0
  212. package/lib/spawn-data-server.js.map +1 -0
  213. package/lib/testing/echo-server.d.ts +11 -0
  214. package/lib/testing/echo-server.d.ts.map +1 -0
  215. package/lib/testing/echo-server.js +79 -0
  216. package/lib/testing/echo-server.js.map +1 -0
  217. package/package.json +105 -0
  218. package/src/cli-version.ts +32 -0
  219. package/src/cli.ts +139 -0
  220. package/src/commands/analyze-heap-args.ts +184 -0
  221. package/src/commands/analyze-heap.ts +76 -0
  222. package/src/commands/ast-ground-truth-args.ts +63 -0
  223. package/src/commands/ast-ground-truth-driver.ts +81 -0
  224. package/src/commands/ast-ground-truth.ts +49 -0
  225. package/src/commands/generate-transfer-model-args.ts +203 -0
  226. package/src/commands/generate-transfer-model-config.ts +128 -0
  227. package/src/commands/generate-transfer-model.ts +716 -0
  228. package/src/commands/harness-args.ts +429 -0
  229. package/src/commands/headless-harness.ts +165 -0
  230. package/src/commands/init-args.ts +273 -0
  231. package/src/commands/init-prompt.ts +149 -0
  232. package/src/commands/init-templates.ts +2037 -0
  233. package/src/commands/init-wizard.ts +378 -0
  234. package/src/commands/init-workspace.ts +442 -0
  235. package/src/commands/init.ts +554 -0
  236. package/src/commands/lint-grammar-args.ts +79 -0
  237. package/src/commands/lint-grammar-driver.ts +83 -0
  238. package/src/commands/lint-grammar-report.ts +74 -0
  239. package/src/commands/lint-grammar.ts +57 -0
  240. package/src/commands/measure-memory-args.ts +123 -0
  241. package/src/commands/measure-memory-driver.ts +128 -0
  242. package/src/commands/measure-memory.ts +143 -0
  243. package/src/commands/model-docs-args.ts +69 -0
  244. package/src/commands/model-docs-driver.ts +68 -0
  245. package/src/commands/model-docs-report.ts +133 -0
  246. package/src/commands/model-docs.ts +48 -0
  247. package/src/commands/projects-args.ts +62 -0
  248. package/src/commands/projects.ts +76 -0
  249. package/src/commands/query-args.ts +61 -0
  250. package/src/commands/query.ts +58 -0
  251. package/src/commands/reflect-args.ts +75 -0
  252. package/src/commands/reflect-driver.ts +76 -0
  253. package/src/commands/reflect-report.ts +138 -0
  254. package/src/commands/reflect.ts +51 -0
  255. package/src/commands/save-args.ts +74 -0
  256. package/src/commands/save.ts +84 -0
  257. package/src/commands/validate-args.ts +76 -0
  258. package/src/commands/validate-driver.ts +84 -0
  259. package/src/commands/validate-report.ts +95 -0
  260. package/src/commands/validate.ts +58 -0
  261. package/src/commands/watch-args.ts +74 -0
  262. package/src/commands/watch.ts +185 -0
  263. package/src/index.ts +16 -0
  264. package/src/log-level.ts +49 -0
  265. package/src/package-name.ts +39 -0
  266. package/src/spawn-data-server.ts +247 -0
  267. package/src/testing/echo-server.ts +93 -0
@@ -0,0 +1,1902 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 CrossBreeze, EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the MIT License which is available in the project root.
6
+ *
7
+ * SPDX-License-Identifier: MIT
8
+ ********************************************************************************/
9
+ /*
10
+ * Template source for the `init` scaffolding command. Every entry is a file the
11
+ * scaffold emits, rendered from an `InitComposition` — one project tier plus N
12
+ * grammar tiers.
13
+ *
14
+ * PROJECT tokens, substituted once:
15
+ *
16
+ * - `__NAME__` — PascalCase project name, the Langium `projectName`. Drives the
17
+ * SHARED generated symbols (`<name>AstReflection`,
18
+ * `<name>GeneratedSharedModule`), one set per project.
19
+ * - `__PROJECT_ID__` — kebab project id, always from `__NAME__`. For
20
+ * project-level names a second grammar must not change: the package name and
21
+ * `bin` key, the DI module filename, and the data-server port command.
22
+ * - `__CONFIG_ROOT_CONST__` — SCREAMING_SNAKE project id, naming the
23
+ * `lsp.configurationRoot` constant a multi-grammar project binds.
24
+ *
25
+ * GRAMMAR tokens, substituted once per grammar:
26
+ *
27
+ * - `__GRAMMAR__` — PascalCase grammar name, the `grammar X` declaration.
28
+ * Drives the PER-LANGUAGE generated symbols (`<grammar>GeneratedModule`,
29
+ * `<grammar>LanguageMetaData`).
30
+ * - `__GRAMMAR_ID__` — kebab grammar id: the `.langium` filename.
31
+ * - `__LANGUAGE_ID__` — kebab routing key: the `langium-config` entry id and
32
+ * its TextMate output.
33
+ * - `__EXTENSION__` — the grammar's FIRST file extension, without the dot.
34
+ * - `__ENTRY_RULE__` / `__NODE_RULE__` — `<Grammar>Model` / `<Grammar>Node`.
35
+ *
36
+ * **The project / grammar / language split is the point, not ceremony.** All
37
+ * three ids hold the same value in the default single-grammar scaffold, so a
38
+ * template that reaches for the wrong one still renders correctly — and then
39
+ * breaks the day a second grammar arrives, renaming the package or colliding on
40
+ * a file extension. Each token above names the tier it belongs to so that day
41
+ * is a no-op.
42
+ *
43
+ * **Rule names are per-grammar and that is load-bearing.** One `langium-cli`
44
+ * run over N grammars emits ONE combined `ast.ts` sharing one reflection, so
45
+ * two grammars declaring `entry Model:` would put the same interface name in it
46
+ * twice. Hence `__ENTRY_RULE__` rather than a fixed `Model`. `__NODE_RULE__` is
47
+ * `<Grammar>Node` and deliberately not `<Grammar>Element`: the transfer-model
48
+ * generator emits `interface <projectName>Element` as the base type every rule
49
+ * extends, which a rule of that name would collide with.
50
+ *
51
+ * Kept as in-source strings (not on-disk assets) so no build-time copy step
52
+ * is needed and the substitution is unit-testable. The `.ts` / `.langium` / JSON
53
+ * templates are derived from the in-repo reference example so the generated project
54
+ * compiles against the real `@hydranium/*` API once `langium generate` has run.
55
+ *
56
+ * Two conventions the templates follow deliberately:
57
+ *
58
+ * - **File-purpose prose is a `//` run, never a leading `/** … *\/` block.** A
59
+ * license-header tool typically REPLACES the leading block comment, so prose in
60
+ * that position is silently deleted the first time the adopter runs theirs. A
61
+ * line-comment run survives, because such tools prepend rather than replace when
62
+ * the file does not open with `/*`.
63
+ * - **Wrapped at the target repo's column budget, not at a fixed one.** No single
64
+ * width is stable for an unknown repo — prettier's own default is 80, and 100,
65
+ * 120 and 140 all wrap differently — so "passes a formatter check as-is" is only
66
+ * ever true relative to a config. A workspace scaffold reads the surrounding
67
+ * repo's `printWidth`; a standalone one falls back to {@link DEFAULT_COLUMNS}.
68
+ * Every wrap goes through {@link importList} / {@link arrayLiteral} rather than
69
+ * being written out by hand, because a hand-wrapped line ignores the budget and
70
+ * is what the detection cannot fix. A very long `--name` can still push a line
71
+ * over; the adopter's own formatter settles it.
72
+ */
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
+ /**
84
+ * The version `init` pins every `@hydranium/*` dependency at: the scaffolding
85
+ * CLI's own.
86
+ *
87
+ * Read rather than written down. The changesets config declares the scope
88
+ * `fixed`, so the CLI's version IS the framework's, and a literal here would be
89
+ * a second source of truth that a scaffold can pin from before anyone gets
90
+ * round to updating it.
91
+ */
92
+ export function readFrameworkVersion() {
93
+ return readCliVersion();
94
+ }
95
+ /**
96
+ * The range a scaffolded project pins a framework package at.
97
+ *
98
+ * Caret rather than exact: the scope's fixed versioning means a caret set can
99
+ * only resolve to one version line, so the two-physical-copies hazard that
100
+ * argues for exact pins cannot arise, while exact pins would deny the adopter a
101
+ * patch release.
102
+ */
103
+ function frameworkPin(composition) {
104
+ return `^${composition.frameworkVersion}`;
105
+ }
106
+ /** SCREAMING_SNAKE a kebab id. */
107
+ function screamingSnake(projectId) {
108
+ return projectId.replace(/-/g, '_').toUpperCase();
109
+ }
110
+ /** Substitute the project-tier tokens. */
111
+ function project(text, composition) {
112
+ return text
113
+ .replace(/__NAME__/g, composition.name)
114
+ .replace(/__PROJECT_ID__/g, composition.projectId)
115
+ .replace(/__CONFIG_ROOT_CONST__/g, screamingSnake(composition.projectId));
116
+ }
117
+ /**
118
+ * Substitute the grammar-tier tokens for ONE grammar.
119
+ *
120
+ * `__GRAMMAR__` must not be folded into `__NAME__`, nor `__GRAMMAR_ID__` into
121
+ * `__PROJECT_ID__`: each pair holds the same value in the single-grammar
122
+ * scaffold and diverges the moment a second grammar arrives, which is precisely
123
+ * the case the separate tokens exist to keep correct.
124
+ */
125
+ function grammarTier(text, grammar) {
126
+ return text
127
+ .replace(/__GRAMMAR_ID__/g, grammar.grammarId)
128
+ .replace(/__GRAMMAR__/g, grammar.grammar)
129
+ .replace(/__LANGUAGE_ID__/g, grammar.languageId)
130
+ .replace(/__EXTENSION__/g, grammar.extensions[0])
131
+ .replace(/__ENTRY_RULE__/g, grammar.entryRule)
132
+ .replace(/__NODE_RULE__/g, grammar.nodeRule);
133
+ }
134
+ /** Render both tiers for a single-grammar file. */
135
+ function render(text, composition, grammar) {
136
+ return grammarTier(project(text, composition), grammar);
137
+ }
138
+ /** A comma-separated list of single-quoted TypeScript/JS string literals. */
139
+ function quotedList(values) {
140
+ return values.map(value => `'${value}'`).join(', ');
141
+ }
142
+ /**
143
+ * The column budget the emitted sources wrap at when nothing better is known.
144
+ *
145
+ * **Not "prettier's default"** — that is 80. 120 is a choice, and stating it as
146
+ * a default was wrong in a way that mattered: no single width is stable for an
147
+ * unknown repo, so emitted code only passes a formatter check relative to a
148
+ * config. A workspace scaffold detects the surrounding repo's `printWidth` and
149
+ * uses it (see {@link columnsFor}); a standalone one has no config to read and
150
+ * falls back here.
151
+ */
152
+ const DEFAULT_COLUMNS = 120;
153
+ /** The width THIS composition wraps at: the detected workspace one, else {@link DEFAULT_COLUMNS}. */
154
+ function columnsFor(composition) {
155
+ return composition.packaging.workspace?.printWidth ?? DEFAULT_COLUMNS;
156
+ }
157
+ /**
158
+ * 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
160
+ * generated-module import grows by one symbol per grammar.
161
+ */
162
+ function importList(symbols, from, columns, typeOnly = false) {
163
+ const keyword = typeOnly ? 'import type' : 'import';
164
+ const single = `${keyword} { ${symbols.join(', ')} } from '${from}';`;
165
+ return single.length <= columns ? single : `${keyword} {\n${symbols.map(symbol => ` ${symbol}`).join(',\n')}\n} from '${from}';`;
166
+ }
167
+ /** An array literal on one line while it fits at `indent`, else one element per line. */
168
+ function arrayLiteral(elements, indent, columns) {
169
+ const single = `[${elements.join(', ')}]`;
170
+ return `${indent}${single}`.length <= columns
171
+ ? single
172
+ : `[\n${elements.map(element => `${indent} ${element}`).join(',\n')}\n${indent}]`;
173
+ }
174
+ /**
175
+ * The FRAMEWORK packages each head adds, carrying no version: every
176
+ * `@hydranium/*` pin is derived at scaffold time from {@link frameworkPin}, so
177
+ * none of them can name a version its siblings were not published at.
178
+ */
179
+ const HEAD_FRAMEWORK_DEPENDENCIES = {
180
+ lsp: ['@hydranium/core', '@hydranium/langium', '@hydranium/protocol'],
181
+ data: ['@hydranium/data-server'],
182
+ glsp: ['@hydranium/glsp-server']
183
+ };
184
+ /**
185
+ * The third-party runtime dependencies each head adds, read off a working
186
+ * project rather than guessed.
187
+ *
188
+ * **This is why the head axis exists.** Keyed per head rather than emitted as
189
+ * one fixed list, because otherwise an adopter who adds a head hand-maintains
190
+ * its dependencies too, and the two drift apart until a lint rule catches the
191
+ * undeclared packages.
192
+ *
193
+ * **These literals are the only hand-maintained versions the scaffold emits, and
194
+ * a bump of the framework's own pinned chain does not touch them.** A scaffolded
195
+ * project pinning a different `langium` than the framework was built against
196
+ * resolves a second physical copy, which is the identity failure the repo's
197
+ * `overrides` block exists to prevent — so a repo gate holds each entry here
198
+ * against the manifest that declares it.
199
+ *
200
+ * `langium` is required by `lsp` even though hand-written code goes through the
201
+ * `@hydranium/langium` chokepoint, because `langium-cli` emits direct imports
202
+ * into the generated files and regenerates them on every build.
203
+ */
204
+ const HEAD_THIRD_PARTY_DEPENDENCIES = {
205
+ lsp: {
206
+ langium: '4.3.1',
207
+ 'vscode-languageserver': '~10.0.1'
208
+ },
209
+ data: {},
210
+ glsp: {
211
+ '@eclipse-glsp/graph': '2.7.0',
212
+ '@eclipse-glsp/server': '2.7.0',
213
+ // `^6.1.3` and NOT `^6.0.0`: `@eclipse-glsp/server@2.7.0` requires it, and
214
+ // `^6.0.0` resolved only because the hoisted copy happened to satisfy it.
215
+ inversify: '^6.1.3',
216
+ 'reflect-metadata': '~0.2.2'
217
+ }
218
+ };
219
+ /** The dependency block for a head set, merged and sorted as npm writes it. */
220
+ function dependencyBlock(composition, indent) {
221
+ const merged = {};
222
+ for (const head of composition.heads) {
223
+ Object.assign(merged, HEAD_THIRD_PARTY_DEPENDENCIES[head]);
224
+ for (const framework of HEAD_FRAMEWORK_DEPENDENCIES[head]) {
225
+ merged[framework] = frameworkPin(composition);
226
+ }
227
+ }
228
+ return Object.keys(merged)
229
+ .sort()
230
+ .map(name => `${indent}"${name}": "${merged[name]}"`)
231
+ .join(',\n');
232
+ }
233
+ // JSON at two-space indent, not the three the `.ts` templates use: that is the
234
+ // npm convention, and `npm install` rewrites `package.json` at two spaces
235
+ // regardless — so three would not survive the adopter's first install.
236
+ //
237
+ // The KEY ORDER is `prettier-plugin-packagejson`'s canonical one, `private`
238
+ // before `license` included. Any other order is rewritten by the scaffolded
239
+ // project's own first `format` run — a diff on a file the adopter never touched
240
+ // — and it makes a byte-compare against a formatted copy of this emission
241
+ // unsatisfiable, since both tools own the file.
242
+ //
243
+ // `private` and `license` are ONE decision, not two independent keys: a manifest
244
+ // declaring UNLICENSED grants no rights, so leaving it publishable to a public
245
+ // registry asserts the opposite of what it grants. The scaffold cannot choose a
246
+ // licence for a stranger's project, so it withholds publication instead, and
247
+ // `--public` is the opt-out once the project has chosen one.
248
+ //
249
+ // `files` is what keeps the first `npm publish` from succeeding with an unusable
250
+ // tarball. With no `files` and no `.npmignore`, npm falls back to `.gitignore`,
251
+ // which this scaffold also emits with `lib/` in it: npm force-includes `main`
252
+ // and OMITS the `bin` target beside it, so the package installs and the binary
253
+ // is missing. `syntaxes` is listed because the TextMate grammar is generated
254
+ // from this package's own grammar and can reach a consumer from nowhere else; a
255
+ // listed path that does not exist yet is inert.
256
+ //
257
+ // `repository` is deliberately NOT emitted while `author` is emitted empty. An
258
+ // empty `author` is the form `npm init` itself writes and asserts nothing, but
259
+ // `repository` is CONSUMED — the registry page, `npm repo` and publish
260
+ // provenance all follow it — so an empty or invented value sends a reader
261
+ // somewhere wrong rather than nowhere. A scaffold has no way to learn the real
262
+ // one: a standalone target has no surrounding repository at all.
263
+ const PACKAGE_JSON = `{
264
+ "name": "__PACKAGE_NAME__",
265
+ "version": "0.0.0",
266
+ __PRIVATE__ "description": "__NAME__ language server, built with the Hydranium framework.",
267
+ "keywords": [
268
+ "hydranium",
269
+ "langium",
270
+ "language-server",
271
+ "__PROJECT_ID__"
272
+ ],
273
+ "license": "UNLICENSED",
274
+ "author": "",
275
+ "type": "module",
276
+ "main": "lib/index.js",
277
+ "types": "lib/index.d.ts",
278
+ "bin": {
279
+ __BIN__ },
280
+ "files": [
281
+ "lib",
282
+ "src",
283
+ "syntaxes"
284
+ ],
285
+ "scripts": {
286
+ "build": "npm run generate && tsc",
287
+ "clean": "rimraf lib syntaxes src/language-server/generated src/language-server/generated-transfer tsconfig.tsbuildinfo",
288
+ "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\\"",
290
+ "langium:generate": "langium generate",
291
+ "langium:watch": "langium generate --watch",
292
+ __LINT__ "start": "node lib/main.js --stdio",
293
+ "test": "npm run typecheck:test && vitest run",
294
+ "typecheck:test": "tsc --noEmit -p tsconfig.test.json",
295
+ "watch": "tsc --watch"
296
+ },
297
+ "dependencies": {
298
+ __DEPENDENCIES__
299
+ },
300
+ "devDependencies": {
301
+ "@hydranium/cli": "__FRAMEWORK_PIN__",
302
+ "@types/node": "^22.0.0",
303
+ "langium-cli": "4.3.0",
304
+ "rimraf": "^5.0.0",
305
+ "typescript": "^5.8.0",
306
+ "vitest": "^4.0.0"
307
+ },
308
+ "engines": {
309
+ "node": ">=22.13"
310
+ }
311
+ }
312
+ `;
313
+ /**
314
+ * How the scaffold names `npm run` when talking about itself.
315
+ *
316
+ * A workspace member's scripts are routinely invoked from the repo root, where
317
+ * a bare `npm run` reaches the ROOT manifest and fails — so the regen hint a
318
+ * generated file prints has to name the package, or it sends every reader who
319
+ * follows it to the wrong place.
320
+ */
321
+ function npmRun(composition) {
322
+ const workspace = composition.packaging.workspace;
323
+ return workspace === undefined ? 'npm run' : `npm --prefix ${workspace.targetPath} run`;
324
+ }
325
+ /**
326
+ * The `bin` targets, one per executable entry the head set emits.
327
+ *
328
+ * The data entry earns a key of its own rather than an argument on the first
329
+ * one: the two entries put DIFFERENT protocols on stdio, so a process can host
330
+ * only one of them, and `--server` takes a command line rather than a package
331
+ * name. Without the second key a scaffolded project has no command line that
332
+ * reaches its own data head at all.
333
+ *
334
+ * Alphabetical, because `prettier-plugin-packagejson` sorts `bin` and any other
335
+ * order is rewritten by the scaffolded project's first `format` run.
336
+ */
337
+ function binBlock(composition) {
338
+ const entries = [[composition.projectId, 'lib/main.js']];
339
+ if (composition.heads.includes('data')) {
340
+ entries.push([`${composition.projectId}-data-server`, 'lib/data-server-main.js']);
341
+ }
342
+ return entries.map(([name, target]) => ` "${name}": "${target}"`).join(',\n') + '\n';
343
+ }
344
+ /** `package.json` with the name, the private flag and the dependency block all derived. */
345
+ function packageJson(composition) {
346
+ const { scope } = composition.packaging;
347
+ const packageName = scope === undefined ? composition.projectId : `${scope}/${composition.projectId}`;
348
+ // Emitted only where a root eslint config says the repo lints. The absence is
349
+ // the dangerous direction: a task runner runs a script only where one is
350
+ // declared, so a package with no `lint` is SKIPPED rather than reported, and
351
+ // 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';
353
+ return project(PACKAGE_JSON, composition)
354
+ .replace('__PACKAGE_NAME__', packageName)
355
+ .replace('__BIN__', binBlock(composition))
356
+ .replace('__LINT__', lint)
357
+ .replace('__PRIVATE__', composition.packaging.private ? ' "private": true,\n' : '')
358
+ .replace('__NPM_RUN__', npmRun(composition))
359
+ .replace('__FRAMEWORK_PIN__', frameworkPin(composition))
360
+ .replace('__DEPENDENCIES__', dependencyBlock(composition, ' '));
361
+ }
362
+ /**
363
+ * `projectName` is the PROJECT name, not any grammar's: one `langium-cli` run
364
+ * over N grammars emits ONE `<projectName>AstReflection` covering all of them.
365
+ * A second grammar is a new entry in `languages` here, never a second config.
366
+ * `textMate.out` gives a VS Code extension its syntax highlighting; drop the
367
+ * block if you are not shipping one.
368
+ */
369
+ function langiumConfig(composition) {
370
+ const languages = composition.grammars
371
+ .map(grammar => [
372
+ ' {',
373
+ ` "id": "${grammar.languageId}",`,
374
+ ` "grammar": "src/grammar/${grammar.grammarId}.langium",`,
375
+ ` "fileExtensions": [${grammar.extensions.map(extension => `".${extension}"`).join(', ')}],`,
376
+ ' "textMate": {',
377
+ ` "out": "syntaxes/${grammar.languageId}.tmLanguage.json"`,
378
+ ' }',
379
+ ' }'
380
+ ].join('\n'))
381
+ .join(',\n');
382
+ return `{
383
+ "projectName": "${composition.name}",
384
+ "languages": [
385
+ ${languages}
386
+ ],
387
+ "out": "src/language-server/generated"
388
+ }
389
+ `;
390
+ }
391
+ /**
392
+ * The compiler options the scaffold needs, as ordered data rather than as a
393
+ * string literal.
394
+ *
395
+ * Structured because a workspace member emits a SUBSET of them — whatever its
396
+ * base config does not already supply — and a second hand-maintained literal
397
+ * for that case would drift from this one silently, which is the failure mode
398
+ * a tsconfig is worst at reporting.
399
+ */
400
+ const TSCONFIG_COMPILER_OPTIONS = [
401
+ ['target', 'ES2022'],
402
+ ['lib', ['ES2022']],
403
+ ['module', 'NodeNext'],
404
+ ['moduleResolution', 'NodeNext'],
405
+ ['rootDir', 'src'],
406
+ ['outDir', 'lib'],
407
+ ['strict', true],
408
+ ['esModuleInterop', true],
409
+ ['skipLibCheck', true],
410
+ ['declaration', true],
411
+ ['experimentalDecorators', true],
412
+ ['emitDecoratorMetadata', true],
413
+ ['forceConsistentCasingInFileNames', true],
414
+ ['types', ['node']]
415
+ ];
416
+ /**
417
+ * The options that describe where THIS package's own files live. Never pruned,
418
+ * and emitted first in the workspace case: a base config that supplied them
419
+ * would be pointing every member at one directory, so a match there means the
420
+ * base is wrong rather than that the member is redundant.
421
+ */
422
+ 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);
426
+ }
427
+ /**
428
+ * `tsconfig.json`, extending the workspace's base config when there is one.
429
+ *
430
+ * Options are dropped only when the base supplies the SAME value, never merely
431
+ * the same key: inheriting `"module": "CommonJS"` where the scaffold needs
432
+ * `NodeNext` would break every emitted import, so a differing base value is a
433
+ * reason to keep the override rather than to trust the parent.
434
+ */
435
+ function tsconfigJson(composition) {
436
+ const workspace = composition.packaging.workspace;
437
+ const base = workspace?.baseTsconfig === undefined ? undefined : (workspace.baseCompilerOptions ?? {});
438
+ const options = base === undefined
439
+ ? TSCONFIG_COMPILER_OPTIONS
440
+ : [
441
+ ...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))
443
+ ];
444
+ const extendsLine = workspace?.baseTsconfig === undefined ? '' : ` "extends": "${workspace.baseTsconfig}",\n`;
445
+ 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`;
447
+ }
448
+ // `isolatedModules` is what makes this check agree with the transform that
449
+ // actually runs the tests. `npm test` is `typecheck:test && vitest run`, and
450
+ // vitest compiles through esbuild — a per-file transform with no type
451
+ // information, which cannot tell a re-exported TYPE from a re-exported value.
452
+ // Without the flag `tsc` accepts `export { SomeType } from './x'` that esbuild
453
+ // then emits as a real import of a symbol that does not exist at runtime, so
454
+ // the typecheck passes and the suite fails with a confusing missing-export.
455
+ const TSCONFIG_TEST = `{
456
+ "extends": "./tsconfig.json",
457
+ "compilerOptions": {
458
+ "rootDir": ".",
459
+ "noEmit": true,
460
+ "isolatedModules": true,
461
+ "types": ["node"]
462
+ },
463
+ "include": ["src", "test"]
464
+ }
465
+ `;
466
+ const VITEST_CONFIG = `import { defineConfig } from 'vitest/config';
467
+
468
+ // Vitest transforms TypeScript itself and resolves \`.js\` specifiers to their
469
+ // \`.ts\` source, so tests import from \`../src/\` with the same specifiers the
470
+ // compiled output uses. \`include\` is scoped to \`test/\` so \`lib/\` is never
471
+ // scanned; \`npm test\` runs \`typecheck:test\` first, because the esbuild
472
+ // transform strips types without checking them.
473
+
474
+ export default defineConfig({
475
+ test: {
476
+ environment: 'node',
477
+ include: ['test/**/*.{test,spec}.ts']
478
+ }
479
+ });
480
+ `;
481
+ const GITIGNORE = `node_modules/
482
+ lib/
483
+ *.tsbuildinfo
484
+
485
+ # Langium-generated TextMate grammar (rewritten by every \`langium generate\`).
486
+ syntaxes/
487
+ `;
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
+ /** The terminals every grammar needs, shared verbatim by the fragment and the lone-grammar case. */
509
+ const TERMINALS = `hidden terminal WS: /\\s+/;
510
+ terminal ID: /[_a-zA-Z][\\w_]*/;
511
+ hidden terminal SL_COMMENT: /\\/\\/[^\\n\\r]*/;
512
+ hidden terminal ML_COMMENT: /\\/\\*[\\s\\S]*?\\*\\//;
513
+ `;
514
+ /**
515
+ * The shared lexical fragment, emitted only for a multi-grammar project.
516
+ *
517
+ * The alternative — every grammar declaring its own `WS` / `ID` — lets the token
518
+ * sets drift apart.
519
+ */
520
+ const COMMON_GRAMMAR = `// Shared lexical basis for this project's grammars. Imported, never registered:
521
+ // it has no entry rule and no \`langium-config.json\` entry, so it is a fragment
522
+ // rather than a language.
523
+ //
524
+ // Keeping the tokens here rather than in one of the languages means a grammar
525
+ // imports \`common\` for its TOKENS and another language only when it needs that
526
+ // language's TYPES — separate reasons that are worth keeping separate.
527
+
528
+ ${TERMINALS}`;
529
+ /** One starter grammar. `shared` decides whether it imports the fragment or declares its own terminals. */
530
+ function grammarFile(shared) {
531
+ const lexis = shared ? "import './common'\n" : `\n${TERMINALS}`;
532
+ return `grammar __GRAMMAR__
533
+ ${shared ? '\n' + lexis : ''}
534
+ // A minimal starter grammar — replace with your own language. \`__NODE_RULE__\` is
535
+ // a named, cross-referenceable node, which is enough for \`hydranium-cli reflect\`,
536
+ // \`lint-grammar\`, and \`validate\` to work out of the box.
537
+ //
538
+ // This name is the GRAMMAR's, not the project's: langium-cli emits
539
+ // \`__GRAMMAR__GeneratedModule\` from it, while \`projectName\` in
540
+ // langium-config.json emits the shared \`__NAME__GeneratedSharedModule\` and
541
+ // \`__NAME__AstReflection\`. They match by default and diverge as soon as you add
542
+ // a second grammar — that grammar gets its own name here, and the project name
543
+ // stays the umbrella.
544
+ //
545
+ // The rule names carry the grammar's prefix (\`__ENTRY_RULE__\`, \`__NODE_RULE__\`)
546
+ // because one langium-cli run over N grammars emits ONE combined \`ast.ts\`: two
547
+ // grammars both declaring \`entry Model:\` would put that interface in it twice.
548
+
549
+ entry __ENTRY_RULE__:
550
+ (nodes+=__NODE_RULE__)*;
551
+
552
+ __NODE_RULE__:
553
+ 'node' name=ID ('->' target=[__NODE_RULE__:ID])?;
554
+ ${shared ? '' : lexis}`;
555
+ }
556
+ /**
557
+ * DI bootstrap. The single-grammar form composes one language; the multi-grammar
558
+ * form adds each further grammar through `additionalLanguages` (one shared tier,
559
+ * so `AstReflection`, `IndexManager` and `DocumentBuilder` are common and
560
+ * cross-grammar references resolve through one global index) and binds
561
+ * `lsp.configurationRoot` explicitly, which the framework warns about otherwise
562
+ * because its default is registration order rather than a decision.
563
+ */
564
+ function moduleFile(composition) {
565
+ const columns = columnsFor(composition);
566
+ const [primary, ...additional] = composition.grammars;
567
+ const multi = additional.length > 0;
568
+ const generatedImports = [
569
+ ...composition.grammars.map(grammar => `${grammar.grammar}GeneratedModule`),
570
+ `${composition.name}GeneratedSharedModule`
571
+ ];
572
+ const configurationRoot = multi
573
+ ? `
574
+ /** LSP settings section every language reads its configuration from. */
575
+ export const __CONFIG_ROOT_CONST___CONFIGURATION_ROOT = '__PROJECT_ID__';
576
+ `
577
+ : '';
578
+ const sharedModule = multi
579
+ ? `const __NAME__SharedModule: Module<
580
+ __NAME__SharedServices,
581
+ PartialLangiumSharedServices & { lsp: { configurationRoot: string } }
582
+ > = {
583
+ lsp: {
584
+ // Several languages are registered, so the framework default ("first
585
+ // registered id") would be registration order rather than a choice.
586
+ configurationRoot: () => __CONFIG_ROOT_CONST___CONFIGURATION_ROOT
587
+ }
588
+ };`
589
+ : `const __NAME__SharedModule: Module<__NAME__SharedServices, PartialLangiumSharedServices> = {};`;
590
+ const serializerImports = composition.grammars
591
+ .map(grammar => `import { ${grammar.grammar}Serializer } from './${grammar.grammarId}-serializer.js';`)
592
+ .join('\n');
593
+ // Each language gets its own adopter module because `Serializer` is a
594
+ // per-language slot. `additionalLanguages` defaults its `adopter` to the
595
+ // primary's, which would give every further grammar the FIRST grammar's
596
+ // serializer — and that one throws on a `$type` it has no emitter for.
597
+ const languageModules = composition.grammars
598
+ .map(grammar => `const ${grammar.grammar}LanguageModule: Module<__NAME__Services, PartialLangiumServices & DeepPartial<ServerAddedServices>> = {
599
+ serializer: {
600
+ Serializer: services => new ${grammar.grammar}Serializer(services)
601
+ }
602
+ };`)
603
+ .join('\n\n');
604
+ const additionalLanguages = multi
605
+ ? `,\n additionalLanguages: ${arrayLiteral(additional.map(grammar => `{ generated: ${grammar.grammar}GeneratedModule, adopter: () => ${grammar.grammar}LanguageModule }`), ' ', columns)}`
606
+ : '';
607
+ const destructure = multi ? '{ shared, languages }' : '{ shared, language }';
608
+ const returnType = composition.grammars.map(grammar => ` ${grammar.grammar}: __NAME__Services;`).join('\n');
609
+ const returnValue = multi
610
+ ? composition.grammars.map((grammar, index) => `${grammar.grammar}: languages[${index}]`).join(', ')
611
+ : `${primary.grammar}: language`;
612
+ const text = `// DI bootstrap for __NAME__. Composes the framework defaults with adopter
613
+ // overrides via Langium's \`inject()\` (through the framework's
614
+ // \`createIntegrationServices\`). Each adopter module binds ONE slot — its
615
+ // language's \`Serializer\`, which the framework cannot default because a
616
+ // concrete syntax is grammar knowledge. Everything else (scope, naming, project
617
+ // management, build pipeline) boots on the framework defaults; scope
618
+ // computation, validation checks and AST extensions go in these same modules.
619
+ //
620
+ // Note which generated symbol comes from which name: the SHARED module is
621
+ // \`__NAME__GeneratedSharedModule\` (from \`projectName\`) and there is one of it,
622
+ // while the per-language \`<Grammar>GeneratedModule\` (from each \`grammar\`
623
+ // declaration) has one per grammar.${multi
624
+ ? `
625
+ //
626
+ // Every grammar here comes from ONE \`langium-cli\` run — \`AstReflection\` is a
627
+ // single shared slot, so two independently generated language packages would
628
+ // leave only the last one bound.`
629
+ : `
630
+ //
631
+ // A second grammar goes in the \`additionalLanguages\` option of
632
+ // \`createIntegrationServices\`, generated from this same \`langium-config.json\`.`}
633
+
634
+ ${importList(['createLspServerLanguageModule', 'createLspServerSharedModule', 'type LspServerAddedServices', 'type LspServerAddedSharedServices'], '@hydranium/core/lsp', columns)}
635
+ import { createIntegrationServices, type ServerAddedServices, type ServerModuleContext, type ServerSharedServices } from '@hydranium/core';
636
+ import { type DeepPartial, EmptyFileSystem, type Module } from '@hydranium/langium';
637
+ import { type LangiumServices, type PartialLangiumServices, type PartialLangiumSharedServices } from '@hydranium/langium/lsp';
638
+ ${importList(generatedImports, './generated/module.js', columns)}
639
+ ${serializerImports}
640
+
641
+ export type __NAME__SharedServices = ServerSharedServices & LspServerAddedSharedServices;
642
+ export type __NAME__Services = LangiumServices &
643
+ ServerAddedServices &
644
+ LspServerAddedServices & {
645
+ shared: __NAME__SharedServices;
646
+ };
647
+ ${configurationRoot}
648
+ ${sharedModule}
649
+
650
+ ${languageModules}
651
+
652
+ /** Compose the Langium DI tree for __NAME__ — returns the shared + language services. */
653
+ export function create__NAME__Services(context: Partial<ServerModuleContext> = EmptyFileSystem): {
654
+ shared: __NAME__SharedServices;
655
+ ${returnType}
656
+ } {
657
+ const fullContext: ServerModuleContext = { ...EmptyFileSystem, ...context };
658
+ const ${destructure} = createIntegrationServices<ServerModuleContext, __NAME__SharedServices, __NAME__Services>({
659
+ context: fullContext,
660
+ sharedModules: {
661
+ generated: __NAME__GeneratedSharedModule,
662
+ adopter: __NAME__SharedModule,
663
+ extra: [createLspServerSharedModule(fullContext)]
664
+ },
665
+ languageModules: {
666
+ generated: ${primary.grammar}GeneratedModule,
667
+ adopter: () => ${primary.grammar}LanguageModule,
668
+ extra: [createLspServerLanguageModule(fullContext)]
669
+ }${additionalLanguages}
670
+ });
671
+ return { shared, ${returnValue} };
672
+ }
673
+ `;
674
+ return project(text, composition);
675
+ }
676
+ const SERVICES = `// Zero-arg service factory for the headless \`hydranium-cli\` tooling: the
677
+ // \`reflect\` / \`lint-grammar\` / \`validate\` subcommands import this via
678
+ // \`--services ./lib/services.js\`. The head wires its own filesystem here.
679
+ //
680
+ // The contract is language-count-agnostic — a second grammar needs no edit here.
681
+
682
+ import { NodeFileSystem } from '@hydranium/core/node';
683
+ import { create__NAME__Services } from './language-server/__PROJECT_ID__-module.js';
684
+
685
+ export function createServices(): ReturnType<typeof create__NAME__Services> {
686
+ return create__NAME__Services({ ...NodeFileSystem });
687
+ }
688
+ `;
689
+ const AST = `// The language's AST entry point: a re-export of what \`langium generate\`
690
+ // emits, plus anywhere you augment those types.
691
+ //
692
+ // Import the AST from HERE rather than from \`./generated/ast.js\`, so any
693
+ // augmentation below travels with every import. One \`langium-cli\` run over N
694
+ // grammars emits ONE combined AST module sharing one reflection, so a further
695
+ // grammar needs no change here.
696
+ //
697
+ // \`generate:transfer-model\` reads this file as its \`--augmentation-file\`: the
698
+ // generated wire types are derived from the AST *as augmented*, not from the
699
+ // raw generated module. Augment a type here and the transfer model follows.
700
+ //
701
+ // A \`@derived\` property is computed at build time rather than parsed, so it is
702
+ // declared here and populated by an AST-extension contribution:
703
+ //
704
+ // declare module './generated/ast.js' {
705
+ // interface __ENTRY_RULE__ {
706
+ // /** @derived Populated by an \`ast.extensions.computedProperties\` contribution. */
707
+ // readonly _summary?: string;
708
+ // }
709
+ // }
710
+
711
+ export * from './generated/ast.js';
712
+ `;
713
+ /**
714
+ * The starter serializer — the concrete-syntax emitter for ONE grammar.
715
+ *
716
+ * Emitted per grammar, not per project, because `services.serializer.Serializer`
717
+ * is a per-language slot and a serializer is grammar-shaped by definition. That
718
+ * is also why the framework refuses to default it: its `UnboundSerializer`
719
+ * throws, naming the binding to add, rather than guessing a syntax.
720
+ */
721
+ const SERIALIZER = `// Concrete-syntax emitter for __GRAMMAR__ — the parser's inverse, turning a
722
+ // model back into text this grammar accepts. Derived from the starter grammar
723
+ // exactly as \`generated/ast.ts\` is, so replacing the grammar replaces this too.
724
+ //
725
+ // Without it every structured write fails: the framework's default binding at
726
+ // \`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.
730
+ //
731
+ // \`AbstractSerializer\`'s generic property walk lays out FORMAT-structured
732
+ // output — its YAML and JSON subclasses are what it exists for — and cannot
733
+ // produce a keyword-delimited syntax like \`node a -> b\`. So \`serializeNode\` is
734
+ // a per-\`$type\` emitter here and the two array hooks are unreachable.
735
+
736
+ import { AbstractSerializer } from '@hydranium/core';
737
+ import type { AstNode } from '@hydranium/langium';
738
+ import { type __ENTRY_RULE__, type __NODE_RULE__, is__ENTRY_RULE__, is__NODE_RULE__ } from './ast.js';
739
+
740
+ export class __GRAMMAR__Serializer extends AbstractSerializer<__ENTRY_RULE__> {
741
+ protected override serializeNode(node: AstNode | Record<string, unknown>): string {
742
+ if (is__ENTRY_RULE__(node)) {
743
+ return node.nodes.map(child => this.emitNode(child)).join('\\n');
744
+ }
745
+ if (is__NODE_RULE__(node)) {
746
+ return this.emitNode(node);
747
+ }
748
+ // Defensive: a rule added to the grammar with no emitter added here.
749
+ throw new Error(\`__GRAMMAR__Serializer: no emitter for $type \${(node as AstNode).$type}\`);
750
+ }
751
+
752
+ /** Unreachable — this grammar's one list is emitted by its \`__ENTRY_RULE__\` parent. */
753
+ protected override serializeArray(): string {
754
+ throw new Error('__GRAMMAR__Serializer: arrays are emitted by the per-$type parent, not the generic dispatch.');
755
+ }
756
+
757
+ /** Unreachable — same reasoning as {@link serializeArray}. */
758
+ protected override serializeReferenceArray(): string {
759
+ throw new Error('__GRAMMAR__Serializer: reference arrays are emitted by the per-$type parent, not the generic dispatch.');
760
+ }
761
+
762
+ /**
763
+ * \`serializeReferenceText\` rather than \`node.target?.$refText\`: it is the one
764
+ * read that spans BOTH input shapes. A transfer model reaching
765
+ * \`serializeTransfer\` carries \`target\` as a plain string rather than a
766
+ * \`Reference\`, and a serializer that reaches for \`$refText\` directly emits
767
+ * the AST correctly and drops every reference on the transfer path.
768
+ */
769
+ private emitNode(node: __NODE_RULE__): string {
770
+ const target = this.serializeReferenceText(node.target);
771
+ return target === undefined ? \`node \${node.name}\` : \`node \${node.name} -> \${target}\`;
772
+ }
773
+ }
774
+ `;
775
+ /**
776
+ * The exported constant names for the head set, in emission order. Both are
777
+ * keyed by the PROJECT rather than by a language: one server of each kind per
778
+ * process serves every registered grammar, so a language-derived name would tie
779
+ * a project-level endpoint to whichever grammar was scaffolded first.
780
+ */
781
+ function portCommandNames(composition) {
782
+ const upper = screamingSnake(composition.projectId);
783
+ return [
784
+ ...(composition.heads.includes('data') ? [`${upper}_DATA_SERVER_PORT_COMMAND`] : []),
785
+ ...(composition.heads.includes('glsp') ? [`${upper}_GLSP_PORT_COMMAND`] : [])
786
+ ];
787
+ }
788
+ /**
789
+ * The socket heads' discovery commands, as an importable module.
790
+ *
791
+ * Deliberately NOT declared in `main.ts`. A host shell has to name the same
792
+ * string to reach the head, and `main.ts` opens a connection at module scope,
793
+ * so importing a constant from it would start a server. A host that retypes the
794
+ * literal instead gets no error when it drifts: the framework's port poll
795
+ * defaults to `findPortAttempts = -1`, so a wrong command retries forever
796
+ * rather than failing.
797
+ */
798
+ function headPortsFile(composition) {
799
+ const upper = screamingSnake(composition.projectId);
800
+ const data = composition.heads.includes('data');
801
+ const glsp = composition.heads.includes('glsp');
802
+ const blocks = [
803
+ ...(data
804
+ ? [
805
+ `/** LSP request the host queries to discover the data-server socket port. */\nexport const ${upper}_DATA_SERVER_PORT_COMMAND = '${composition.projectId}/data-server/port';`
806
+ ]
807
+ : []),
808
+ ...(glsp
809
+ ? [
810
+ `/** LSP request the host queries to discover the GLSP socket port. */\nexport const ${upper}_GLSP_PORT_COMMAND = '${composition.projectId}/glsp/port';`
811
+ ]
812
+ : [])
813
+ ];
814
+ return `// The LSP requests a host queries to discover this server's socket head${blocks.length > 1 ? 's' : ''}.
815
+ //
816
+ // Both are keyed by the PROJECT, not the language: one server of each kind per
817
+ // process serves every registered grammar, so a language-derived name would tie
818
+ // a project-level endpoint to whichever grammar was scaffolded first.
819
+ //
820
+ // They live here rather than in \`main.ts\` because a host shell has to name the
821
+ // same string to reach the head, and \`main.ts\` is an executable entry — nothing
822
+ // can import from it. A shell that retypes the literal gets no error when it
823
+ // drifts: the framework's port poll retries indefinitely by default. Import
824
+ // these instead, and assert any host-side copy against them.
825
+
826
+ ${blocks.join('\n\n')}
827
+ `;
828
+ }
829
+ const INDEX = `// Public surface of the __NAME__ language server: the DI factory, the generated
830
+ // AST, and the headless \`createServices\` entry.
831
+ //
832
+ // The entry points under \`src/\` are deliberately NOT re-exported. Each opens a
833
+ // transport at module scope, so importing one starts a server as a side effect
834
+ // — which is why they are \`bin\` targets and this file is \`main\`.
835
+
836
+ export * from './language-server/__PROJECT_ID__-module.js';
837
+ export * from './language-server/ast.js';
838
+ export { createServices } from './services.js';
839
+ `;
840
+ /**
841
+ * The public surface, plus the head-port commands when there is a socket head.
842
+ *
843
+ * The commands belong here rather than only in `head-ports.ts` so a host shell
844
+ * reaches them from the package root, the same way it reaches `createServices`.
845
+ */
846
+ function indexFile(composition) {
847
+ const base = project(INDEX, composition);
848
+ return portCommandNames(composition).length ? `${base}export * from './head-ports.js';\n` : base;
849
+ }
850
+ /**
851
+ * The `#!` line every emitted `bin` target carries.
852
+ *
853
+ * Required in the SOURCE, not added at publish time: npm's install-time `fixBin`
854
+ * sets the exec bit on a linked `bin` target but writes no interpreter line, so
855
+ * an entry whose first line is the SPDX header is handed to `/bin/sh`, which
856
+ * tries to execute the licence comment. `tsc` carries a leading shebang through
857
+ * to `lib/` unchanged, so emitting it here is what makes the linked binary
858
+ * runnable — and it must stay the FIRST line, ahead of the header a licence
859
+ * sweep adds below it.
860
+ */
861
+ const SHEBANG = '#!/usr/bin/env node\n';
862
+ /**
863
+ * The launcher, assembled from the head set.
864
+ *
865
+ * Every head runs in the same process over ONE `createXxxServices` call, which
866
+ * is what "three heads over one workspace" means: they share the model store,
867
+ * the index and the build pipeline rather than each parsing their own copy.
868
+ *
869
+ * The data head's root type is the union of every grammar's transfer root, which
870
+ * is what one data server serving N grammars means.
871
+ */
872
+ function mainFile(composition) {
873
+ const columns = columnsFor(composition);
874
+ const roots = [...composition.grammars.map(grammar => grammar.entryRule)].sort();
875
+ const upper = screamingSnake(composition.projectId);
876
+ const data = composition.heads.includes('data');
877
+ const glsp = composition.heads.includes('glsp');
878
+ const diagrams = composition.grammars.filter(grammar => grammar.diagram);
879
+ const plural = roots.length > 1 ? 's' : '';
880
+ const headNames = composition.heads.map(head => head === 'lsp' ? 'the Langium LSP head' : head === 'data' ? 'a socket data-server (model server) head' : 'a GLSP head');
881
+ // Comma-joined but for the last, which takes `and`: a bare `, ` join reads as
882
+ // a truncated list, and the emitted comment is the first thing an adopter
883
+ // reads in the file they run.
884
+ const headSummary = headNames.length === 1 ? headNames[0] : `${headNames.slice(0, -1).join(', ')} and ${headNames[headNames.length - 1]}`;
885
+ // `reflect-metadata` must be imported before anything that reads a
886
+ // decorator's emitted metadata, which inversify does at module scope.
887
+ const reflectImport = glsp ? "import 'reflect-metadata';\n" : '';
888
+ const coreNodeSymbols = ['NodeFileSystem', ...(data ? ['publishPortOnLspConnection', 'startSocketServer'] : [])];
889
+ const glspImports = glsp
890
+ ? `import { ServerModule } from '@eclipse-glsp/server/node.js';
891
+ import { GlspClientLogger, HydraniumGlspAppModule } from '@hydranium/glsp-server';
892
+ import { startGlspServer } from '@hydranium/glsp-server/node';
893
+ `
894
+ : '';
895
+ const diagramImports = diagrams
896
+ .map(grammar => `import { ${grammar.grammar}DiagramModule } from './glsp/${grammar.grammarId}/diagram-module.js';\n`)
897
+ .join('');
898
+ const transferImports = data
899
+ ? `// The TRANSFER root${plural}, not the AST one${plural}. The data head serialises to the
900
+ // persisted shape, where \`Reference<T>\` is a plain \`string\`; the AST's is a
901
+ // Langium reference object with \`.ref\` / \`.$refText\`. Both satisfy
902
+ // \`TransferElement\` structurally, so naming the AST type here compiles fine and
903
+ // silently tells every typed client that a reference is a resolvable object
904
+ // rather than a name.
905
+ ${importList(roots, './language-server/generated-transfer/transfer-model.js', columns, true)}
906
+ `
907
+ : '';
908
+ // The commands live in `head-ports.ts`, not here: a host shell has to name
909
+ // the same string to reach the head, and this file is an executable entry, so
910
+ // nothing can import from it.
911
+ const portCommands = portCommandNames(composition).length
912
+ ? importList(portCommandNames(composition), './head-ports.js', columns) + '\n'
913
+ : '';
914
+ const dataBlock = data
915
+ ? `
916
+ // Data-server head alongside LSP: binds an ephemeral port, published over the LSP
917
+ // connection for the host to discover. Each accepted client gets its own DataServer.
918
+ //
919
+ // Neither the bind nor the publish may be swallowed: either failure leaves the LSP
920
+ // head serving text edits while every data client waits on a port command that was
921
+ // never registered, and the launcher reports a bind failure only if given a logger.
922
+ const dataServer = startSocketServer({ port: 0, logTag: 'ModelServer', logger: shared.Logger }, dataConnection => {
923
+ new DataServer<${roots.join(' | ')}>(dataConnection, shared);
924
+ return { dispose: () => undefined };
925
+ });
926
+ 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
+ })
937
+ .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}\`);
940
+ });
941
+ `
942
+ : '';
943
+ const glspBlock = glsp
944
+ ? `
945
+ // GLSP head on the same shared workspace. \`HydraniumGlspAppModule\` is used
946
+ // unsubclassed: the app container is one per process and cannot know which
947
+ // grammar a request concerns, so nothing per-language belongs there — each
948
+ // diagram module declares its own language instead.
949
+ //
950
+ // GLSP framework logs route through the LSP connection rather than stdout, which
951
+ // IS the LSP transport in stdio mode; writing there corrupts the protocol stream.
952
+ const glspServer = startGlspServer({
953
+ // The GLSP log threshold lives on the logger, not beside it: the framework
954
+ // replaces GLSP's own \`Logger\` binding, so a launcher-level \`logLevel\` would
955
+ // be discarded. The logger tracks the framework's process-wide threshold, so
956
+ // \`HYDRANIUM_LOG_LEVEL\` and the LSP log-level setting govern GLSP output too;
957
+ // pass \`logLevel\` only to make GLSP quieter than the rest of the server.
958
+ createLogger: caller => new GlspClientLogger(shared, { component: caller }),
959
+ serverModule: new ServerModule()${diagrams.map(grammar => `.configureDiagramModule(new ${grammar.grammar}DiagramModule())`).join('')},
960
+ appModules: [new HydraniumGlspAppModule({ shared })],
961
+ lspConnection: shared.lsp.Connection,
962
+ portCommand: ${upper}_GLSP_PORT_COMMAND
963
+ });
964
+ void glspServer;
965
+ `
966
+ : '';
967
+ const text = `${SHEBANG}// Standalone entry point: starts ${headSummary} in the same
968
+ // process, so every head shares one model store.
969
+ // Invocation: \`node lib/main.js --stdio\`, or the package's \`__PROJECT_ID__\` bin
970
+ // script.${data
971
+ ? `
972
+ //
973
+ // NOT the entry \`hydranium-cli query\` / \`save\` / \`projects\` / \`watch\` speak to:
974
+ // stdio here carries LSP, and the data head is a socket whose port is published
975
+ // over the LSP connection. Those subcommands spawn \`data-server-main.js\`.`
976
+ : ''}
977
+ //
978
+ // Everything here runs at module scope, so this file is an executable rather
979
+ // than a library entry — import \`./index.js\` instead to compose the language.
980
+
981
+ ${reflectImport}${glspImports}${importList(coreNodeSymbols, '@hydranium/core/node', columns)}
982
+ ${data ? "import { DataServer } from '@hydranium/data-server';\n" : ''}import { startLanguageServer } from '@hydranium/langium/lsp';
983
+ import { ProposedFeatures, createConnection } from 'vscode-languageserver/node';
984
+ ${diagramImports}${transferImports}import { create__NAME__Services } from './language-server/__PROJECT_ID__-module.js';
985
+ ${portCommands === '' ? '' : '\n' + portCommands}
986
+ const connection = createConnection(ProposedFeatures.all);
987
+ const { shared } = create__NAME__Services({ connection, ...NodeFileSystem });
988
+ startLanguageServer(shared);
989
+ ${dataBlock}${glspBlock}`;
990
+ return project(text, composition);
991
+ }
992
+ /**
993
+ * The second executable entry: the data head alone, on stdio.
994
+ *
995
+ * Emitted with the `data` head because it is the ONLY command line that reaches
996
+ * that head from outside the editor. `main.ts` gives stdio to LSP and publishes
997
+ * the data head's socket port over the LSP connection, so a client that is not
998
+ * an LSP client cannot discover it — which is every one of `hydranium-cli`'s
999
+ * `query` / `save` / `projects` / `watch`, all of which drive the data protocol
1000
+ * over a spawned child's stdin/stdout. Without this file a scaffolded project
1001
+ * has no `--server` value, and the closest guess — the LSP entry — fails as an
1002
+ * unrecognised method rather than as a missing head.
1003
+ *
1004
+ * A separate process rather than a flag on `main.ts`: only one protocol can own
1005
+ * stdio, so the two entries are mutually exclusive by construction.
1006
+ */
1007
+ function dataServerMainFile(composition) {
1008
+ const columns = columnsFor(composition);
1009
+ const roots = [...composition.grammars.map(grammar => grammar.entryRule)].sort();
1010
+ const plural = roots.length > 1 ? 's' : '';
1011
+ const text = `${SHEBANG}// Standalone entry point: a data-server head on **stdio**, with no LSP head in
1012
+ // the process. This is the transport \`hydranium-cli\` speaks — \`query\`, \`save\`,
1013
+ // \`projects\` and \`watch\` all spawn a server command and drive JSON-RPC over its
1014
+ // stdin/stdout — so it is what makes those subcommands usable against this
1015
+ // language.
1016
+ //
1017
+ // Invocation: \`node lib/data-server-main.js [<workspace-path>]\`, or the
1018
+ // package's \`__PROJECT_ID__-data-server\` bin script. The workspace path defaults to
1019
+ // the process cwd, which is what the CLI's \`--cwd\` sets on the child.
1020
+ //
1021
+ // Contrast with \`main.ts\`, the editor entry: there the LSP head owns stdio and
1022
+ // the data head is a socket published over the LSP connection. Here there is no
1023
+ // LSP connection at all, so the workspace initialization an
1024
+ // \`initialize\`/\`initialized\` pair would otherwise drive has to happen here —
1025
+ // which is why this entry uses \`startStdioServer\` rather than wiring a
1026
+ // connection directly.
1027
+ //
1028
+ // Everything here runs at module scope, so this file is an executable rather
1029
+ // than a library entry — import \`./index.js\` instead to compose the language.
1030
+
1031
+ ${importList(['NodeFileSystem', 'startStdioServer'], '@hydranium/core/node', columns)}
1032
+ import { DataServer } from '@hydranium/data-server';
1033
+ // 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)}
1035
+ import { create__NAME__Services } from './language-server/__PROJECT_ID__-module.js';
1036
+
1037
+ const { shared } = create__NAME__Services({ ...NodeFileSystem });
1038
+
1039
+ // \`startStdioServer\` owns the transport AND the workspace bring-up, including
1040
+ // the ordering between them: a head with no LSP connection never receives
1041
+ // \`initialize\`/\`initialized\`, and initialization has to complete before the
1042
+ // reader is attached or a request arriving during startup races an unpopulated
1043
+ // project registry. The launcher exists so no adopter has to re-derive that
1044
+ // ordering by hand.
1045
+ const server = startStdioServer(
1046
+ {
1047
+ shared,
1048
+ // Defaults to the process cwd, which is what the CLI's \`--cwd\` sets on the
1049
+ // spawned child; an explicit path argument overrides it.
1050
+ workspace: process.argv[2] ?? process.cwd(),
1051
+ logger: shared.Logger,
1052
+ logTag: 'ModelServer'
1053
+ },
1054
+ connection => {
1055
+ new DataServer<${roots.join(' | ')}>(connection, shared);
1056
+ // The DataServer self-cleans via \`connection.onClose\`, so there is nothing
1057
+ // extra to tear down here.
1058
+ return { dispose: () => undefined };
1059
+ }
1060
+ );
1061
+
1062
+ // Surfaces a failed bring-up as a non-zero exit instead of a silent, listening
1063
+ // head that would answer against an empty workspace.
1064
+ server.started.catch(() => process.exit(1));
1065
+ `;
1066
+ return project(text, composition);
1067
+ }
1068
+ /** The scaffold's first test: the DI tree composes and every language is registered. */
1069
+ function servicesTest(composition) {
1070
+ const ids = composition.grammars.map(grammar => grammar.languageId);
1071
+ const extensions = composition.grammars.flatMap(grammar => grammar.extensions.map(extension => `.${extension}`));
1072
+ const title = ids.length === 1 ? 'exactly one language' : `all ${ids.length} languages`;
1073
+ const text = `// The scaffold's first test: the DI tree composes and every language is
1074
+ // registered. Deliberately grammar-agnostic, so it keeps passing once you
1075
+ // replace the starter grammar with your own.
1076
+ //
1077
+ // \`createServices()\` not throwing is itself an assertion — the framework's
1078
+ // \`assertCoreSlotsBound\` runs during bootstrap and fails loudly when a module
1079
+ // is missing from the composition.
1080
+
1081
+ import { describe, expect, it } from 'vitest';
1082
+ import { createServices } from '../src/services.js';
1083
+
1084
+ describe('__NAME__ services', () => {
1085
+ it('composes the DI tree and registers ${title}', () => {
1086
+ const { shared } = createServices();
1087
+
1088
+ const registered = shared.ServiceRegistry.all.map(language => language.LanguageMetaData);
1089
+ expect(registered.map(metadata => metadata.languageId)).toEqual([${quotedList(ids)}]);
1090
+ expect(registered.flatMap(metadata => [...metadata.fileExtensions])).toEqual([${quotedList(extensions)}]);
1091
+ });
1092
+
1093
+ it('binds a reflection covering the generated AST', () => {
1094
+ const { shared } = createServices();
1095
+
1096
+ expect(shared.AstReflection.getAllTypes().length).toBeGreaterThan(0);
1097
+ });
1098
+ });
1099
+ `;
1100
+ return project(text, composition);
1101
+ }
1102
+ /**
1103
+ * One `describe` per grammar, round-tripping that grammar's serializer.
1104
+ *
1105
+ * Both directions, because they fail independently: an emitter reaching for
1106
+ * `$refText` renders the AST correctly and silently drops every reference on the
1107
+ * transfer path, which is the shape the data head hands it.
1108
+ */
1109
+ const SERIALIZATION_SUITE = `describe('__GRAMMAR__ serialization', () => {
1110
+ const source = 'node first -> second\\nnode second';
1111
+
1112
+ it('round-trips parsed source back to the same text', async () => {
1113
+ const { __GRAMMAR__ } = createServices();
1114
+
1115
+ const document = await parseHelper<__ENTRY_RULE__>(__GRAMMAR__)(source, { documentUri: 'file:///round-trip.__EXTENSION__' });
1116
+
1117
+ expect(document.parseResult.parserErrors).toHaveLength(0);
1118
+ expect(await __GRAMMAR__.serializer.Serializer.serializeAst(document.parseResult.value)).toBe(source);
1119
+ });
1120
+
1121
+ it('emits the same text from a transfer model, whose references are plain strings', async () => {
1122
+ const { __GRAMMAR__ } = createServices();
1123
+ const model: Transfer__ENTRY_RULE__ = {
1124
+ $type: '__ENTRY_RULE__',
1125
+ nodes: [
1126
+ { $type: '__NODE_RULE__', name: 'first', target: 'second' },
1127
+ { $type: '__NODE_RULE__', name: 'second' }
1128
+ ]
1129
+ };
1130
+
1131
+ expect(await __GRAMMAR__.serializer.Serializer.serializeTransfer(model)).toBe(source);
1132
+ });
1133
+ });`;
1134
+ /**
1135
+ * The serializer round-trip test, one suite per grammar.
1136
+ *
1137
+ * Grammar-DERIVED, unlike `services.test.ts` next to it: it names the starter
1138
+ * rules and the syntax they spell, so replacing the grammar replaces this file
1139
+ * along with the serializer it covers. That is the same bargain
1140
+ * `gmodel-factory.ts` makes, and the reason it is worth making here is that
1141
+ * nothing else executes the serializer — the golden pins its bytes and `tsc`
1142
+ * pins its types, and neither can see a wrong emission.
1143
+ */
1144
+ function serializationTest(composition) {
1145
+ const columns = columnsFor(composition);
1146
+ const astTypes = composition.grammars.map(grammar => grammar.entryRule);
1147
+ const transferTypes = composition.grammars.map(grammar => `${grammar.entryRule} as Transfer${grammar.entryRule}`);
1148
+ const text = `// Round-trips each grammar through its serializer: parse the source, serialize
1149
+ // 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.
1152
+ //
1153
+ // The transfer case is the one that catches the mistake worth catching.
1154
+ // \`ModelService.modelToText\` short-circuits only a RAW STRING, so a typed model
1155
+ // from the data head reaches \`serializeTransfer\` — where a cross-reference is a
1156
+ // plain string, not a \`Reference\`.
1157
+
1158
+ import { parseHelper } from '@hydranium/core/testing';
1159
+ import { describe, expect, it } from 'vitest';
1160
+ ${importList(astTypes, '../src/language-server/ast.js', columns, true)}
1161
+ ${importList(transferTypes, '../src/language-server/generated-transfer/transfer-model.js', columns, true)}
1162
+ import { createServices } from '../src/services.js';
1163
+
1164
+ ${composition.grammars.map(grammar => render(SERIALIZATION_SUITE, composition, grammar)).join('\n\n')}
1165
+ `;
1166
+ return text;
1167
+ }
1168
+ /**
1169
+ * The three tiers `generator-langium` scaffolds, in the order a new adopter
1170
+ * breaks them: does my rule parse, does my cross-reference resolve, does a
1171
+ * broken one get reported.
1172
+ *
1173
+ * Each is grammar-derived and each keeps working in SHAPE once the starter
1174
+ * grammar is replaced, which is the same bargain `services.test.ts` makes from
1175
+ * the other direction — that one stays grammar-agnostic and asserts nothing
1176
+ * about the language.
1177
+ */
1178
+ const PARSING_SUITE = `describe('__GRAMMAR__ parsing', () => {
1179
+ it('parses the starter rules and populates the AST', async () => {
1180
+ const { __GRAMMAR__ } = createServices();
1181
+
1182
+ const document = await parseHelper<__ENTRY_RULE__>(__GRAMMAR__)('node first -> second\\nnode second', {
1183
+ documentUri: 'file:///parsing.__EXTENSION__'
1184
+ });
1185
+
1186
+ expect(document.parseResult.lexerErrors).toHaveLength(0);
1187
+ expect(document.parseResult.parserErrors).toHaveLength(0);
1188
+ expect(document.parseResult.value.nodes.map(node => node.name)).toEqual(['first', 'second']);
1189
+ });
1190
+
1191
+ it('reports a parser error for text the grammar does not accept', async () => {
1192
+ const { __GRAMMAR__ } = createServices();
1193
+
1194
+ // The name is mandatory, so this is a parse failure rather than a
1195
+ // validation one — nothing downstream of the parser runs on it.
1196
+ const document = await parseHelper<__ENTRY_RULE__>(__GRAMMAR__)('node -> second', {
1197
+ documentUri: 'file:///invalid.__EXTENSION__'
1198
+ });
1199
+
1200
+ expect(document.parseResult.parserErrors.length).toBeGreaterThan(0);
1201
+ });
1202
+ });`;
1203
+ /**
1204
+ * Both directions of linking, and the CROSS-document one is the half that
1205
+ * matters: a same-document reference resolves through the local scope and would
1206
+ * still pass with the global index empty.
1207
+ *
1208
+ * `DocumentBuilder.build` rather than `parseHelper`'s `validation` option
1209
+ * because linking is what is under test, and a build over BOTH documents is
1210
+ * what puts the first one's exports in the index the second one reads.
1211
+ */
1212
+ const LINKING_SUITE = `describe('__GRAMMAR__ linking', () => {
1213
+ it('resolves a reference within one document', async () => {
1214
+ const { shared, __GRAMMAR__ } = createServices();
1215
+
1216
+ const document = await parseHelper<__ENTRY_RULE__>(__GRAMMAR__)('node first -> second\\nnode second', {
1217
+ documentUri: 'file:///within.__EXTENSION__'
1218
+ });
1219
+ await shared.workspace.DocumentBuilder.build([document]);
1220
+
1221
+ expect(document.parseResult.value.nodes[0].target?.ref?.name).toBe('second');
1222
+ });
1223
+
1224
+ it('resolves a reference across documents, through the shared index', async () => {
1225
+ const { shared, __GRAMMAR__ } = createServices();
1226
+ const parse = parseHelper<__ENTRY_RULE__>(__GRAMMAR__);
1227
+
1228
+ const declaring = await parse('node second', { documentUri: 'file:///declaring.__EXTENSION__' });
1229
+ const referencing = await parse('node first -> second', { documentUri: 'file:///referencing.__EXTENSION__' });
1230
+ await shared.workspace.DocumentBuilder.build([declaring, referencing]);
1231
+
1232
+ // The URI, not just the name: the referencing document declares no
1233
+ // \`second\` of its own, but asserting WHERE the target came from is what
1234
+ // keeps this about the global index rather than about local scope.
1235
+ const target = referencing.parseResult.value.nodes[0].target?.ref;
1236
+ expect(target?.name).toBe('second');
1237
+ expect(target ? AstUtils.getDocument(target).uri.toString() : undefined).toBe('file:///declaring.__EXTENSION__');
1238
+ });
1239
+ });`;
1240
+ /**
1241
+ * The validation tier, asserting the FRAMEWORK's diagnostics rather than an
1242
+ * adopter rule.
1243
+ *
1244
+ * The scaffold binds no `validation.checks`, so a test over an adopter check
1245
+ * would have to invent one and would then assert the test's own fixture rather
1246
+ * than the language. What a scaffolded project really guarantees on day one is
1247
+ * that a dangling reference is reported, which is a linker diagnostic — and
1248
+ * that is the tier a new adopter breaks third.
1249
+ */
1250
+ const VALIDATING_SUITE = `describe('__GRAMMAR__ validation', () => {
1251
+ it('reports nothing for a well-formed document', async () => {
1252
+ const { shared, __GRAMMAR__ } = createServices();
1253
+
1254
+ const document = await parseHelper<__ENTRY_RULE__>(__GRAMMAR__)('node first -> second\\nnode second', {
1255
+ documentUri: 'file:///valid.__EXTENSION__'
1256
+ });
1257
+ await shared.workspace.DocumentBuilder.build([document], { validation: true });
1258
+
1259
+ expect(document.diagnostics ?? []).toHaveLength(0);
1260
+ });
1261
+
1262
+ it('reports an error for a reference that resolves to nothing', async () => {
1263
+ const { shared, __GRAMMAR__ } = createServices();
1264
+
1265
+ const document = await parseHelper<__ENTRY_RULE__>(__GRAMMAR__)('node first -> absent', {
1266
+ documentUri: 'file:///dangling.__EXTENSION__'
1267
+ });
1268
+ await shared.workspace.DocumentBuilder.build([document], { validation: true });
1269
+
1270
+ const diagnostics = document.diagnostics ?? [];
1271
+ expect(diagnostics).toHaveLength(1);
1272
+ expect(diagnostics[0].severity).toBe(DiagnosticSeverity.Error);
1273
+ expect(diagnostics[0].message).toContain('absent');
1274
+ });
1275
+ });`;
1276
+ /** Assemble one grammar-derived test file: a fixed preamble plus one suite per grammar. */
1277
+ function grammarTest(composition, header, imports, suite) {
1278
+ const columns = columnsFor(composition);
1279
+ const astTypes = composition.grammars.map(grammar => grammar.entryRule);
1280
+ return `${header}
1281
+
1282
+ import { parseHelper } from '@hydranium/core/testing';
1283
+ import { describe, expect, it } from 'vitest';
1284
+ ${imports.join('\n')}${imports.length ? '\n' : ''}${importList(astTypes, '../src/language-server/ast.js', columns, true)}
1285
+ import { createServices } from '../src/services.js';
1286
+
1287
+ ${composition.grammars.map(grammar => render(suite, composition, grammar)).join('\n\n')}
1288
+ `;
1289
+ }
1290
+ const PARSING_HEADER = `// Tier one of the three a new language breaks in order: does my rule parse.
1291
+ //
1292
+ // Grammar-derived, so replacing the starter grammar replaces this file — but
1293
+ // the SHAPE survives, which is the point of scaffolding it: the questions stay
1294
+ // the same for every language, only the source text changes.`;
1295
+ const LINKING_HEADER = `// Tier two: does my cross-reference resolve.
1296
+ //
1297
+ // Both cases matter and they fail for different reasons. A same-document
1298
+ // reference resolves through local scope alone; a cross-document one needs the
1299
+ // declaring document's exports in the shared global index, which is what a
1300
+ // multi-file workspace depends on and what a single-document test cannot see.`;
1301
+ const VALIDATING_HEADER = `// Tier three: does a broken model get reported.
1302
+ //
1303
+ // These are the FRAMEWORK's own linker diagnostics, not adopter checks — the
1304
+ // scaffold binds no \`validation.checks\`, and a test over an invented one would
1305
+ // assert its own fixture rather than the language. Bind a check in your
1306
+ // language module and assert it here alongside these.`;
1307
+ /** The README, assembled from lines to avoid escaping the Markdown code fences. */
1308
+ function readme(composition) {
1309
+ const multi = composition.grammars.length > 1;
1310
+ const data = composition.heads.includes('data');
1311
+ const primaryExtension = composition.grammars[0].extensions[0];
1312
+ const grammarBullets = composition.grammars.map(grammar => `- \`src/grammar/${grammar.grammarId}.langium\` — the \`${grammar.grammar}\` grammar (.${grammar.extensions.join(', .')})`);
1313
+ const lines = [
1314
+ '# __NAME__',
1315
+ '',
1316
+ 'A Hydranium language project scaffolded by `hydranium-cli init`.',
1317
+ '',
1318
+ '## Getting started',
1319
+ '',
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
+ '```bash',
1335
+ 'npm install',
1336
+ 'npm run langium:generate # generate the AST from the grammar',
1337
+ 'npm run build # generate + compile to lib/',
1338
+ 'npm test # typecheck the tests, then run them',
1339
+ '```',
1340
+ '',
1341
+ '## Try the framework CLI against it',
1342
+ '',
1343
+ // `npx`, not a bare invocation: `@hydranium/cli` is a devDependency of the
1344
+ // emitted project, so the binary is on PATH inside an npm script and
1345
+ // nowhere else.
1346
+ '```bash',
1347
+ '# Grammar / AST reflection',
1348
+ 'npx hydranium-cli reflect --services ./lib/services.js',
1349
+ '',
1350
+ '# Grammar-convention lint (CI gate)',
1351
+ 'npx hydranium-cli lint-grammar --services ./lib/services.js',
1352
+ '',
1353
+ '# Validate a workspace of model files (non-zero exit on errors)',
1354
+ 'npx hydranium-cli validate --services ./lib/services.js <workspace-dir>',
1355
+ ...(data
1356
+ ? [
1357
+ '',
1358
+ '# Data-head operations. `--server` is a command line the CLI spawns and then',
1359
+ "# drives the data protocol over the child's stdin/stdout, so it has to name",
1360
+ '# `data-server-main.js` — `main.js` gives stdio to LSP and answers these',
1361
+ '# methods with "Unhandled method".',
1362
+ '#',
1363
+ '# The workspace goes to the ENTRY, not to `--cwd`: `--cwd` re-roots the child,',
1364
+ '# so a relative entry path is refused by name (an absolute one is fine).',
1365
+ 'npx hydranium-cli projects --server "node ./lib/data-server-main.js <workspace-dir>"',
1366
+ `npx hydranium-cli query --server "node ./lib/data-server-main.js <workspace-dir>" --uri <file:// URI of a .${primaryExtension} file>`
1367
+ ]
1368
+ : []),
1369
+ '```',
1370
+ '',
1371
+ '## Layout',
1372
+ '',
1373
+ ...grammarBullets,
1374
+ ...(multi ? ['- `src/grammar/common.langium` — shared terminals, imported by each grammar (not a language)'] : []),
1375
+ '- `src/language-server/__PROJECT_ID__-module.ts` — `create__NAME__Services` DI wiring',
1376
+ ...composition.grammars.map(grammar => `- \`src/language-server/${grammar.grammarId}-serializer.ts\` — emits \`${grammar.grammar}\` back to text (the framework defaults this to a throw)`),
1377
+ '- `src/services.ts` — zero-arg `createServices()` for the headless CLI',
1378
+ '- `src/index.ts` — the package entry (`main`): DI factory + generated AST',
1379
+ `- \`src/main.ts\` — starts ${composition.heads.join(' + ')}, the \`__PROJECT_ID__\` bin entry`,
1380
+ ...(data
1381
+ ? [
1382
+ '- `src/data-server-main.ts` — the data head alone on stdio, the',
1383
+ ' `__PROJECT_ID__-data-server` bin entry and the `--server` value the CLI needs'
1384
+ ]
1385
+ : []),
1386
+ ...composition.grammars
1387
+ .filter(grammar => grammar.diagram)
1388
+ .map(grammar => `- \`src/glsp/${grammar.grammarId}/\` — the \`${grammar.grammar}\` diagram: type ids, state, storage, submission handler, GModel factory, configuration, create-node handler, DI module`),
1389
+ '- `test/services.test.ts` — the DI tree composes; grows as your language does',
1390
+ '- `test/parsing.test.ts` / `linking.test.ts` / `validating.test.ts` — the three tiers a new language breaks first',
1391
+ '- `test/serialization.test.ts` — each grammar round-trips through its serializer',
1392
+ '- `syntaxes/` — generated TextMate grammar for a VS Code extension (gitignored)',
1393
+ '',
1394
+ ...(data
1395
+ ? ['The two entry points are `bin` scripts rather than `main` on purpose:']
1396
+ : ['`main.ts` is a `bin` script rather than `main` on purpose:']),
1397
+ 'each opens a transport at module scope, so importing one would start a',
1398
+ 'server as a side effect. Compose the language through `src/index.ts` instead.',
1399
+ '',
1400
+ '## Three names, and when they diverge',
1401
+ '',
1402
+ ...(multi
1403
+ ? [
1404
+ 'This project holds several grammars, so the tiers have already diverged:',
1405
+ 'the project name is the umbrella and each grammar names its own language.'
1406
+ ]
1407
+ : [
1408
+ 'The scaffold sets all three to the same value, which is right for one',
1409
+ 'grammar and stops being right the moment you add a second:'
1410
+ ]),
1411
+ '',
1412
+ '| Name | Set by | Generates |',
1413
+ '| --- | --- | --- |',
1414
+ '| project | `--name` / `projectName` | `__NAME__AstReflection`, `__NAME__GeneratedSharedModule` — one set per project |',
1415
+ '| grammar | `--grammar` / `grammar X` | `<Grammar>GeneratedModule`, `<Grammar>LanguageMetaData` — one set per grammar |',
1416
+ '| language id | derived, or `--language-id` | file routing, the `langium-config` entry id |',
1417
+ '',
1418
+ 'A further grammar is another `--grammar` (or another entry in',
1419
+ '`langium-config.json` — never a second config file, because `AstReflection`',
1420
+ 'is one shared slot) with its own grammar name, while the project name stays',
1421
+ 'the umbrella.',
1422
+ '',
1423
+ 'Each grammar declares its own entry rule (`<Grammar>Model`) rather than a',
1424
+ 'shared `Model`, because one `langium-cli` run over N grammars emits one',
1425
+ 'combined `ast.ts` and two `Model` interfaces would collide in it.',
1426
+ '',
1427
+ ...(multi
1428
+ ? [
1429
+ 'The scaffolded grammars are independent — each imports `common.langium`',
1430
+ 'for its terminals and nothing else. Which grammar may reference which is',
1431
+ 'a modelling decision, so add an `import` between them when you know the',
1432
+ 'direction, and remember that references point one way.',
1433
+ ''
1434
+ ]
1435
+ : []),
1436
+ 'The `langium` / `langium-cli` versions are pinned exactly rather than',
1437
+ 'ranged: the framework treats `langium` and its `vscode-*` chain as one',
1438
+ 'atomic set and depends on a single physical copy, so a floating range can',
1439
+ 'silently resolve a second one.',
1440
+ '',
1441
+ '## If your repo gates license headers',
1442
+ '',
1443
+ 'The emitted `.ts` files carry no copyright header — the scaffold cannot know',
1444
+ 'your license. Run your own header tool over `src/` and `test/` after',
1445
+ 'scaffolding. The file-purpose comments are `//` runs rather than `/** */`',
1446
+ 'blocks precisely so that a tool which REPLACES the leading block comment',
1447
+ 'does not silently delete them.',
1448
+ '',
1449
+ `The ${data ? 'two `bin` entries' : '`bin` entry'} start with a \`#!\` line, and it has to STAY the first`,
1450
+ 'line: a header tool that prepends unconditionally leaves the shell reading',
1451
+ 'the license comment as a script, which is what a linked binary then runs.',
1452
+ ''
1453
+ ];
1454
+ return project(lines.join('\n'), composition);
1455
+ }
1456
+ /**
1457
+ * The GLSP files for ONE grammar's diagram.
1458
+ *
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.
1463
+ *
1464
+ * The index and the computed-bounds handler are framework classes used
1465
+ * unmodified; storage and the submission handler are thin typed subclasses,
1466
+ * which is where an adopter's own load/save and log formatting land.
1467
+ *
1468
+ * The set is EDITABLE rather than read-only: one create-node handler, registered
1469
+ * on the module. GLSP's own `DiagramModule` binds no handler that mutates a
1470
+ * source model, so without it the head renders and answers while telling the
1471
+ * client it may change nothing — which reads as a broken diagram rather than as a
1472
+ * deliberate viewer.
1473
+ */
1474
+ function glspFiles(composition, grammar) {
1475
+ const columns = columnsFor(composition);
1476
+ const upper = screamingSnake(grammar.grammarId);
1477
+ const dir = `src/glsp/${grammar.grammarId}`;
1478
+ const render = (text) => grammarTier(project(text, composition), grammar);
1479
+ const types = `// GLSP diagram-type and element-type ids for the __GRAMMAR__ diagram.
1480
+ //
1481
+ // **Authoritative half of a client/server contract.** Every id here has to be
1482
+ // registered on the client too: sprotty's registries are exact-key maps with no
1483
+ // prefix fallback, so an id the client does not know yields an element with none
1484
+ // of a node's features, rendered by \`MissingView\` with only a
1485
+ // \`no registered view for type '…'\` console warning. Nothing fails server-side.
1486
+ //
1487
+ // The ids stay namespaced under the GLSP defaults (\`node:\` / \`edge:\`) so a
1488
+ // reader can tell a node id from an edge id at a glance.
1489
+
1490
+ import { DefaultTypes } from '@eclipse-glsp/server';
1491
+
1492
+ /**
1493
+ * The GLSP diagram type — the string GLSP routes every per-diagram-type request
1494
+ * by. Mirrored by the client; a mismatch silently DROPS the request rather than
1495
+ * reporting an unknown diagram type. Same value as the language id, so the two
1496
+ * cannot drift apart as a diagram gains element types.
1497
+ */
1498
+ export const ${upper}_DIAGRAM_TYPE = '__LANGUAGE_ID__';
1499
+
1500
+ /** A \`__NODE_RULE__\` — the starter grammar's one named, referenceable node. */
1501
+ export const ${upper}_NODE_TYPE = \`\${DefaultTypes.NODE}:__GRAMMAR_ID__-node\`;
1502
+
1503
+ /** A resolved \`target\` reference, drawn as a connection between two nodes. */
1504
+ export const ${upper}_EDGE_TYPE = \`\${DefaultTypes.EDGE}:__GRAMMAR_ID__-target\`;
1505
+ `;
1506
+ const state = `// GLSP state for the __GRAMMAR__ diagram.
1507
+ //
1508
+ // \`FullTextHydraniumGlspState\` is the simplest of the framework source-model
1509
+ // strategies (the others project a structured transfer model, over one document
1510
+ // or several): the source model is the whole document text, serialised through
1511
+ // the per-URI \`Serializer\` and round-tripped by re-parsing. Both seams resolve
1512
+ // through shared services, so narrowing the root type is all an adopter adds.
1513
+ //
1514
+ // It cannot field-merge — every concurrent edit on a whole-document model is a
1515
+ // same-document collision, so undo / redo degrade to drop-on-divergence. Move to
1516
+ // \`ReconcilingTransferHydraniumGlspState\` when you need field-level undo.
1517
+
1518
+ import { FullTextHydraniumGlspState } from '@hydranium/glsp-server';
1519
+ import { injectable } from 'inversify';
1520
+ import type { __ENTRY_RULE__ } from '../../language-server/ast.js';
1521
+
1522
+ @injectable()
1523
+ export class __GRAMMAR__GlspState extends FullTextHydraniumGlspState<__ENTRY_RULE__> {}
1524
+ `;
1525
+ const storage = `// Source-model storage for the __GRAMMAR__ diagram, inheriting both framework
1526
+ // defaults: \`loadSourceModel\` (open + settle + \`setSourceRoot\`) and
1527
+ // \`saveSourceModel\` (through \`ModelService.save\` → the per-URI \`Serializer\` →
1528
+ // the multi-client text store → \`WritableFileSystemProvider\`).
1529
+ //
1530
+ // The subclass exists so a bespoke load or save has a stable place to land.
1531
+
1532
+ import { type FullTextSourceModel, HydraniumGlspStorage } from '@hydranium/glsp-server';
1533
+ import { injectable } from 'inversify';
1534
+ import type { __ENTRY_RULE__ } from '../../language-server/ast.js';
1535
+
1536
+ @injectable()
1537
+ export class __GRAMMAR__GlspStorage extends HydraniumGlspStorage<__ENTRY_RULE__, FullTextSourceModel> {}
1538
+ `;
1539
+ const submission = `// Submission handler for the __GRAMMAR__ diagram.
1540
+ //
1541
+ // Inherits the framework's \`readyEvent = IntegrityService.SettledState\`, which
1542
+ // is load-bearing rather than incidental: the GModel factory resolves
1543
+ // \`target.ref\`, so it needs a fully-linked AST. Without the gate those reads
1544
+ // fire mid-build and warn about resolution before scopes are computed.
1545
+ //
1546
+ // Only \`formatSourceRoot\` is overridden, so the submit log names the model and
1547
+ // its node count instead of a bare \`$type\`.
1548
+
1549
+ import { ModelState } from '@eclipse-glsp/server';
1550
+ import { type FullTextSourceModel, HydraniumGlspSubmissionHandler } from '@hydranium/glsp-server';
1551
+ import { inject, injectable } from 'inversify';
1552
+ import type { __ENTRY_RULE__ } from '../../language-server/ast.js';
1553
+ import type { __GRAMMAR__GlspState } from './state.js';
1554
+
1555
+ @injectable()
1556
+ export class __GRAMMAR__SubmissionHandler extends HydraniumGlspSubmissionHandler<__ENTRY_RULE__, FullTextSourceModel> {
1557
+ @inject(ModelState) declare protected modelState: __GRAMMAR__GlspState;
1558
+
1559
+ protected override formatSourceRoot(root: __ENTRY_RULE__ | undefined): string {
1560
+ return root ? \`__ENTRY_RULE__ nodes=\${root.nodes.length}\` : 'none';
1561
+ }
1562
+ }
1563
+ `;
1564
+ const factory = `// AST → GModel for the __GRAMMAR__ diagram: one node per \`__NODE_RULE__\`, one
1565
+ // edge per resolved \`target\` reference.
1566
+ //
1567
+ // Ids come from the index rather than being composed here, so the id strategy
1568
+ // stays in one place. Nodes are emitted before edges, because a GLSP edge
1569
+ // pointing at an id that does not exist fails client-side with a far less
1570
+ // obvious error than a missing edge. An unresolved reference is SKIPPED rather
1571
+ // than treated as an error: a dangling reference is ordinary editing state, and
1572
+ // the LSP head already reports it as a diagnostic.
1573
+
1574
+ import { DefaultTypes, GEdge, GGraph, GLabel, type GModelFactory, GNode, ModelState } from '@eclipse-glsp/server';
1575
+ import { inject, injectable } from 'inversify';
1576
+ import type { __ENTRY_RULE__ } from '../../language-server/ast.js';
1577
+ import { ${upper}_EDGE_TYPE, ${upper}_NODE_TYPE } from './types.js';
1578
+ import type { __GRAMMAR__GlspState } from './state.js';
1579
+
1580
+ @injectable()
1581
+ export class __GRAMMAR__GModelFactory implements GModelFactory {
1582
+ @inject(ModelState) protected readonly modelState!: __GRAMMAR__GlspState;
1583
+
1584
+ createModel(): void {
1585
+ const root = this.modelState.sourceRoot;
1586
+ const graph = GGraph.builder().id(this.modelState.sourceUri).build();
1587
+ if (root) {
1588
+ this.buildGraph(root, graph);
1589
+ }
1590
+ this.modelState.updateRoot(graph);
1591
+ }
1592
+
1593
+ protected buildGraph(root: __ENTRY_RULE__, graph: GGraph): void {
1594
+ for (const node of root.nodes) {
1595
+ const id = this.modelState.index.createId(node);
1596
+ graph.children.push(
1597
+ GNode.builder()
1598
+ .id(id)
1599
+ .type(${upper}_NODE_TYPE)
1600
+ .add(GLabel.builder().id(\`\${id}_name\`).text(node.name).type(DefaultTypes.LABEL).build())
1601
+ .build()
1602
+ );
1603
+ }
1604
+ for (const node of root.nodes) {
1605
+ const target = node.target?.ref;
1606
+ if (!target) {
1607
+ continue;
1608
+ }
1609
+ const sourceId = this.modelState.index.createId(node);
1610
+ graph.children.push(
1611
+ GEdge.builder()
1612
+ .id(\`\${sourceId}_target\`)
1613
+ .type(${upper}_EDGE_TYPE)
1614
+ .sourceId(sourceId)
1615
+ .targetId(this.modelState.index.createId(target))
1616
+ .build()
1617
+ );
1618
+ }
1619
+ }
1620
+ }
1621
+ `;
1622
+ const configuration = `// Diagram configuration for the __GRAMMAR__ diagram.
1623
+ //
1624
+ // **Every hint is \`false\`, and that is not the same as read-only.** The starter
1625
+ // __GRAMMAR__CreateNodeOperationHandler makes this diagram editable through the
1626
+ // tool palette, which GLSP assembles from the create handlers' trigger actions —
1627
+ // there is no \`creatable\` hint. What the hints govern is delete / reparent /
1628
+ // reposition / resize, and nothing backs those, so declaring one would offer a
1629
+ // gesture whose operation the server rejects: a worse failure than the tool being
1630
+ // absent. Turn a hint on in the same change that adds its handler.
1631
+ //
1632
+ // \`needsClientLayout\` is \`true\` and \`layoutKind\` is \`NONE\`: the starter grammar
1633
+ // persists no bounds, so the client measures and places everything. That is also
1634
+ // why \`ChangeBoundsOperation\` is a poor second handler to add — with nowhere in
1635
+ // the grammar to write a position, a move never reaches the text and is lost on
1636
+ // the next reload.
1637
+
1638
+ import { type GModelElementConstructor } from '@eclipse-glsp/graph';
1639
+ ${importList(['type DiagramConfiguration', 'type EdgeTypeHint', 'ServerLayoutKind', 'type ShapeTypeHint', 'getDefaultMapping'], '@eclipse-glsp/server', columns)}
1640
+ import { injectable } from 'inversify';
1641
+ import { ${upper}_EDGE_TYPE, ${upper}_NODE_TYPE } from './types.js';
1642
+
1643
+ @injectable()
1644
+ export class __GRAMMAR__DiagramConfiguration implements DiagramConfiguration {
1645
+ readonly layoutKind: ServerLayoutKind = ServerLayoutKind.NONE;
1646
+ readonly needsClientLayout: boolean = true;
1647
+ readonly animatedUpdate: boolean = false;
1648
+
1649
+ readonly typeMapping: Map<string, GModelElementConstructor> = getDefaultMapping();
1650
+
1651
+ readonly shapeTypeHints: ShapeTypeHint[] = [
1652
+ {
1653
+ elementTypeId: ${upper}_NODE_TYPE,
1654
+ deletable: false,
1655
+ reparentable: false,
1656
+ repositionable: false,
1657
+ resizable: false
1658
+ }
1659
+ ];
1660
+
1661
+ readonly edgeTypeHints: EdgeTypeHint[] = [
1662
+ {
1663
+ elementTypeId: ${upper}_EDGE_TYPE,
1664
+ deletable: false,
1665
+ repositionable: false,
1666
+ routable: false,
1667
+ sourceElementTypeIds: [${upper}_NODE_TYPE],
1668
+ targetElementTypeIds: [${upper}_NODE_TYPE]
1669
+ }
1670
+ ];
1671
+ }
1672
+ `;
1673
+ const createHandler = `// The starter operation handler for the __GRAMMAR__ diagram: the tool-palette
1674
+ // entry that creates a \`__NODE_RULE__\`.
1675
+ //
1676
+ // **Deleting this file and its \`configureOperationHandlers\` registration in
1677
+ // \`diagram-module.ts\` gives a read-only viewer**, and nothing else has to change
1678
+ // — creation is offered through the palette rather than through a type hint, so
1679
+ // every hint in \`diagram-configuration.ts\` is already \`false\`. The scaffold emits
1680
+ // the editable direction because that asymmetry runs one way: editable →
1681
+ // read-only is a deletion the compiler checks, while read-only → editable is
1682
+ // authoring against a seam you have not used yet.
1683
+ //
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.
1693
+ //
1694
+ // **The drop location is discarded.** \`needsClientLayout\` is \`true\` and the
1695
+ // starter grammar persists no bounds, so there is nowhere to put a coordinate: the
1696
+ // node is appended at the end of the document and the client places it.
1697
+ //
1698
+ // 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.
1701
+
1702
+ import { type Command, type CreateNodeOperation, JsonCreateNodeOperationHandler, type MaybePromise } from '@eclipse-glsp/server';
1703
+ import { findNextUnique } from '@hydranium/protocol';
1704
+ import { injectable } from 'inversify';
1705
+ import { ${upper}_NODE_TYPE } from './types.js';
1706
+ import type { __GRAMMAR__GlspState } from './state.js';
1707
+
1708
+ /** Proposed name for a new node, uniquified against the ones the document already has. */
1709
+ const NODE_NAME_STEM = 'Node';
1710
+
1711
+ @injectable()
1712
+ export class __GRAMMAR__CreateNodeOperationHandler extends JsonCreateNodeOperationHandler {
1713
+ declare protected modelState: __GRAMMAR__GlspState;
1714
+
1715
+ /** The palette's word for the thing it creates, so a noun rather than an action. */
1716
+ override readonly label = '__NODE_RULE__';
1717
+ elementTypeIds = [${upper}_NODE_TYPE];
1718
+
1719
+ override createCommand(operation: CreateNodeOperation): MaybePromise<Command | undefined> {
1720
+ if (!this.elementTypeIds.includes(operation.elementTypeId)) {
1721
+ return undefined;
1722
+ }
1723
+ const state = this.modelState;
1724
+ const before = this.documentText();
1725
+ const after = this.withNode(
1726
+ before,
1727
+ findNextUnique(
1728
+ 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\`;
1751
+ }
1752
+ }
1753
+ `;
1754
+ const module = `// GLSP diagram module for __GRAMMAR__ — the DI wiring of one diagram type.
1755
+ //
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
+ // \`configureActionHandlers\` REBINDS rather than adds: GLSP's own
1763
+ // \`DiagramModule\` already registers \`ComputedBoundsActionHandler\`, and two
1764
+ // handlers for one action would both run.
1765
+ //
1766
+ // \`configureOperationHandlers\` ADDS: GLSP's default pair
1767
+ // (\`CompoundOperationHandler\` and \`LayoutOperationHandler\`) mutates no source
1768
+ // model, so the starter create handler is what makes this diagram editable at all.
1769
+
1770
+ ${importList([
1771
+ 'ActionHandlerConstructor',
1772
+ 'BindingTarget',
1773
+ 'DiagramConfiguration',
1774
+ 'GModelFactory',
1775
+ 'GModelIndex',
1776
+ 'InstanceMultiBinding',
1777
+ 'ModelState',
1778
+ 'ModelSubmissionHandler',
1779
+ 'OperationHandlerConstructor',
1780
+ 'SourceModelStorage'
1781
+ ], '@eclipse-glsp/server', columns, true)}
1782
+ import { ComputedBoundsActionHandler } from '@eclipse-glsp/server';
1783
+ import type { LanguageMetaData } from '@hydranium/langium';
1784
+ ${importList(['HydraniumGlspComputedBoundsActionHandler', 'AbstractHydraniumGlspDiagramModule', 'HydraniumGlspIndex'], '@hydranium/glsp-server', columns)}
1785
+ import { __GRAMMAR__LanguageMetaData } from '../../language-server/generated/module.js';
1786
+ import { __GRAMMAR__CreateNodeOperationHandler } from './create-node-operation-handler.js';
1787
+ import { __GRAMMAR__DiagramConfiguration } from './diagram-configuration.js';
1788
+ import { __GRAMMAR__GModelFactory } from './gmodel-factory.js';
1789
+ import { __GRAMMAR__GlspState } from './state.js';
1790
+ import { __GRAMMAR__GlspStorage } from './storage.js';
1791
+ import { __GRAMMAR__SubmissionHandler } from './submission-handler.js';
1792
+ import { ${upper}_DIAGRAM_TYPE } from './types.js';
1793
+
1794
+ export class __GRAMMAR__DiagramModule extends AbstractHydraniumGlspDiagramModule {
1795
+ readonly diagramType = ${upper}_DIAGRAM_TYPE;
1796
+
1797
+ protected override declareLanguage(): LanguageMetaData {
1798
+ return __GRAMMAR__LanguageMetaData;
1799
+ }
1800
+
1801
+ protected override bindModelState(): BindingTarget<ModelState> {
1802
+ return { service: __GRAMMAR__GlspState };
1803
+ }
1804
+
1805
+ protected override bindSourceModelStorage(): BindingTarget<SourceModelStorage> {
1806
+ return { service: __GRAMMAR__GlspStorage };
1807
+ }
1808
+
1809
+ protected override bindModelSubmissionHandler(): BindingTarget<ModelSubmissionHandler> {
1810
+ return { service: __GRAMMAR__SubmissionHandler };
1811
+ }
1812
+
1813
+ protected override bindDiagramConfiguration(): BindingTarget<DiagramConfiguration> {
1814
+ return { service: __GRAMMAR__DiagramConfiguration };
1815
+ }
1816
+
1817
+ protected override bindGModelFactory(): BindingTarget<GModelFactory> {
1818
+ return { service: __GRAMMAR__GModelFactory };
1819
+ }
1820
+
1821
+ /** The framework index unmodified — it keys elements by name, with a positional fallback. */
1822
+ protected override bindGModelIndex(): BindingTarget<GModelIndex> {
1823
+ return { service: HydraniumGlspIndex };
1824
+ }
1825
+
1826
+ protected override configureActionHandlers(binding: InstanceMultiBinding<ActionHandlerConstructor>): void {
1827
+ super.configureActionHandlers(binding);
1828
+ binding.rebind(ComputedBoundsActionHandler, HydraniumGlspComputedBoundsActionHandler);
1829
+ }
1830
+
1831
+ protected override configureOperationHandlers(binding: InstanceMultiBinding<OperationHandlerConstructor>): void {
1832
+ super.configureOperationHandlers(binding);
1833
+ binding.add(__GRAMMAR__CreateNodeOperationHandler);
1834
+ }
1835
+ }
1836
+ `;
1837
+ return [
1838
+ { path: `${dir}/types.ts`, content: render(types) },
1839
+ { path: `${dir}/state.ts`, content: render(state) },
1840
+ { path: `${dir}/storage.ts`, content: render(storage) },
1841
+ { path: `${dir}/submission-handler.ts`, content: render(submission) },
1842
+ { path: `${dir}/gmodel-factory.ts`, content: render(factory) },
1843
+ { path: `${dir}/diagram-configuration.ts`, content: render(configuration) },
1844
+ { path: `${dir}/create-node-operation-handler.ts`, content: render(createHandler) },
1845
+ { path: `${dir}/diagram-module.ts`, content: render(module) }
1846
+ ];
1847
+ }
1848
+ /**
1849
+ * Every file the scaffold emits, fully rendered.
1850
+ *
1851
+ * Grammar-count-dependent emissions live here rather than in a template string:
1852
+ * one `.langium` file per grammar, and the shared `common.langium` fragment only
1853
+ * when there is more than one grammar to share it.
1854
+ */
1855
+ export function buildInitTemplates(composition) {
1856
+ const multi = composition.grammars.length > 1;
1857
+ const grammarFiles = composition.grammars.map(grammar => ({
1858
+ path: `src/grammar/${grammar.grammarId}.langium`,
1859
+ content: render(grammarFile(multi), composition, grammar)
1860
+ }));
1861
+ if (multi) {
1862
+ grammarFiles.unshift({ path: 'src/grammar/common.langium', content: COMMON_GRAMMAR });
1863
+ }
1864
+ const diagramFiles = composition.grammars.filter(grammar => grammar.diagram).flatMap(grammar => glspFiles(composition, grammar));
1865
+ return [
1866
+ { path: 'package.json', content: packageJson(composition) },
1867
+ { path: 'langium-config.json', content: langiumConfig(composition) },
1868
+ { path: 'tsconfig.json', content: tsconfigJson(composition) },
1869
+ { path: 'tsconfig.test.json', content: TSCONFIG_TEST },
1870
+ { path: 'vitest.config.ts', content: VITEST_CONFIG },
1871
+ {
1872
+ path: '.gitignore',
1873
+ content: composition.packaging.workspace === undefined ? GITIGNORE : GITIGNORE_WORKSPACE_MEMBER
1874
+ },
1875
+ { path: 'README.md', content: readme(composition) },
1876
+ ...grammarFiles,
1877
+ { path: `src/language-server/${composition.projectId}-module.ts`, content: moduleFile(composition) },
1878
+ { path: 'src/language-server/ast.ts', content: render(AST, composition, composition.grammars[0]) },
1879
+ ...composition.grammars.map(grammar => ({
1880
+ path: `src/language-server/${grammar.grammarId}-serializer.ts`,
1881
+ content: render(SERIALIZER, composition, grammar)
1882
+ })),
1883
+ { path: 'src/index.ts', content: indexFile(composition) },
1884
+ { path: 'src/services.ts', content: project(SERVICES, composition) },
1885
+ { path: 'src/main.ts', content: mainFile(composition) },
1886
+ ...(composition.heads.includes('data') ? [{ path: 'src/data-server-main.ts', content: dataServerMainFile(composition) }] : []),
1887
+ ...(portCommandNames(composition).length ? [{ path: 'src/head-ports.ts', content: headPortsFile(composition) }] : []),
1888
+ ...diagramFiles,
1889
+ { path: 'test/services.test.ts', content: servicesTest(composition) },
1890
+ { path: 'test/parsing.test.ts', content: grammarTest(composition, PARSING_HEADER, [], PARSING_SUITE) },
1891
+ {
1892
+ path: 'test/linking.test.ts',
1893
+ content: grammarTest(composition, LINKING_HEADER, ["import { AstUtils } from '@hydranium/langium';"], LINKING_SUITE)
1894
+ },
1895
+ {
1896
+ path: 'test/validating.test.ts',
1897
+ content: grammarTest(composition, VALIDATING_HEADER, ["import { DiagnosticSeverity } from 'vscode-languageserver';"], VALIDATING_SUITE)
1898
+ },
1899
+ { path: 'test/serialization.test.ts', content: serializationTest(composition) }
1900
+ ];
1901
+ }
1902
+ //# sourceMappingURL=init-templates.js.map