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