@hydranium/cli 1.0.0-next.31 → 1.0.0-next.312

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 (252) hide show
  1. package/README.md +79 -205
  2. package/bin/hydranium-cli.js +13 -0
  3. package/heap-analysis/heap/core/load.mjs +2 -2
  4. package/lib/cli-version.js.map +1 -1
  5. package/lib/cli.js +7 -2
  6. package/lib/cli.js.map +1 -1
  7. package/lib/commands/analyze-heap-args.js.map +1 -1
  8. package/lib/commands/analyze-heap.d.ts +3 -0
  9. package/lib/commands/analyze-heap.d.ts.map +1 -1
  10. package/lib/commands/analyze-heap.js +2 -1
  11. package/lib/commands/analyze-heap.js.map +1 -1
  12. package/lib/commands/ast-ground-truth-args.d.ts +3 -2
  13. package/lib/commands/ast-ground-truth-args.d.ts.map +1 -1
  14. package/lib/commands/ast-ground-truth-args.js +11 -9
  15. package/lib/commands/ast-ground-truth-args.js.map +1 -1
  16. package/lib/commands/ast-ground-truth-driver.js +2 -1
  17. package/lib/commands/ast-ground-truth-driver.js.map +1 -1
  18. package/lib/commands/ast-ground-truth.js +1 -1
  19. package/lib/commands/ast-ground-truth.js.map +1 -1
  20. package/lib/commands/client-session.d.ts +17 -0
  21. package/lib/commands/client-session.d.ts.map +1 -0
  22. package/lib/commands/client-session.js +27 -0
  23. package/lib/commands/client-session.js.map +1 -0
  24. package/lib/commands/create-args.d.ts +17 -0
  25. package/lib/commands/create-args.d.ts.map +1 -0
  26. package/lib/commands/create-args.js +53 -0
  27. package/lib/commands/create-args.js.map +1 -0
  28. package/lib/commands/create.d.ts +38 -0
  29. package/lib/commands/create.d.ts.map +1 -0
  30. package/lib/commands/create.js +43 -0
  31. package/lib/commands/create.js.map +1 -0
  32. package/lib/commands/delete-args.d.ts +17 -0
  33. package/lib/commands/delete-args.d.ts.map +1 -0
  34. package/lib/commands/delete-args.js +49 -0
  35. package/lib/commands/delete-args.js.map +1 -0
  36. package/lib/commands/delete.d.ts +30 -0
  37. package/lib/commands/delete.d.ts.map +1 -0
  38. package/lib/commands/delete.js +32 -0
  39. package/lib/commands/delete.js.map +1 -0
  40. package/lib/commands/generate-languages.d.ts +35 -0
  41. package/lib/commands/generate-languages.d.ts.map +1 -0
  42. package/lib/commands/generate-languages.js +105 -0
  43. package/lib/commands/generate-languages.js.map +1 -0
  44. package/lib/commands/generate-transfer-model-args.d.ts.map +1 -1
  45. package/lib/commands/generate-transfer-model-args.js +7 -0
  46. package/lib/commands/generate-transfer-model-args.js.map +1 -1
  47. package/lib/commands/generate-transfer-model-config.d.ts.map +1 -1
  48. package/lib/commands/generate-transfer-model-config.js +6 -3
  49. package/lib/commands/generate-transfer-model-config.js.map +1 -1
  50. package/lib/commands/generate-transfer-model.d.ts +15 -1
  51. package/lib/commands/generate-transfer-model.d.ts.map +1 -1
  52. package/lib/commands/generate-transfer-model.js +46 -8
  53. package/lib/commands/generate-transfer-model.js.map +1 -1
  54. package/lib/commands/harness-args.d.ts +44 -42
  55. package/lib/commands/harness-args.d.ts.map +1 -1
  56. package/lib/commands/harness-args.js +109 -27
  57. package/lib/commands/harness-args.js.map +1 -1
  58. package/lib/commands/headless-harness.d.ts +37 -20
  59. package/lib/commands/headless-harness.d.ts.map +1 -1
  60. package/lib/commands/headless-harness.js +68 -17
  61. package/lib/commands/headless-harness.js.map +1 -1
  62. package/lib/commands/init-args.d.ts.map +1 -1
  63. package/lib/commands/init-args.js +32 -27
  64. package/lib/commands/init-args.js.map +1 -1
  65. package/lib/commands/init-prompt.d.ts +3 -3
  66. package/lib/commands/init-prompt.d.ts.map +1 -1
  67. package/lib/commands/init-prompt.js +4 -5
  68. package/lib/commands/init-prompt.js.map +1 -1
  69. package/lib/commands/init-templates.d.ts +18 -14
  70. package/lib/commands/init-templates.d.ts.map +1 -1
  71. package/lib/commands/init-templates.js +732 -677
  72. package/lib/commands/init-templates.js.map +1 -1
  73. package/lib/commands/init-wizard.d.ts.map +1 -1
  74. package/lib/commands/init-wizard.js +17 -27
  75. package/lib/commands/init-wizard.js.map +1 -1
  76. package/lib/commands/init-workspace.d.ts +13 -11
  77. package/lib/commands/init-workspace.d.ts.map +1 -1
  78. package/lib/commands/init-workspace.js +21 -7
  79. package/lib/commands/init-workspace.js.map +1 -1
  80. package/lib/commands/init.d.ts +45 -19
  81. package/lib/commands/init.d.ts.map +1 -1
  82. package/lib/commands/init.js +80 -43
  83. package/lib/commands/init.js.map +1 -1
  84. package/lib/commands/kebab.d.ts +15 -0
  85. package/lib/commands/kebab.d.ts.map +1 -0
  86. package/lib/commands/kebab.js +17 -0
  87. package/lib/commands/kebab.js.map +1 -0
  88. package/lib/commands/lint-grammar-args.d.ts +3 -2
  89. package/lib/commands/lint-grammar-args.d.ts.map +1 -1
  90. package/lib/commands/lint-grammar-args.js +13 -10
  91. package/lib/commands/lint-grammar-args.js.map +1 -1
  92. package/lib/commands/lint-grammar-driver.js +2 -1
  93. package/lib/commands/lint-grammar-driver.js.map +1 -1
  94. package/lib/commands/lint-grammar-report.js.map +1 -1
  95. package/lib/commands/lint-grammar.js +1 -1
  96. package/lib/commands/lint-grammar.js.map +1 -1
  97. package/lib/commands/measure-memory-args.d.ts +3 -2
  98. package/lib/commands/measure-memory-args.d.ts.map +1 -1
  99. package/lib/commands/measure-memory-args.js +22 -11
  100. package/lib/commands/measure-memory-args.js.map +1 -1
  101. package/lib/commands/measure-memory-driver.js +5 -3
  102. package/lib/commands/measure-memory-driver.js.map +1 -1
  103. package/lib/commands/measure-memory.js +1 -1
  104. package/lib/commands/measure-memory.js.map +1 -1
  105. package/lib/commands/model-docs-args.d.ts +4 -3
  106. package/lib/commands/model-docs-args.d.ts.map +1 -1
  107. package/lib/commands/model-docs-args.js +12 -10
  108. package/lib/commands/model-docs-args.js.map +1 -1
  109. package/lib/commands/model-docs-driver.js +2 -1
  110. package/lib/commands/model-docs-driver.js.map +1 -1
  111. package/lib/commands/model-docs.js +1 -1
  112. package/lib/commands/model-docs.js.map +1 -1
  113. package/lib/commands/projects-args.d.ts.map +1 -1
  114. package/lib/commands/projects-args.js +5 -5
  115. package/lib/commands/projects-args.js.map +1 -1
  116. package/lib/commands/projects.d.ts +3 -4
  117. package/lib/commands/projects.d.ts.map +1 -1
  118. package/lib/commands/projects.js +2 -2
  119. package/lib/commands/projects.js.map +1 -1
  120. package/lib/commands/query-args.d.ts.map +1 -1
  121. package/lib/commands/query-args.js +5 -5
  122. package/lib/commands/query-args.js.map +1 -1
  123. package/lib/commands/query.d.ts +3 -4
  124. package/lib/commands/query.d.ts.map +1 -1
  125. package/lib/commands/query.js +2 -2
  126. package/lib/commands/query.js.map +1 -1
  127. package/lib/commands/reflect-args.d.ts +3 -2
  128. package/lib/commands/reflect-args.d.ts.map +1 -1
  129. package/lib/commands/reflect-args.js +13 -10
  130. package/lib/commands/reflect-args.js.map +1 -1
  131. package/lib/commands/reflect-driver.js +2 -1
  132. package/lib/commands/reflect-driver.js.map +1 -1
  133. package/lib/commands/reflect-report.js.map +1 -1
  134. package/lib/commands/reflect.js +1 -1
  135. package/lib/commands/reflect.js.map +1 -1
  136. package/lib/commands/save-args.d.ts.map +1 -1
  137. package/lib/commands/save-args.js +7 -4
  138. package/lib/commands/save-args.js.map +1 -1
  139. package/lib/commands/save.d.ts +8 -4
  140. package/lib/commands/save.d.ts.map +1 -1
  141. package/lib/commands/save.js +12 -23
  142. package/lib/commands/save.js.map +1 -1
  143. package/lib/commands/validate-args.d.ts +3 -2
  144. package/lib/commands/validate-args.d.ts.map +1 -1
  145. package/lib/commands/validate-args.js +13 -10
  146. package/lib/commands/validate-args.js.map +1 -1
  147. package/lib/commands/validate-driver.js +2 -1
  148. package/lib/commands/validate-driver.js.map +1 -1
  149. package/lib/commands/validate-report.js.map +1 -1
  150. package/lib/commands/validate.js +1 -1
  151. package/lib/commands/validate.js.map +1 -1
  152. package/lib/commands/watch-args.d.ts.map +1 -1
  153. package/lib/commands/watch-args.js +6 -4
  154. package/lib/commands/watch-args.js.map +1 -1
  155. package/lib/commands/watch.d.ts +3 -4
  156. package/lib/commands/watch.d.ts.map +1 -1
  157. package/lib/commands/watch.js +20 -2
  158. package/lib/commands/watch.js.map +1 -1
  159. package/lib/driver-heap.d.ts +52 -0
  160. package/lib/driver-heap.d.ts.map +1 -0
  161. package/lib/driver-heap.js +51 -0
  162. package/lib/driver-heap.js.map +1 -0
  163. package/lib/index.d.ts +2 -0
  164. package/lib/index.d.ts.map +1 -1
  165. package/lib/index.js +2 -0
  166. package/lib/index.js.map +1 -1
  167. package/lib/log-level.d.ts +21 -11
  168. package/lib/log-level.d.ts.map +1 -1
  169. package/lib/log-level.js +25 -14
  170. package/lib/log-level.js.map +1 -1
  171. package/lib/spawn-data-server.d.ts +9 -4
  172. package/lib/spawn-data-server.d.ts.map +1 -1
  173. package/lib/spawn-data-server.js +5 -4
  174. package/lib/spawn-data-server.js.map +1 -1
  175. package/lib/stderr-connection-logger.d.ts +12 -0
  176. package/lib/stderr-connection-logger.d.ts.map +1 -0
  177. package/lib/stderr-connection-logger.js +16 -0
  178. package/lib/stderr-connection-logger.js.map +1 -0
  179. package/lib/testing/echo-server.js +9 -1
  180. package/lib/testing/echo-server.js.map +1 -1
  181. package/package.json +52 -39
  182. package/src/cli.ts +7 -2
  183. package/src/commands/analyze-heap.ts +4 -1
  184. package/src/commands/ast-ground-truth-args.ts +15 -12
  185. package/src/commands/ast-ground-truth-driver.ts +2 -1
  186. package/src/commands/ast-ground-truth.ts +1 -1
  187. package/src/commands/client-session.ts +33 -0
  188. package/src/commands/create-args.ts +68 -0
  189. package/src/commands/create.ts +83 -0
  190. package/src/commands/delete-args.ts +64 -0
  191. package/src/commands/delete.ts +61 -0
  192. package/src/commands/generate-languages.ts +132 -0
  193. package/src/commands/generate-transfer-model-args.ts +7 -0
  194. package/src/commands/generate-transfer-model-config.ts +6 -3
  195. package/src/commands/generate-transfer-model.ts +56 -8
  196. package/src/commands/harness-args.ts +99 -31
  197. package/src/commands/headless-harness.ts +86 -26
  198. package/src/commands/init-args.ts +32 -27
  199. package/src/commands/init-prompt.ts +5 -6
  200. package/src/commands/init-templates.ts +790 -696
  201. package/src/commands/init-wizard.ts +17 -27
  202. package/src/commands/init-workspace.ts +31 -16
  203. package/src/commands/init.ts +126 -58
  204. package/src/commands/kebab.ts +17 -0
  205. package/src/commands/lint-grammar-args.ts +17 -13
  206. package/src/commands/lint-grammar-driver.ts +2 -1
  207. package/src/commands/lint-grammar.ts +1 -1
  208. package/src/commands/measure-memory-args.ts +27 -14
  209. package/src/commands/measure-memory-driver.ts +5 -3
  210. package/src/commands/measure-memory.ts +1 -1
  211. package/src/commands/model-docs-args.ts +16 -13
  212. package/src/commands/model-docs-driver.ts +2 -1
  213. package/src/commands/model-docs.ts +1 -1
  214. package/src/commands/projects-args.ts +5 -4
  215. package/src/commands/projects.ts +4 -6
  216. package/src/commands/query-args.ts +5 -4
  217. package/src/commands/query.ts +4 -6
  218. package/src/commands/reflect-args.ts +17 -13
  219. package/src/commands/reflect-driver.ts +2 -1
  220. package/src/commands/reflect.ts +1 -1
  221. package/src/commands/save-args.ts +7 -3
  222. package/src/commands/save.ts +24 -27
  223. package/src/commands/validate-args.ts +17 -13
  224. package/src/commands/validate-driver.ts +2 -1
  225. package/src/commands/validate.ts +1 -1
  226. package/src/commands/watch-args.ts +6 -3
  227. package/src/commands/watch.ts +28 -7
  228. package/src/driver-heap.ts +65 -0
  229. package/src/index.ts +2 -0
  230. package/src/log-level.ts +42 -14
  231. package/src/spawn-data-server.ts +17 -5
  232. package/src/stderr-connection-logger.ts +18 -0
  233. package/src/testing/echo-server.ts +11 -2
  234. package/templates/gitignore +1 -0
  235. package/templates/protocol/gitignore +3 -0
  236. package/templates/protocol/src/__GRAMMAR_ID__-diagram-types.ts +41 -0
  237. package/templates/server/gitignore +6 -0
  238. package/templates/server/src/glsp/__GRAMMAR_ID__/create-node-operation-handler.ts +62 -0
  239. package/templates/server/src/glsp/__GRAMMAR_ID__/gmodel-factory.ts +60 -0
  240. package/templates/server/src/glsp/__GRAMMAR_ID__/state.ts +18 -0
  241. package/templates/server/src/glsp/__GRAMMAR_ID__/storage.ts +16 -0
  242. package/templates/server/src/glsp/__GRAMMAR_ID__/submission-handler.ts +24 -0
  243. package/templates/server/src/grammar/common.langium +12 -0
  244. package/templates/server/src/language-server/__GRAMMAR_ID__-serializer.ts +53 -0
  245. package/templates/server/src/language-server/ast.ts +23 -0
  246. package/templates/server/src/node/services.ts +16 -0
  247. package/templates/server/test/__GRAMMAR_ID__-diagram.test.ts +112 -0
  248. package/templates/server/test/tsconfig.json +4 -0
  249. package/templates/server/tsconfig.test.json +10 -0
  250. package/templates/server/vitest.config.ts +14 -0
  251. package/templates/workspace/nodes.__EXTENSION__ +5 -0
  252. package/templates/workspace/references.__EXTENSION__ +2 -0
@@ -21,6 +21,9 @@
21
21
  * `bin` key, the DI module filename, and the data-server port command.
22
22
  * - `__CONFIG_ROOT_CONST__` — SCREAMING_SNAKE project id, naming the
23
23
  * `lsp.configurationRoot` constant a multi-grammar project binds.
24
+ * - `__PROTOCOL_PACKAGE__` — the protocol member's package name, scope
25
+ * included, which the server and every client import ids and wire types
26
+ * from.
24
27
  *
25
28
  * GRAMMAR tokens, substituted once per grammar:
26
29
  *
@@ -28,6 +31,8 @@
28
31
  * Drives the PER-LANGUAGE generated symbols (`<grammar>GeneratedModule`,
29
32
  * `<grammar>LanguageMetaData`).
30
33
  * - `__GRAMMAR_ID__` — kebab grammar id: the `.langium` filename.
34
+ * - `__GRAMMAR_ID_CONST__` — SCREAMING_SNAKE grammar id, prefixing the
35
+ * diagram's type-id constants.
31
36
  * - `__LANGUAGE_ID__` — kebab routing key: the `langium-config` entry id and
32
37
  * its TextMate output.
33
38
  * - `__EXTENSION__` — the grammar's FIRST file extension, without the dot.
@@ -48,10 +53,19 @@
48
53
  * generator emits `interface <projectName>Element` as the base type every rule
49
54
  * extends, which a rule of that name would collide with.
50
55
  *
51
- * Kept as in-source strings (not on-disk assets) so no build-time copy step
52
- * is needed and the substitution is unit-testable. The `.ts` / `.langium` / JSON
53
- * templates are derived from the in-repo reference example so the generated project
54
- * compiles against the real `@hydranium/*` API once `langium generate` has run.
56
+ * The project is two packages and a starter workspace, each in its own
57
+ * directory: `server/` (the grammar, the language and its heads), `protocol/`
58
+ * (what the server and its clients both import, so a client never installs
59
+ * Langium), and `workspace/` (starter model files every host and the CLI
60
+ * open). A standalone project adds a root manifest naming them as npm
61
+ * workspaces; a workspace member joins the surrounding root instead.
62
+ *
63
+ * A file that is fixed text with tokens lives in the `templates/` tree the
64
+ * package ships beside `lib`, at the path it is emitted at, tokens included
65
+ * (each ignore file without its dot); one computed from the composition is
66
+ * built here. The tree is read synchronously, so `runInit` stays synchronous,
67
+ * and it is neither formatted with the repo's settings nor given a license
68
+ * header, since either would change what `init` emits.
55
69
  *
56
70
  * Two conventions the templates follow deliberately:
57
71
  *
@@ -70,16 +84,8 @@
70
84
  * is what the detection cannot fix. A very long `--name` can still push a line
71
85
  * over; the adopter's own formatter settles it.
72
86
  */
87
+ import { readFileSync } from 'node:fs';
73
88
  import { readCliVersion } from '../cli-version.js';
74
- /**
75
- * The version every framework package carries before the first release.
76
- *
77
- * A scaffold made by a CLI still at this version pins a range the registry
78
- * cannot serve, so the emitted README keeps its yalc note and `init` keeps its
79
- * install warning while — and only while — it holds: from a published CLI the
80
- * derived pins resolve and either note would be false as printed.
81
- */
82
- export const UNPUBLISHED_FRAMEWORK_VERSION = '0.0.0';
83
89
  /**
84
90
  * The version `init` pins every `@hydranium/*` dependency at: the scaffolding
85
91
  * CLI's own.
@@ -107,12 +113,47 @@ function frameworkPin(composition) {
107
113
  function screamingSnake(projectId) {
108
114
  return projectId.replace(/-/g, '_').toUpperCase();
109
115
  }
116
+ /**
117
+ * A member's package name: `<id>-<member>`, under the scope when there is one.
118
+ *
119
+ * Suffixed even for the server, because a project of several packages has no
120
+ * member entitled to the bare id, and a later client or host would otherwise
121
+ * read as a part of the server.
122
+ */
123
+ export function memberPackageName(composition, member) {
124
+ const { scope } = composition.packaging;
125
+ const name = `${composition.projectId}-${member}`;
126
+ return scope === undefined ? name : `${scope}/${name}`;
127
+ }
128
+ /**
129
+ * The composition as one member sees it: inside a workspace, its placement
130
+ * sits one directory below the project's, so its `extends` and its lint
131
+ * config path reach one level further up.
132
+ */
133
+ function forMember(composition, member) {
134
+ const workspace = composition.packaging.workspace;
135
+ if (workspace === undefined) {
136
+ return composition;
137
+ }
138
+ return {
139
+ ...composition,
140
+ packaging: {
141
+ ...composition.packaging,
142
+ workspace: {
143
+ ...workspace,
144
+ targetPath: `${workspace.targetPath}/${member}`,
145
+ baseTsconfig: workspace.baseTsconfig === undefined ? undefined : `../${workspace.baseTsconfig}`
146
+ }
147
+ }
148
+ };
149
+ }
110
150
  /** Substitute the project-tier tokens. */
111
151
  function project(text, composition) {
112
152
  return text
113
153
  .replace(/__NAME__/g, composition.name)
114
154
  .replace(/__PROJECT_ID__/g, composition.projectId)
115
- .replace(/__CONFIG_ROOT_CONST__/g, screamingSnake(composition.projectId));
155
+ .replace(/__CONFIG_ROOT_CONST__/g, screamingSnake(composition.projectId))
156
+ .replace(/__PROTOCOL_PACKAGE__/g, memberPackageName(composition, 'protocol'));
116
157
  }
117
158
  /**
118
159
  * Substitute the grammar-tier tokens for ONE grammar.
@@ -124,6 +165,7 @@ function project(text, composition) {
124
165
  */
125
166
  function grammarTier(text, grammar) {
126
167
  return text
168
+ .replace(/__GRAMMAR_ID_CONST__/g, screamingSnake(grammar.grammarId))
127
169
  .replace(/__GRAMMAR_ID__/g, grammar.grammarId)
128
170
  .replace(/__GRAMMAR__/g, grammar.grammar)
129
171
  .replace(/__LANGUAGE_ID__/g, grammar.languageId)
@@ -135,6 +177,25 @@ function grammarTier(text, grammar) {
135
177
  function render(text, composition, grammar) {
136
178
  return grammarTier(project(text, composition), grammar);
137
179
  }
180
+ /** The template tree, the same two levels up from `src/commands` and `lib/commands`. */
181
+ const TEMPLATES = new URL('../../templates/', import.meta.url);
182
+ /** One template file's text. */
183
+ function template(path) {
184
+ return readFileSync(new URL(path, TEMPLATES), 'utf-8');
185
+ }
186
+ /** A template emitted once per project at its own path, rendered with the project tokens. */
187
+ function projectTemplate(path, composition) {
188
+ return { path, content: project(template(path), composition) };
189
+ }
190
+ /** A template emitted once per grammar, its path rendered like its content. */
191
+ function grammarTemplate(path, composition, grammar) {
192
+ return { path: render(path, composition, grammar), content: render(template(path), composition, grammar) };
193
+ }
194
+ /** An ignore file, stored without its dot because `npm pack` drops a nested `.gitignore` and applies it as an ignore file. */
195
+ function ignoreFile(directory) {
196
+ const prefix = directory === '' ? '' : `${directory}/`;
197
+ return { path: `${prefix}.gitignore`, content: template(`${prefix}gitignore`) };
198
+ }
138
199
  /** A comma-separated list of single-quoted TypeScript/JS string literals. */
139
200
  function quotedList(values) {
140
201
  return values.map(value => `'${value}'`).join(', ');
@@ -156,7 +217,7 @@ function columnsFor(composition) {
156
217
  }
157
218
  /**
158
219
  * A named import on one line while it fits the column budget, else one symbol
159
- * per line. Emitted files are supposed to pass a prettier check as-is, and the
220
+ * per line. Emitted files are supposed to pass a format check as-is, and the
160
221
  * generated-module import grows by one symbol per grammar.
161
222
  */
162
223
  function importList(symbols, from, columns, typeOnly = false) {
@@ -175,9 +236,13 @@ function arrayLiteral(elements, indent, columns) {
175
236
  * The FRAMEWORK packages each head adds, carrying no version: every
176
237
  * `@hydranium/*` pin is derived at scaffold time from {@link frameworkPin}, so
177
238
  * none of them can name a version its siblings were not published at.
239
+ *
240
+ * `base` is every head set's: the services every head serves from are composed
241
+ * the same with or without an LSP head.
178
242
  */
179
243
  const HEAD_FRAMEWORK_DEPENDENCIES = {
180
- lsp: ['@hydranium/core', '@hydranium/langium', '@hydranium/protocol'],
244
+ base: ['@hydranium/core', '@hydranium/langium', '@hydranium/protocol'],
245
+ lsp: [],
181
246
  data: ['@hydranium/data-server'],
182
247
  glsp: ['@hydranium/glsp-server']
183
248
  };
@@ -197,15 +262,22 @@ const HEAD_FRAMEWORK_DEPENDENCIES = {
197
262
  * `overrides` block exists to prevent — so a repo gate holds each entry here
198
263
  * against the manifest that declares it.
199
264
  *
200
- * `langium` is required by `lsp` even though hand-written code goes through the
265
+ * `langium` is required even though hand-written code goes through the
201
266
  * `@hydranium/langium` chokepoint, because `langium-cli` emits direct imports
202
- * into the generated files and regenerates them on every build.
267
+ * into the generated files and regenerates them on every build. The `vscode-*`
268
+ * packages are required peers of `@hydranium/core`, so they are `base` too.
203
269
  */
204
270
  const HEAD_THIRD_PARTY_DEPENDENCIES = {
205
- lsp: {
271
+ base: {
206
272
  langium: '4.3.1',
207
- 'vscode-languageserver': '~10.0.1'
273
+ // Exact, as are the protocol and transport it pins, so Langium and the
274
+ // framework share their copies; across copies, an error a handler throws
275
+ // or returns loses its code, and a later patch can pin another protocol.
276
+ 'vscode-languageserver': '10.0.1',
277
+ 'vscode-jsonrpc': '9.0.0',
278
+ 'vscode-languageserver-protocol': '3.18.1'
208
279
  },
280
+ lsp: {},
209
281
  data: {},
210
282
  glsp: {
211
283
  '@eclipse-glsp/graph': '2.7.0',
@@ -216,25 +288,79 @@ const HEAD_THIRD_PARTY_DEPENDENCIES = {
216
288
  'reflect-metadata': '~0.2.2'
217
289
  }
218
290
  };
219
- /** The dependency block for a head set, merged and sorted as npm writes it. */
291
+ /**
292
+ * What each head adds to the protocol member, which carries only what a
293
+ * client may install. Keyed by head because the `glsp` head implies a diagram,
294
+ * whose element ids extend GLSP's default type names.
295
+ */
296
+ const PROTOCOL_THIRD_PARTY_DEPENDENCIES = {
297
+ lsp: {},
298
+ data: {},
299
+ glsp: { '@eclipse-glsp/protocol': '2.7.0' }
300
+ };
301
+ /**
302
+ * The exact pins of `heads`, for a workspace root to declare too. npm installs a
303
+ * root's own dependencies at the top of the tree, so another member's version of
304
+ * one nests beneath that member instead of taking the place of the copy this
305
+ * package shares with the framework.
306
+ */
307
+ export function workspaceRootPins(heads) {
308
+ return [
309
+ ...Object.entries(HEAD_THIRD_PARTY_DEPENDENCIES.base),
310
+ ...heads.flatMap(head => [
311
+ ...Object.entries(HEAD_THIRD_PARTY_DEPENDENCIES[head]),
312
+ ...Object.entries(PROTOCOL_THIRD_PARTY_DEPENDENCIES[head])
313
+ ])
314
+ ]
315
+ .filter(([, version]) => /^\d+\.\d+\.\d+$/.test(version))
316
+ .sort(([left], [right]) => left.localeCompare(right));
317
+ }
318
+ /** Whether an npm version is 11.6.0 or later, the first that installs `vitest` 4.1's peer set; `11.6.0-rc.1` is earlier. */
319
+ function installsVitest41(npmVersion) {
320
+ const [release, prerelease] = (npmVersion ?? '0.0.0').split('-', 2);
321
+ const [major, minor, patch] = release.split('.').map(Number);
322
+ return major > 11 || (major === 11 && (minor > 6 || (minor === 6 && (patch > 0 || prerelease === undefined))));
323
+ }
324
+ /**
325
+ * Whether the scaffold holds `vitest` below 4.1, whose peer set crashes npm
326
+ * before 11.6 (every npm a Node 22 release bundles) on the first install. Only a
327
+ * workspace root declaring a newer npm lifts it, and only when `init` runs under
328
+ * an npm known to be newer: npm does not enforce `packageManager`.
329
+ */
330
+ function pinsVitest(composition) {
331
+ const workspace = composition.packaging.workspace;
332
+ return !(installsVitest41(workspace?.packageManagerNpm) && installsVitest41(workspace?.npmVersion));
333
+ }
334
+ /**
335
+ * The version a member pins a sibling member at: exact, at the version every
336
+ * member starts with. npm links a sibling only when the range admits its local
337
+ * version, and otherwise looks for it in the registry, where it was never
338
+ * published.
339
+ */
340
+ const MEMBER_PIN = '0.0.0';
341
+ /** A dependency block, sorted as npm writes it. */
342
+ function sortedBlock(dependencies, indent) {
343
+ return Object.keys(dependencies)
344
+ .sort()
345
+ .map(name => `${indent}"${name}": "${dependencies[name]}"`)
346
+ .join(',\n');
347
+ }
348
+ /** The server's dependency block: the base and head set's packages and the protocol member. */
220
349
  function dependencyBlock(composition, indent) {
221
- const merged = {};
222
- for (const head of composition.heads) {
350
+ const merged = { [memberPackageName(composition, 'protocol')]: MEMBER_PIN };
351
+ for (const head of ['base', ...composition.heads]) {
223
352
  Object.assign(merged, HEAD_THIRD_PARTY_DEPENDENCIES[head]);
224
353
  for (const framework of HEAD_FRAMEWORK_DEPENDENCIES[head]) {
225
354
  merged[framework] = frameworkPin(composition);
226
355
  }
227
356
  }
228
- return Object.keys(merged)
229
- .sort()
230
- .map(name => `${indent}"${name}": "${merged[name]}"`)
231
- .join(',\n');
357
+ return sortedBlock(merged, indent);
232
358
  }
233
359
  // JSON at two-space indent, not the three the `.ts` templates use: that is the
234
360
  // npm convention, and `npm install` rewrites `package.json` at two spaces
235
361
  // regardless — so three would not survive the adopter's first install.
236
362
  //
237
- // The KEY ORDER is `prettier-plugin-packagejson`'s canonical one, `private`
363
+ // The KEY ORDER is the one Oxfmt's `sortPackageJson` canonicalises to, `private`
238
364
  // before `license` included. Any other order is rewritten by the scaffolded
239
365
  // project's own first `format` run — a diff on a file the adopter never touched
240
366
  // — and it makes a byte-compare against a formatted copy of this emission
@@ -254,6 +380,11 @@ function dependencyBlock(composition, indent) {
254
380
  // from this package's own grammar and can reach a consumer from nowhere else; a
255
381
  // listed path that does not exist yet is inert.
256
382
  //
383
+ // `exports` puts `createServices`, the one module reading the real filesystem,
384
+ // behind `./node`, so the root loads in a browser worker that has no `node:fs`.
385
+ // `./lib/*` and `./package.json` stay reachable for a host that resolves
386
+ // `<server>/lib/main.js` to spawn the server.
387
+ //
257
388
  // `repository` is deliberately NOT emitted while `author` is emitted empty. An
258
389
  // empty `author` is the form `npm init` itself writes and asserts nothing, but
259
390
  // `repository` is CONSUMED — the registry page, `npm repo` and publish
@@ -265,16 +396,9 @@ const PACKAGE_JSON = `{
265
396
  "version": "0.0.0",
266
397
  __PRIVATE__ "description": "__NAME__ language server, built with the Hydranium framework.",
267
398
  "keywords": [
268
- "hydranium",
269
- "langium",
270
- "language-server",
271
- "__PROJECT_ID__"
272
- ],
399
+ __KEYWORDS__ ],
273
400
  "license": "UNLICENSED",
274
401
  "author": "",
275
- "type": "module",
276
- "main": "lib/index.js",
277
- "types": "lib/index.d.ts",
278
402
  "bin": {
279
403
  __BIN__ },
280
404
  "files": [
@@ -282,17 +406,32 @@ __BIN__ },
282
406
  "src",
283
407
  "syntaxes"
284
408
  ],
409
+ "type": "module",
410
+ "main": "lib/index.js",
411
+ "types": "lib/index.d.ts",
412
+ "exports": {
413
+ ".": {
414
+ "types": "./lib/index.d.ts",
415
+ "default": "./lib/index.js"
416
+ },
417
+ "./node": {
418
+ "types": "./lib/node/services.d.ts",
419
+ "default": "./lib/node/services.js"
420
+ },
421
+ "./lib/*": "./lib/*",
422
+ "./package.json": "./package.json"
423
+ },
285
424
  "scripts": {
286
- "build": "npm run generate && tsc",
425
+ "build": "npm run generate && tsc -b",
287
426
  "clean": "rimraf lib syntaxes src/language-server/generated src/language-server/generated-hydranium tsconfig.tsbuildinfo",
288
427
  "generate": "npm run langium:generate && npm run generate:transfer-model",
289
- "generate:transfer-model": "hydranium-cli generate-transfer-model --ast-file src/language-server/generated/ast.ts --augmentation-file src/language-server/ast.ts --out-file src/language-server/generated-hydranium/transfer-model.ts --ast-builder-file src/language-server/generated-hydranium/ast-builder.ts --element-type-name __NAME__Element --terminals-name __NAME__Terminals --regen-command \\"Run: __NPM_RUN__ generate:transfer-model\\"",
428
+ "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 ../protocol/src/generated-hydranium/transfer-model.ts --ast-builder-file src/language-server/generated-hydranium/ast-builder.ts --languages-file ../protocol/src/generated-hydranium/languages.ts --element-type-name __NAME__Element --terminals-name __NAME__Terminals --regen-command \\"Run: __NPM_RUN__ generate:transfer-model\\"",
290
429
  "langium:generate": "langium generate",
291
430
  "langium:watch": "langium generate --watch",
292
- __LINT__ "start": "node lib/main.js --stdio",
431
+ __LINT__ "start": "node lib/main.js__START_ARGS__",
293
432
  "test": "npm run typecheck:test && vitest run",
294
433
  "typecheck:test": "tsc --noEmit -p tsconfig.test.json",
295
- "watch": "tsc --watch"
434
+ "watch": "tsc -b --watch"
296
435
  },
297
436
  "dependencies": {
298
437
  __DEPENDENCIES__
@@ -302,8 +441,8 @@ __DEPENDENCIES__
302
441
  "@types/node": "^22.0.0",
303
442
  "langium-cli": "4.3.0",
304
443
  "rimraf": "^5.0.0",
305
- "typescript": "^5.8.0",
306
- "vitest": "^4.0.0"
444
+ "typescript": "^7.0.2",
445
+ "vitest": "__VITEST__"
307
446
  },
308
447
  "engines": {
309
448
  "node": ">=22.13"
@@ -311,16 +450,17 @@ __DEPENDENCIES__
311
450
  }
312
451
  `;
313
452
  /**
314
- * How the scaffold names `npm run` when talking about itself.
453
+ * How the scaffold names `npm run` for one member's script.
315
454
  *
316
- * A workspace member's scripts are routinely invoked from the repo root, where
317
- * a bare `npm run` reaches the ROOT manifest and fails — so the regen hint a
318
- * generated file prints has to name the package, or it sends every reader who
319
- * follows it to the wrong place.
455
+ * Always with `--prefix`, from the directory commands are run in: the
456
+ * workspace root for a member, the project root for a standalone project. A
457
+ * bare `npm run` there reaches the ROOT manifest and fails, and the regen hint
458
+ * a generated file prints is read in `protocol/` as well, where the script it
459
+ * names does not exist.
320
460
  */
321
- function npmRun(composition) {
461
+ function npmRun(composition, member) {
322
462
  const workspace = composition.packaging.workspace;
323
- return workspace === undefined ? 'npm run' : `npm --prefix ${workspace.targetPath} run`;
463
+ return `npm --prefix ${workspace === undefined ? member : `${workspace.targetPath}/${member}`} run`;
324
464
  }
325
465
  /**
326
466
  * The `bin` targets, one per executable entry the head set emits.
@@ -331,7 +471,7 @@ function npmRun(composition) {
331
471
  * name. Without the second key a scaffolded project has no command line that
332
472
  * reaches its own data head at all.
333
473
  *
334
- * Alphabetical, because `prettier-plugin-packagejson` sorts `bin` and any other
474
+ * Alphabetical, because Oxfmt's `sortPackageJson` sorts `bin` and any other
335
475
  * order is rewritten by the scaffolded project's first `format` run.
336
476
  */
337
477
  function binBlock(composition) {
@@ -341,21 +481,37 @@ function binBlock(composition) {
341
481
  }
342
482
  return entries.map(([name, target]) => ` "${name}": "${target}"`).join(',\n') + '\n';
343
483
  }
344
- /** `package.json` with the name, the private flag and the dependency block all derived. */
484
+ /**
485
+ * A member's `lint` script line, over `directories`, or nothing.
486
+ *
487
+ * Emitted only where a root lint config says the repo lints. The absence is
488
+ * the dangerous direction: a task runner runs a script only where one is
489
+ * declared, so a package with no `lint` is SKIPPED rather than reported, and
490
+ * that reads as a clean lint. A wrong invocation is one visible line to edit.
491
+ */
492
+ function lintScript(member, directories) {
493
+ const workspace = member.packaging.workspace;
494
+ if (workspace?.oxlintConfig !== undefined) {
495
+ const config = `${'../'.repeat(workspace.targetPath.split('/').length)}${workspace.oxlintConfig}`;
496
+ return ` "lint": "oxlint --report-unused-disable-directives --format stylish --config ${config} ${directories} --max-warnings 0",\n`;
497
+ }
498
+ return workspace?.eslintConfig !== undefined ? ` "lint": "eslint ${directories} --max-warnings 0",\n` : '';
499
+ }
500
+ /** The server's `package.json`, with the name, the private flag and the dependency block all derived. */
345
501
  function packageJson(composition) {
346
- const { scope } = composition.packaging;
347
- const packageName = scope === undefined ? composition.projectId : `${scope}/${composition.projectId}`;
348
- // Emitted only where a root eslint config says the repo lints. The absence is
349
- // the dangerous direction: a task runner runs a script only where one is
350
- // declared, so a package with no `lint` is SKIPPED rather than reported, and
351
- // that reads as a clean lint. A wrong invocation is one visible line to edit.
352
- const lint = composition.packaging.workspace?.eslintConfig === undefined ? '' : ' "lint": "eslint src test --max-warnings 0",\n';
502
+ const keywords = ['hydranium', 'langium', 'language-server', composition.projectId]
503
+ .sort()
504
+ .map(keyword => ` ${JSON.stringify(keyword)}`)
505
+ .join(',\n') + '\n';
353
506
  return project(PACKAGE_JSON, composition)
354
- .replace('__PACKAGE_NAME__', packageName)
507
+ .replace('__PACKAGE_NAME__', memberPackageName(composition, 'server'))
508
+ .replace('__KEYWORDS__', keywords)
355
509
  .replace('__BIN__', binBlock(composition))
356
- .replace('__LINT__', lint)
510
+ .replace('__LINT__', lintScript(forMember(composition, 'server'), 'src test'))
511
+ .replace('__START_ARGS__', composition.heads.includes('lsp') ? ' --stdio' : ' ../workspace')
512
+ .replace('__VITEST__', pinsVitest(composition) ? '~4.0.18' : '^4.0.0')
357
513
  .replace('__PRIVATE__', composition.packaging.private ? ' "private": true,\n' : '')
358
- .replace('__NPM_RUN__', npmRun(composition))
514
+ .replace('__NPM_RUN__', npmRun(composition, 'server'))
359
515
  .replace('__FRAMEWORK_PIN__', frameworkPin(composition))
360
516
  .replace('__DEPENDENCIES__', dependencyBlock(composition, ' '));
361
517
  }
@@ -406,6 +562,11 @@ const TSCONFIG_COMPILER_OPTIONS = [
406
562
  ['outDir', 'lib'],
407
563
  ['strict', true],
408
564
  ['esModuleInterop', true],
565
+ // A server that renders its own user-facing messages loads a catalogue, and
566
+ // a catalogue is JSON. Without this the import does not resolve at all, and
567
+ // without the `include` glob below a composite project rejects it with
568
+ // TS6307 naming neither cause.
569
+ ['resolveJsonModule', true],
409
570
  ['skipLibCheck', true],
410
571
  ['declaration', true],
411
572
  ['experimentalDecorators', true],
@@ -413,6 +574,23 @@ const TSCONFIG_COMPILER_OPTIONS = [
413
574
  ['forceConsistentCasingInFileNames', true],
414
575
  ['types', ['node']]
415
576
  ];
577
+ /** The protocol member's compiler options, pruned against a workspace base like the server's. */
578
+ const PROTOCOL_COMPILER_OPTIONS = [
579
+ ['target', 'ES2022'],
580
+ ['lib', ['ES2022']],
581
+ ['module', 'NodeNext'],
582
+ ['moduleResolution', 'NodeNext'],
583
+ ['rootDir', 'src'],
584
+ ['outDir', 'lib'],
585
+ ['strict', true],
586
+ ['skipLibCheck', true],
587
+ ['declaration', true],
588
+ // The server's `tsc -b` builds this project through a reference, which
589
+ // `tsc` refuses unless the referenced project is composite.
590
+ ['composite', true],
591
+ // No ambient Node types: a browser client compiles against this package.
592
+ ['types', []]
593
+ ];
416
594
  /**
417
595
  * The options that describe where THIS package's own files live. Never pruned,
418
596
  * and emitted first in the workspace case: a base config that supplied them
@@ -420,9 +598,19 @@ const TSCONFIG_COMPILER_OPTIONS = [
420
598
  * base is wrong rather than that the member is redundant.
421
599
  */
422
600
  const PACKAGE_LOCAL_OPTIONS = ['rootDir', 'outDir'];
423
- /** Compare two JSON option values. Sound here because compiler options are scalars and flat arrays. */
424
- function jsonEquals(left, right) {
425
- return JSON.stringify(left) === JSON.stringify(right);
601
+ /**
602
+ * The enum-valued options of {@link TSCONFIG_COMPILER_OPTIONS}, whose values
603
+ * `tsc` reads case-insensitively, so `nodenext` and `NodeNext` are one value.
604
+ * An enum-valued option added there belongs here too.
605
+ */
606
+ const CASE_INSENSITIVE_OPTIONS = ['target', 'lib', 'module', 'moduleResolution'];
607
+ /** Compare two option values. Sound here because compiler options are scalars and flat arrays. */
608
+ function optionEquals(key, left, right) {
609
+ const normalise = (value) => {
610
+ const text = JSON.stringify(value);
611
+ return CASE_INSENSITIVE_OPTIONS.includes(key) ? text?.toLowerCase() : text;
612
+ };
613
+ return normalise(left) === normalise(right);
426
614
  }
427
615
  /**
428
616
  * `tsconfig.json`, extending the workspace's base config when there is one.
@@ -431,104 +619,41 @@ function jsonEquals(left, right) {
431
619
  * the same key: inheriting `"module": "CommonJS"` where the scaffold needs
432
620
  * `NodeNext` would break every emitted import, so a differing base value is a
433
621
  * reason to keep the override rather than to trust the parent.
622
+ *
623
+ * `member` is the composition as that member sees it, so `extends` reaches the
624
+ * workspace base from the member's own directory.
434
625
  */
435
- function tsconfigJson(composition) {
436
- const workspace = composition.packaging.workspace;
626
+ function tsconfigJson(member, compilerOptions, include, references = '') {
627
+ const workspace = member.packaging.workspace;
437
628
  const base = workspace?.baseTsconfig === undefined ? undefined : (workspace.baseCompilerOptions ?? {});
438
629
  const options = base === undefined
439
- ? TSCONFIG_COMPILER_OPTIONS
630
+ ? compilerOptions
440
631
  : [
441
- ...TSCONFIG_COMPILER_OPTIONS.filter(([key]) => PACKAGE_LOCAL_OPTIONS.includes(key)),
442
- ...TSCONFIG_COMPILER_OPTIONS.filter(([key, value]) => !PACKAGE_LOCAL_OPTIONS.includes(key) && !jsonEquals(base[key], value))
632
+ ...compilerOptions.filter(([key]) => PACKAGE_LOCAL_OPTIONS.includes(key)),
633
+ ...compilerOptions.filter(([key, value]) => !PACKAGE_LOCAL_OPTIONS.includes(key) && !optionEquals(key, base[key], value))
443
634
  ];
444
635
  const extendsLine = workspace?.baseTsconfig === undefined ? '' : ` "extends": "${workspace.baseTsconfig}",\n`;
445
636
  const body = options.map(([key, value]) => ` "${key}": ${JSON.stringify(value)}`).join(',\n');
446
- return `{\n${extendsLine} "compilerOptions": {\n${body}\n },\n "include": ["src"]\n}\n`;
447
- }
448
- // `isolatedModules` is what makes this check agree with the transform that
449
- // actually runs the tests. `npm test` is `typecheck:test && vitest run`, and
450
- // vitest compiles through esbuild — a per-file transform with no type
451
- // information, which cannot tell a re-exported TYPE from a re-exported value.
452
- // Without the flag `tsc` accepts `export { SomeType } from './x'` that esbuild
453
- // then emits as a real import of a symbol that does not exist at runtime, so
454
- // the typecheck passes and the suite fails with a confusing missing-export.
455
- const TSCONFIG_TEST = `{
456
- "extends": "./tsconfig.json",
457
- "compilerOptions": {
458
- "rootDir": ".",
459
- "noEmit": true,
460
- "isolatedModules": true,
461
- "types": ["node"]
462
- },
463
- "include": ["src", "test"]
637
+ const referencesLine = references === '' ? '' : `,\n "references": ${references}`;
638
+ return `{\n${extendsLine} "compilerOptions": {\n${body}\n },\n "include": ${include}${referencesLine}\n}\n`;
464
639
  }
465
- `;
466
- const VITEST_CONFIG = `import { defineConfig } from 'vitest/config';
467
-
468
- // Vitest transforms TypeScript itself and resolves \`.js\` specifiers to their
469
- // \`.ts\` source, so tests import from \`../src/\` with the same specifiers the
470
- // compiled output uses. \`include\` is scoped to \`test/\` so \`lib/\` is never
471
- // scanned; \`npm test\` runs \`typecheck:test\` first, because the esbuild
472
- // transform strips types without checking them.
473
-
474
- export default defineConfig({
475
- test: {
476
- environment: 'node',
477
- include: ['test/**/*.{test,spec}.ts']
478
- }
479
- });
480
- `;
481
- const GITIGNORE = `node_modules/
482
- lib/
483
- *.tsbuildinfo
484
-
485
- # Langium-generated TextMate grammar (rewritten by every \`langium generate\`).
486
- syntaxes/
487
- `;
488
640
  /**
489
- * The workspace-member `.gitignore` — one entry, and the one entry a root
490
- * cannot be assumed to have.
491
- *
492
- * A member inherits the root's rules, and `node_modules/`, `lib/` and
493
- * `*.tsbuildinfo` are in every monorepo root already, so repeating them here
494
- * would be three lines to keep in step for no coverage. `syntaxes/` is not like
495
- * them: it is a Langium artefact, so a root that has never held a Langium
496
- * package has no rule for it, and the first `langium generate` then offers
497
- * generated output up for commit with nothing to warn the adopter. Dropping the
498
- * whole file rather than this subset is the mistake this template exists to
499
- * undo.
641
+ * The server's `tsconfig.json`. It references the protocol member, so `tsc -b`
642
+ * compiles protocol first, after `generate` has written its generated modules.
500
643
  */
501
- const GITIGNORE_WORKSPACE_MEMBER = `# The workspace root already covers \`node_modules/\`, \`lib/\` and \`*.tsbuildinfo\`.
502
- # It has no reason to know about this one: the TextMate grammar is a Langium
503
- # artefact, rewritten by every \`langium generate\`, so a root that has never held
504
- # a Langium package ignores nothing here and the generated file is offered for
505
- # commit.
506
- syntaxes/
507
- `;
508
- /** The terminals every grammar needs, shared verbatim by the fragment and the lone-grammar case. */
509
- const TERMINALS = `hidden terminal WS: /\\s+/;
510
- terminal ID: /[_a-zA-Z][\\w_]*/;
511
- hidden terminal SL_COMMENT: /\\/\\/[^\\n\\r]*/;
512
- hidden terminal ML_COMMENT: /\\/\\*[\\s\\S]*?\\*\\//;
513
- `;
644
+ function serverTsconfigJson(composition) {
645
+ return tsconfigJson(forMember(composition, 'server'), TSCONFIG_COMPILER_OPTIONS, '["src", "src/**/*.json"]', '[{ "path": "../protocol" }]');
646
+ }
514
647
  /**
515
- * The shared lexical fragment, emitted only for a multi-grammar project.
516
- *
517
- * The alternative — every grammar declaring its own `WS` / `ID` — lets the token
518
- * sets drift apart.
648
+ * The terminals every grammar needs: the shared fragment's text after its
649
+ * leading comment run, so a lone grammar declares the same set.
519
650
  */
520
- const COMMON_GRAMMAR = `// Shared lexical basis for this project's grammars. Imported, never registered:
521
- // it has no entry rule and no \`langium-config.json\` entry, so it is a fragment
522
- // rather than a language.
523
- //
524
- // Keeping the tokens here rather than in one of the languages means a grammar
525
- // imports \`common\` for its TOKENS and another language only when it needs that
526
- // language's TYPES — separate reasons that are worth keeping separate.
527
-
528
- ${TERMINALS}`;
651
+ function terminals() {
652
+ return template('server/src/grammar/common.langium').replace(/^(\/\/.*\n)*\n/, '');
653
+ }
529
654
  /** One starter grammar. `shared` decides whether it imports the fragment or declares its own terminals. */
530
655
  function grammarFile(shared) {
531
- const lexis = shared ? "import './common'\n" : `\n${TERMINALS}`;
656
+ const lexis = shared ? "import './common'\n" : `\n${terminals()}`;
532
657
  return `grammar __GRAMMAR__
533
658
  ${shared ? '\n' + lexis : ''}
534
659
  // A minimal starter grammar — replace with your own language. \`__NODE_RULE__\` is
@@ -696,179 +821,89 @@ ${returnType}
696
821
  `;
697
822
  return project(text, composition);
698
823
  }
699
- const SERVICES = `// Zero-arg service factory for the headless \`hydranium-cli\` tooling: the
700
- // \`reflect\` / \`lint-grammar\` / \`validate\` subcommands import this via
701
- // \`--services ./lib/services.js\`. The head wires its own filesystem here.
702
- //
703
- // The contract is language-count-agnostic — a second grammar needs no edit here.
704
-
705
- import { NodeFileSystem } from '@hydranium/core/node';
706
- import { create__NAME__Services } from './language-server/__PROJECT_ID__-module.js';
707
-
708
- export function createServices(): ReturnType<typeof create__NAME__Services> {
709
- return create__NAME__Services({ ...NodeFileSystem });
710
- }
711
- `;
712
- const AST = `// The language's AST entry point: a re-export of what \`langium generate\`
713
- // emits, plus anywhere you augment those types.
714
- //
715
- // Import the AST from HERE rather than from \`./generated/ast.js\`, so any
716
- // augmentation below travels with every import. One \`langium-cli\` run over N
717
- // grammars emits ONE combined AST module sharing one reflection, so a further
718
- // grammar needs no change here.
719
- //
720
- // \`generate:transfer-model\` reads this file as its \`--augmentation-file\`: the
721
- // generated wire types are derived from the AST *as augmented*, not from the
722
- // raw generated module. Augment a type here and the transfer model follows.
723
- //
724
- // A \`@derived\` property is computed at build time rather than parsed, so it is
725
- // declared here and populated by an AST-extension contribution:
726
- //
727
- // declare module './generated/ast.js' {
728
- // interface __ENTRY_RULE__ {
729
- // /** @derived Populated by an \`ast.extensions.computedProperties\` contribution. */
730
- // readonly _summary?: string;
731
- // }
732
- // }
733
-
734
- export * from './generated/ast.js';
735
- `;
736
824
  /**
737
- * The starter serializer — the concrete-syntax emitter for ONE grammar.
825
+ * How a host finds each socket head's port, in emission order: an LSP request
826
+ * when the LSP head publishes the ports, else the environment variable
827
+ * `main.ts` reads a port from.
738
828
  *
739
- * Emitted per grammar, not per project, because `services.serializer.Serializer`
740
- * is a per-language slot and a serializer is grammar-shaped by definition. That
741
- * is also why the framework refuses to default it: its `UnboundSerializer`
742
- * throws, naming the binding to add, rather than guessing a syntax.
743
- */
744
- const SERIALIZER = `// Concrete-syntax emitter for __GRAMMAR__ — the parser's inverse, turning a
745
- // model back into text this grammar accepts. Derived from the starter grammar
746
- // exactly as \`generated/ast.ts\` is, so replacing the grammar replaces this too.
747
- //
748
- // Without it every structured write fails: the framework's default binding at
749
- // \`services.serializer.Serializer\` THROWS, because a concrete syntax is
750
- // language knowledge no framework can derive. \`ModelService.update\` / \`save\`,
751
- // the data head's \`saveModelDocument\` and a GLSP \`SaveModelAction\` all reach
752
- // it.
753
- //
754
- // \`AbstractSerializer\`'s generic property walk lays out FORMAT-structured
755
- // output — its YAML and JSON subclasses are what it exists for — and cannot
756
- // produce a keyword-delimited syntax like \`node a -> b\`. So \`serializeNode\` is
757
- // a per-\`$type\` emitter here and the two array hooks are unreachable.
758
-
759
- import { AbstractSerializer } from '@hydranium/core';
760
- import type { AstNode } from '@hydranium/langium';
761
- import { type __ENTRY_RULE__, type __NODE_RULE__, is__ENTRY_RULE__, is__NODE_RULE__ } from './ast.js';
762
-
763
- export class __GRAMMAR__Serializer extends AbstractSerializer<__ENTRY_RULE__> {
764
- protected override serializeNode(node: AstNode | Record<string, unknown>): string {
765
- if (is__ENTRY_RULE__(node)) {
766
- return node.nodes.map(child => this.emitNode(child)).join('\\n');
767
- }
768
- if (is__NODE_RULE__(node)) {
769
- return this.emitNode(node);
770
- }
771
- // Defensive: a rule added to the grammar with no emitter added here.
772
- throw new Error(\`__GRAMMAR__Serializer: no emitter for $type \${(node as AstNode).$type}\`);
773
- }
774
-
775
- /** Unreachable — this grammar's one list is emitted by its \`__ENTRY_RULE__\` parent. */
776
- protected override serializeArray(): string {
777
- throw new Error('__GRAMMAR__Serializer: arrays are emitted by the per-$type parent, not the generic dispatch.');
778
- }
779
-
780
- /** Unreachable — same reasoning as {@link serializeArray}. */
781
- protected override serializeReferenceArray(): string {
782
- throw new Error('__GRAMMAR__Serializer: reference arrays are emitted by the per-$type parent, not the generic dispatch.');
783
- }
784
-
785
- /**
786
- * \`serializeReferenceText\` rather than \`node.target?.$refText\`: it is the one
787
- * read that spans BOTH input shapes. A transfer model reaching
788
- * \`serializeTransfer\` carries \`target\` as a plain string rather than a
789
- * \`Reference\`, and a serializer that reaches for \`$refText\` directly emits
790
- * the AST correctly and drops every reference on the transfer path.
791
- */
792
- private emitNode(node: __NODE_RULE__): string {
793
- const target = this.serializeReferenceText(node.target);
794
- return target === undefined ? \`node \${node.name}\` : \`node \${node.name} -> \${target}\`;
795
- }
796
- }
797
- `;
798
- /**
799
- * The exported constant names for the head set, in emission order. Both are
800
- * keyed by the PROJECT rather than by a language: one server of each kind per
829
+ * Keyed by the PROJECT rather than by a language: one server of each kind per
801
830
  * process serves every registered grammar, so a language-derived name would tie
802
831
  * a project-level endpoint to whichever grammar was scaffolded first.
803
832
  */
804
- function portCommandNames(composition) {
833
+ function headPortConstants(composition) {
805
834
  const upper = screamingSnake(composition.projectId);
806
- return [
807
- ...(composition.heads.includes('data') ? [`${upper}_DATA_SERVER_PORT_COMMAND`] : []),
808
- ...(composition.heads.includes('glsp') ? [`${upper}_GLSP_PORT_COMMAND`] : [])
835
+ const lsp = composition.heads.includes('lsp');
836
+ const heads = [
837
+ ...(composition.heads.includes('data') ? [{ key: 'DATA_SERVER', path: 'data-server', label: 'data-server' }] : []),
838
+ ...(composition.heads.includes('glsp') ? [{ key: 'GLSP_SERVER', path: 'glsp-server', label: 'GLSP' }] : [])
809
839
  ];
840
+ return heads.map(head => lsp
841
+ ? {
842
+ name: `${upper}_${head.key}_PORT_COMMAND`,
843
+ value: `${composition.projectId}/${head.path}/port`,
844
+ doc: `LSP request the host queries to discover the ${head.label} socket port.`
845
+ }
846
+ : {
847
+ name: `${upper}_${head.key}_PORT_ENV`,
848
+ value: `${upper}_${head.key}_PORT`,
849
+ doc: `Environment variable naming the ${head.label} socket port when no \`--${head.path}-port\` flag does.`
850
+ });
810
851
  }
811
852
  /**
812
- * The socket heads' discovery commands, as an importable module.
853
+ * The socket heads' port commands or variables, in the protocol member.
813
854
  *
814
- * Deliberately NOT declared in `main.ts`. A host shell has to name the same
815
- * string to reach the head, and `main.ts` opens a connection at module scope,
816
- * so importing a constant from it would start a server. A host that retypes the
817
- * literal instead gets no error when it drifts: the framework's port poll
818
- * defaults to `findPortAttempts = -1`, so a wrong command retries forever
819
- * rather than failing.
855
+ * A host shell has to name the same string the server reads, and reaches it
856
+ * here without depending on the server. A host that retypes the literal
857
+ * instead gets no error when it drifts: the framework's port poll defaults to
858
+ * `findPortAttempts = -1`, so a wrong command retries forever rather than
859
+ * failing, and a wrong variable is never read.
820
860
  */
821
861
  function headPortsFile(composition) {
822
- const upper = screamingSnake(composition.projectId);
823
- const data = composition.heads.includes('data');
824
- const glsp = composition.heads.includes('glsp');
825
- const blocks = [
826
- ...(data
827
- ? [
828
- `/** LSP request the host queries to discover the data-server socket port. */\nexport const ${upper}_DATA_SERVER_PORT_COMMAND = '${composition.projectId}/data-server/port';`
829
- ]
830
- : []),
831
- ...(glsp
832
- ? [
833
- `/** LSP request the host queries to discover the GLSP socket port. */\nexport const ${upper}_GLSP_PORT_COMMAND = '${composition.projectId}/glsp/port';`
834
- ]
835
- : [])
836
- ];
837
- return `// The LSP requests a host queries to discover this server's socket head${blocks.length > 1 ? 's' : ''}.
862
+ const constants = headPortConstants(composition);
863
+ const blocks = constants.map(constant => `/** ${constant.doc} */\nexport const ${constant.name} = '${constant.value}';`);
864
+ const heads = `socket head${blocks.length > 1 ? 's' : ''}`;
865
+ const opening = composition.heads.includes('lsp')
866
+ ? `// The LSP requests a host queries to discover this server's ${heads}.`
867
+ : `// The environment variables that name the ports of this server's ${heads}.`;
868
+ const usage = composition.heads.includes('lsp')
869
+ ? `// The server publishes them and a host shell has to name the same string to
870
+ // reach the head; both import them from here. A shell that retypes the literal
871
+ // gets no error when it drifts: the framework's port poll retries indefinitely
872
+ // by default.`
873
+ : `// The server reads them and a process that starts it sets them; both import
874
+ // them from here. A variable retyped with a typo is never read, and the head
875
+ // then listens on whichever port is free.`;
876
+ return `${opening}
838
877
  //
839
- // Both are keyed by the PROJECT, not the language: one server of each kind per
878
+ // ${blocks.length > 1 ? 'Both are' : 'It is'} keyed by the PROJECT, not the language: one server of each kind per
840
879
  // process serves every registered grammar, so a language-derived name would tie
841
880
  // a project-level endpoint to whichever grammar was scaffolded first.
842
881
  //
843
- // They live here rather than in \`main.ts\` because a host shell has to name the
844
- // same string to reach the head, and \`main.ts\` is an executable entry — nothing
845
- // can import from it. A shell that retypes the literal gets no error when it
846
- // drifts: the framework's port poll retries indefinitely by default. Import
847
- // these instead, and assert any host-side copy against them.
882
+ ${usage}
848
883
 
849
884
  ${blocks.join('\n\n')}
850
885
  `;
851
886
  }
852
- const INDEX = `// Public surface of the __NAME__ language server: the DI factory, the generated
853
- // AST, and the headless \`createServices\` entry.
887
+ /**
888
+ * The server's root entry: the DI factory, the generated AST and each diagram
889
+ * module, all loadable in a browser worker.
890
+ */
891
+ function indexFile(composition) {
892
+ const diagrams = composition.grammars.filter(grammar => grammar.diagram);
893
+ const diagramPhrase = diagrams.length === 0 ? '' : diagrams.length === 1 ? ', plus the diagram module' : ', plus the diagram modules';
894
+ const text = `// Public surface of the __NAME__ language server: the DI factory and the
895
+ // generated AST${diagramPhrase}.
854
896
  //
855
- // The entry points under \`src/\` are deliberately NOT re-exported. Each opens a
856
- // transport at module scope, so importing one starts a server as a side effect
857
- // — which is why they are \`bin\` targets and this file is \`main\`.
897
+ // Everything here loads in a browser worker as well as in Node, so a host can
898
+ // compose the language wherever it runs it. \`createServices\`, which reads the
899
+ // real filesystem, is the package's \`./node\` entry instead. The entry points
900
+ // under \`src/\` are not re-exported either: each opens a transport at module
901
+ // scope, so importing one starts a server, which is why they are \`bin\` targets.
858
902
 
859
903
  export * from './language-server/__PROJECT_ID__-module.js';
860
904
  export * from './language-server/ast.js';
861
- export { createServices } from './services.js';
862
- `;
863
- /**
864
- * The public surface, plus the head-port commands when there is a socket head.
865
- *
866
- * The commands belong here rather than only in `head-ports.ts` so a host shell
867
- * reaches them from the package root, the same way it reaches `createServices`.
868
- */
869
- function indexFile(composition) {
870
- const base = project(INDEX, composition);
871
- return portCommandNames(composition).length ? `${base}export * from './head-ports.js';\n` : base;
905
+ ${diagrams.map(grammar => `export * from './glsp/${grammar.grammarId}/diagram-module.js';\n`).join('')}`;
906
+ return project(text, composition);
872
907
  }
873
908
  /**
874
909
  * The `#!` line every emitted `bin` target carries.
@@ -896,10 +931,12 @@ function mainFile(composition) {
896
931
  const columns = columnsFor(composition);
897
932
  const roots = [...composition.grammars.map(grammar => grammar.entryRule)].sort();
898
933
  const upper = screamingSnake(composition.projectId);
934
+ const lsp = composition.heads.includes('lsp');
899
935
  const data = composition.heads.includes('data');
900
936
  const glsp = composition.heads.includes('glsp');
901
937
  const diagrams = composition.grammars.filter(grammar => grammar.diagram);
902
938
  const plural = roots.length > 1 ? 's' : '';
939
+ const portFlags = [...(data ? ['--data-server-port'] : []), ...(glsp ? ['--glsp-server-port'] : [])];
903
940
  const headNames = composition.heads.map(head => head === 'lsp' ? 'the Langium LSP head' : head === 'data' ? 'a socket data-server (model server) head' : 'a GLSP head');
904
941
  // Comma-joined but for the last, which takes `and`: a bare `, ` join reads as
905
942
  // a truncated list, and the emitted comment is the first thing an adopter
@@ -908,7 +945,7 @@ function mainFile(composition) {
908
945
  // `reflect-metadata` must be imported before anything that reads a
909
946
  // decorator's emitted metadata, which inversify does at module scope.
910
947
  const reflectImport = glsp ? "import 'reflect-metadata';\n" : '';
911
- const coreNodeSymbols = ['NodeFileSystem', ...(data ? ['publishPortOnLspConnection', 'startSocketServer'] : [])];
948
+ const coreNodeSymbols = ['NodeFileSystem', ...(data ? [...(lsp ? ['publishPortOnLspConnection'] : []), 'startSocketServer'] : [])];
912
949
  const glspImports = glsp
913
950
  ? `import { ServerModule } from '@eclipse-glsp/server/node.js';
914
951
  import { GlspClientLogger, HydraniumGlspAppModule } from '@hydranium/glsp-server';
@@ -925,44 +962,57 @@ import { startGlspServer } from '@hydranium/glsp-server/node';
925
962
  // \`TransferElement\` structurally, so naming the AST type here compiles fine and
926
963
  // silently tells every typed client that a reference is a resolvable object
927
964
  // rather than a name.
928
- ${importList(roots, './language-server/generated-hydranium/transfer-model.js', columns, true)}
965
+ ${importList(roots, memberPackageName(composition, 'protocol'), columns, true)}
929
966
  `
930
967
  : '';
931
- // The commands live in `head-ports.ts`, not here: a host shell has to name
932
- // the same string to reach the head, and this file is an executable entry, so
933
- // nothing can import from it.
934
- const portCommands = portCommandNames(composition).length
935
- ? importList(portCommandNames(composition), './head-ports.js', columns) + '\n'
936
- : '';
937
- const dataBlock = data
938
- ? `
968
+ // The names live in the protocol member, not here: a host shell has to name
969
+ // the same string to reach the head, and this file is an executable entry,
970
+ // so nothing can import from it.
971
+ const portNames = headPortConstants(composition).map(constant => constant.name);
972
+ const portImports = portNames.length ? importList(portNames, memberPackageName(composition, 'protocol'), columns) + '\n' : '';
973
+ const dataBlock = !data
974
+ ? ''
975
+ : !lsp
976
+ ? `
977
+ // Data-server head: each accepted client gets its own DataServer.
978
+ const dataServer = startSocketServer(
979
+ { shared, workspace, port: portFrom('data-server-port', ${upper}_DATA_SERVER_PORT_ENV), logTag: 'ModelServer', logger: shared.Logger },
980
+ dataConnection => {
981
+ new DataServer<${roots.join(' | ')}>(dataConnection, shared);
982
+ return { dispose: () => undefined };
983
+ }
984
+ );
985
+ `
986
+ : `
939
987
  // Data-server head alongside LSP: binds an ephemeral port, published over the LSP
940
988
  // connection for the host to discover. Each accepted client gets its own DataServer.
941
989
  //
942
990
  // Neither the bind nor the publish may be swallowed: either failure leaves the LSP
943
991
  // head serving text edits while every data client waits on a port command that was
944
992
  // never registered, and the launcher reports a bind failure only if given a logger.
945
- const dataServer = startSocketServer({ port: 0, logTag: 'ModelServer', logger: shared.Logger }, dataConnection => {
993
+ const dataServer = startSocketServer({ shared, port: 0, logTag: 'ModelServer', logger: shared.Logger }, dataConnection => {
946
994
  new DataServer<${roots.join(' | ')}>(dataConnection, shared);
947
995
  return { dispose: () => undefined };
948
996
  });
949
997
  dataServer.started
950
- .then(() => {
951
- const { port } = dataServer;
952
- if (port === undefined) {
953
- // \`started\` resolves only once the address is resolved, so this is
954
- // unreachable; a non-null assertion in its place would publish
955
- // \`undefined\`, which the host cannot tell from an unreachable port.
956
- throw new Error('the data head started without a resolved port');
957
- }
958
- publishPortOnLspConnection(shared.lsp.Connection, ${upper}_DATA_SERVER_PORT_COMMAND, port);
959
- })
998
+ .then(
999
+ () => {
1000
+ const { port } = dataServer;
1001
+ if (port === undefined) {
1002
+ // \`started\` resolves only once the address is resolved, so this is
1003
+ // unreachable; a non-null assertion in its place would publish
1004
+ // \`undefined\`, which the host cannot tell from an unreachable port.
1005
+ throw new Error('the data head started without a resolved port');
1006
+ }
1007
+ publishPortOnLspConnection(shared.lsp.Connection, ${upper}_DATA_SERVER_PORT_COMMAND, port);
1008
+ },
1009
+ // The launcher logged its own bind failure; the rejection is control flow.
1010
+ () => undefined
1011
+ )
960
1012
  .catch((error: unknown) => {
961
- const reason = error instanceof Error ? error.message : String(error);
962
- shared.Logger.error(\`[ModelServer] Could not publish the data-server port: \${reason}\`);
1013
+ shared.Logger.error(\`[ModelServer] Could not publish the data-server port: \${describeErrorWithStack(error)}\`);
963
1014
  });
964
- `
965
- : '';
1015
+ `;
966
1016
  const glspBlock = glsp
967
1017
  ? `
968
1018
  // GLSP head on the same shared workspace. \`HydraniumGlspAppModule\` is used
@@ -970,59 +1020,118 @@ dataServer.started
970
1020
  // grammar a request concerns, so nothing per-language belongs there — each
971
1021
  // diagram module declares its own language instead.
972
1022
  //
973
- // GLSP framework logs route through the LSP connection rather than stdout, which
974
- // IS the LSP transport in stdio mode; writing there corrupts the protocol stream.
1023
+ ${lsp
1024
+ ? `// GLSP framework logs route through the LSP connection rather than stdout, which
1025
+ // IS the LSP transport in stdio mode; writing there corrupts the protocol stream.`
1026
+ : `// GLSP framework logs route through the framework's logger to stderr. GLSP's
1027
+ // own startup line goes to stdout, beside the ports line below.`}
975
1028
  const glspServer = startGlspServer({
976
1029
  // The GLSP log threshold lives on the logger, not beside it: the framework
977
1030
  // replaces GLSP's own \`Logger\` binding, so a launcher-level \`logLevel\` would
978
1031
  // be discarded. The logger tracks the framework's process-wide threshold, so
979
- // \`HYDRANIUM_LOG_LEVEL\` and the LSP log-level setting govern GLSP output too;
1032
+ // \`HYDRANIUM_LOG_LEVEL\` ${lsp ? 'and the LSP log-level setting govern' : 'governs'} GLSP output too;
980
1033
  // pass \`logLevel\` only to make GLSP quieter than the rest of the server.
981
1034
  createLogger: caller => new GlspClientLogger(shared, { component: caller }),
982
1035
  serverModule: new ServerModule()${diagrams.map(grammar => `.configureDiagramModule(new ${grammar.grammar}DiagramModule())`).join('')},
983
1036
  appModules: [new HydraniumGlspAppModule({ shared })],
984
- lspConnection: shared.lsp.Connection,
985
- portCommand: ${upper}_GLSP_PORT_COMMAND
1037
+ ${lsp
1038
+ ? ` lspConnection: shared.lsp.Connection,
1039
+ portCommand: ${upper}_GLSP_SERVER_PORT_COMMAND`
1040
+ : ` workspace,
1041
+ port: portFrom('glsp-server-port', ${upper}_GLSP_SERVER_PORT_ENV),
1042
+ logger: shared.Logger`}
986
1043
  });
987
- void glspServer;
988
- `
1044
+ ${lsp ? 'void glspServer;\n' : ''}`
989
1045
  : '';
1046
+ const lspImports = `// The framework's entry point, NOT Langium's \`@hydranium/langium/lsp\` one. It is
1047
+ // signature-compatible and delegates straight through; what it adds first is
1048
+ // \`assertLspHeadComposed\`, which fails the start if the LSP head's SHARED module
1049
+ // was never composed. Reaching for Langium's is the natural mistake and it is
1050
+ // silent: the server boots, links and completes, while echo suppression, the
1051
+ // didChangeContent debounce and the last-client-close rebuild are all inert.
1052
+ import { startLanguageServer, withHydraniumLspFeatures } from '@hydranium/core/lsp';
1053
+ import { ProposedFeatures, createConnection } from 'vscode-languageserver/node';
1054
+ `;
1055
+ const lspStart = `
1056
+ const connection = createConnection(withHydraniumLspFeatures(ProposedFeatures.all));
1057
+ const { shared } = create__NAME__Services({ connection, ...NodeFileSystem });
1058
+ startLanguageServer(shared);
1059
+ `;
1060
+ // No LSP client names the workspace or asks for the ports here, so the
1061
+ // command line does, and the ports go to stdout once every head listens.
1062
+ const servers = [...(data ? ['dataServer'] : []), ...(glsp ? ['glspServer'] : [])];
1063
+ const socketStart = `
1064
+ const { values, positionals } = parseArgs({
1065
+ options: { ${portFlags.map(flag => `'${flag.slice(2)}': { type: 'string' }`).join(', ')} },
1066
+ allowPositionals: true
1067
+ });
1068
+ // The workspace root: the first argument, else the process cwd.
1069
+ const workspace = positionals[0] ?? process.cwd();
1070
+
1071
+ /** A head's port: its flag, else its environment variable, else 0 for any free port. */
1072
+ function portFrom(flag: keyof typeof values, variable: string): number {
1073
+ const text = values[flag] ?? process.env[variable] ?? '0';
1074
+ const port = Number(text);
1075
+ // Decimal digits only: \`Number\` also reads '' as 0 and takes '1e3' or '0x10'.
1076
+ if (!/^\\d+$/.test(text) || port > 65_535) {
1077
+ throw new Error(\`--\${flag} or \${variable} is '\${text}', not a port\`);
1078
+ }
1079
+ return port;
1080
+ }
1081
+
1082
+ const { shared } = create__NAME__Services({ ...NodeFileSystem });
1083
+ `;
1084
+ const portsLine = `
1085
+ // One JSON line on stdout once every head listens, for the process that started
1086
+ // this one to read the ports from: the one line that starts with \`{\`. Each
1087
+ // head brings the workspace up before it listens; a head that fails to start
1088
+ // has logged why and ends the process, which would otherwise serve without it.
1089
+ Promise.all([${servers.map(server => `${server}.started`).join(', ')}]).then(
1090
+ () => process.stdout.write(\`\${JSON.stringify({ ${servers.map(server => `${server}: ${server}.port`).join(', ')} })}\\n\`),
1091
+ () => process.exit(1)
1092
+ );
1093
+ `;
1094
+ const invocation = lsp
1095
+ ? `// Invocation: \`node lib/main.js --stdio\`, or the package's \`__PROJECT_ID__\` bin
1096
+ // script.`
1097
+ : `// Invocation: \`node lib/main.js [<workspace-path>] ${portFlags.map(flag => `[${flag} <port>]`).join(' ')}\`,
1098
+ // or the package's \`__PROJECT_ID__\` bin script. Each port falls back to its
1099
+ // environment variable, then to any free port.`;
1100
+ const dataNote = !data
1101
+ ? ''
1102
+ : lsp
1103
+ ? `
1104
+ //
1105
+ // NOT the entry the data-server subcommands of \`hydranium-cli\` speak to: stdio
1106
+ // here carries LSP, and the data head is a socket whose port is published over
1107
+ // the LSP connection. Those subcommands spawn \`data-server-main.js\`.`
1108
+ : `
1109
+ //
1110
+ // NOT the entry the data-server subcommands of \`hydranium-cli\` speak to: they
1111
+ // drive the data protocol over a child's stdio, and the data head here is a
1112
+ // socket. Those subcommands spawn \`data-server-main.js\`.`;
990
1113
  const text = `${SHEBANG}// Standalone entry point: starts ${headSummary} in the same
991
1114
  // process, so every head shares one model store.
992
- // Invocation: \`node lib/main.js --stdio\`, or the package's \`__PROJECT_ID__\` bin
993
- // script.${data
994
- ? `
995
- //
996
- // NOT the entry \`hydranium-cli query\` / \`save\` / \`projects\` / \`watch\` speak to:
997
- // stdio here carries LSP, and the data head is a socket whose port is published
998
- // over the LSP connection. Those subcommands spawn \`data-server-main.js\`.`
999
- : ''}
1115
+ ${invocation}${dataNote}
1000
1116
  //
1001
1117
  // Everything here runs at module scope, so this file is an executable rather
1002
1118
  // than a library entry — import \`./index.js\` instead to compose the language.
1003
1119
 
1004
1120
  ${reflectImport}${glspImports}${importList(coreNodeSymbols, '@hydranium/core/node', columns)}
1005
- ${data ? "import { DataServer } from '@hydranium/data-server';\n" : ''}import { startLanguageServer } from '@hydranium/langium/lsp';
1006
- import { ProposedFeatures, createConnection } from 'vscode-languageserver/node';
1007
- ${diagramImports}${transferImports}import { create__NAME__Services } from './language-server/__PROJECT_ID__-module.js';
1008
- ${portCommands === '' ? '' : '\n' + portCommands}
1009
- const connection = createConnection(ProposedFeatures.all);
1010
- const { shared } = create__NAME__Services({ connection, ...NodeFileSystem });
1011
- startLanguageServer(shared);
1012
- ${dataBlock}${glspBlock}`;
1121
+ ${data ? "import { DataServer } from '@hydranium/data-server';\n" : ''}${data && lsp ? "import { describeErrorWithStack } from '@hydranium/protocol';\n" : ''}${lsp ? lspImports : "import { parseArgs } from 'node:util';\n"}${diagramImports}${transferImports}import { create__NAME__Services } from './language-server/__PROJECT_ID__-module.js';
1122
+ ${portImports === '' ? '' : '\n' + portImports}${lsp ? lspStart : socketStart}${dataBlock}${glspBlock}${lsp ? '' : portsLine}`;
1013
1123
  return project(text, composition);
1014
1124
  }
1015
1125
  /**
1016
1126
  * The second executable entry: the data head alone, on stdio.
1017
1127
  *
1018
1128
  * Emitted with the `data` head because it is the ONLY command line that reaches
1019
- * that head from outside the editor. `main.ts` gives stdio to LSP and publishes
1020
- * the data head's socket port over the LSP connection, so a client that is not
1021
- * an LSP client cannot discover it — which is every one of `hydranium-cli`'s
1022
- * `query` / `save` / `projects` / `watch`, all of which drive the data protocol
1023
- * over a spawned child's stdin/stdout. Without this file a scaffolded project
1024
- * has no `--server` value, and the closest guess — the LSP entry — fails as an
1025
- * unrecognised method rather than as a missing head.
1129
+ * that head from outside the editor. `main.ts` serves the data head on a
1130
+ * socket, so a client driving the data protocol over a spawned child's
1131
+ * stdin/stdout cannot reach it — which is every data-server subcommand of
1132
+ * `hydranium-cli`. Without this file a scaffolded project has no `--server`
1133
+ * value, and the closest guess, `main.ts`, fails as an unrecognised method or
1134
+ * an unknown flag rather than as a missing head.
1026
1135
  *
1027
1136
  * A separate process rather than a flag on `main.ts`: only one protocol can own
1028
1137
  * stdio, so the two entries are mutually exclusive by construction.
@@ -1032,21 +1141,25 @@ function dataServerMainFile(composition) {
1032
1141
  const roots = [...composition.grammars.map(grammar => grammar.entryRule)].sort();
1033
1142
  const plural = roots.length > 1 ? 's' : '';
1034
1143
  const text = `${SHEBANG}// Standalone entry point: a data-server head on **stdio**, with no LSP head in
1035
- // the process. This is the transport \`hydranium-cli\` speaks — \`query\`, \`save\`,
1036
- // \`projects\` and \`watch\` all spawn a server command and drive JSON-RPC over its
1037
- // stdin/stdout — so it is what makes those subcommands usable against this
1038
- // language.
1144
+ // the process. This is the transport \`hydranium-cli\` speaks — its data-server
1145
+ // subcommands spawn a server command and drive JSON-RPC over its stdin/stdout —
1146
+ // so it is what makes those subcommands usable against this language.
1039
1147
  //
1040
1148
  // Invocation: \`node lib/data-server-main.js [<workspace-path>]\`, or the
1041
1149
  // package's \`__PROJECT_ID__-data-server\` bin script. The workspace path defaults to
1042
1150
  // the process cwd, which is what the CLI's \`--cwd\` sets on the child.
1043
1151
  //
1044
- // Contrast with \`main.ts\`, the editor entry: there the LSP head owns stdio and
1152
+ ${composition.heads.includes('lsp')
1153
+ ? `// Contrast with \`main.ts\`, the editor entry: there the LSP head owns stdio and
1045
1154
  // the data head is a socket published over the LSP connection. Here there is no
1046
1155
  // LSP connection at all, so the workspace initialization an
1047
1156
  // \`initialize\`/\`initialized\` pair would otherwise drive has to happen here —
1048
1157
  // which is why this entry uses \`startStdioServer\` rather than wiring a
1049
- // connection directly.
1158
+ // connection directly.`
1159
+ : `// Contrast with \`main.ts\`, where the data head is a socket whose port it
1160
+ // prints. Here it owns stdio, and as there, no LSP connection drives the
1161
+ // workspace initialization, so \`startStdioServer\` brings the workspace up
1162
+ // before it listens.`}
1050
1163
  //
1051
1164
  // Everything here runs at module scope, so this file is an executable rather
1052
1165
  // than a library entry — import \`./index.js\` instead to compose the language.
@@ -1054,7 +1167,7 @@ function dataServerMainFile(composition) {
1054
1167
  ${importList(['NodeFileSystem', 'startStdioServer'], '@hydranium/core/node', columns)}
1055
1168
  import { DataServer } from '@hydranium/data-server';
1056
1169
  // The TRANSFER root${plural}, not the AST one${plural} — same reasoning as \`main.ts\`.
1057
- ${importList(roots, './language-server/generated-hydranium/transfer-model.js', columns, true)}
1170
+ ${importList(roots, memberPackageName(composition, 'protocol'), columns, true)}
1058
1171
  import { create__NAME__Services } from './language-server/__PROJECT_ID__-module.js';
1059
1172
 
1060
1173
  const { shared } = create__NAME__Services({ ...NodeFileSystem });
@@ -1102,7 +1215,7 @@ function servicesTest(composition) {
1102
1215
  // is missing from the composition.
1103
1216
 
1104
1217
  import { describe, expect, it } from 'vitest';
1105
- import { createServices } from '../src/services.js';
1218
+ import { createServices } from '../src/node/services.js';
1106
1219
 
1107
1220
  describe('__NAME__ services', () => {
1108
1221
  it('composes the DI tree and registers ${title}', () => {
@@ -1170,8 +1283,8 @@ function serializationTest(composition) {
1170
1283
  const transferTypes = composition.grammars.map(grammar => `${grammar.entryRule} as Transfer${grammar.entryRule}`);
1171
1284
  const text = `// Round-trips each grammar through its serializer: parse the source, serialize
1172
1285
  // the model, compare the text. The serializer is the parser's inverse, so this
1173
- // is the assertion that keeps \`ModelService.update\` / \`save\` writing files the
1174
- // language server can read back.
1286
+ // is the assertion that keeps a client session's \`update\` / \`save\` writing
1287
+ // files the language server can read back.
1175
1288
  //
1176
1289
  // The transfer case is the one that catches the mistake worth catching.
1177
1290
  // \`ModelService.modelToText\` short-circuits only a RAW STRING, so a typed model
@@ -1181,8 +1294,8 @@ function serializationTest(composition) {
1181
1294
  import { parseHelper } from '@hydranium/core/testing';
1182
1295
  import { describe, expect, it } from 'vitest';
1183
1296
  ${importList(astTypes, '../src/language-server/ast.js', columns, true)}
1184
- ${importList(transferTypes, '../src/language-server/generated-hydranium/transfer-model.js', columns, true)}
1185
- import { createServices } from '../src/services.js';
1297
+ ${importList(transferTypes, memberPackageName(composition, 'protocol'), columns, true)}
1298
+ import { createServices } from '../src/node/services.js';
1186
1299
 
1187
1300
  ${composition.grammars.map(grammar => render(SERIALIZATION_SUITE, composition, grammar)).join('\n\n')}
1188
1301
  `;
@@ -1305,7 +1418,7 @@ function grammarTest(composition, header, imports, suite) {
1305
1418
  import { parseHelper } from '@hydranium/core/testing';
1306
1419
  import { describe, expect, it } from 'vitest';
1307
1420
  ${imports.join('\n')}${imports.length ? '\n' : ''}${importList(astTypes, '../src/language-server/ast.js', columns, true)}
1308
- import { createServices } from '../src/services.js';
1421
+ import { createServices } from '../src/node/services.js';
1309
1422
 
1310
1423
  ${composition.grammars.map(grammar => render(suite, composition, grammar)).join('\n\n')}
1311
1424
  `;
@@ -1332,6 +1445,7 @@ function readme(composition) {
1332
1445
  const multi = composition.grammars.length > 1;
1333
1446
  const data = composition.heads.includes('data');
1334
1447
  const primaryExtension = composition.grammars[0].extensions[0];
1448
+ const diagramCount = composition.grammars.filter(grammar => grammar.diagram).length;
1335
1449
  const grammarBullets = composition.grammars.map(grammar => `- \`src/grammar/${grammar.grammarId}.langium\` — the \`${grammar.grammar}\` grammar (.${grammar.extensions.join(', .')})`);
1336
1450
  const lines = [
1337
1451
  '# __NAME__',
@@ -1340,20 +1454,6 @@ function readme(composition) {
1340
1454
  '',
1341
1455
  '## Getting started',
1342
1456
  '',
1343
- // Conditional on the pin this scaffold actually carries — see
1344
- // `UNPUBLISHED_FRAMEWORK_VERSION`.
1345
- ...(composition.frameworkVersion === UNPUBLISHED_FRAMEWORK_VERSION
1346
- ? [
1347
- '> **Pre-publish note.** `@hydranium/*` is not on npm yet, so the `0.0.0`',
1348
- '> pins below are placeholders and `npm install` will fail with a 404 until',
1349
- '> the framework is released. Until then, supply the packages from a local',
1350
- '> framework checkout with [yalc](https://github.com/wclr/yalc) — a plain',
1351
- '> `file:` path or `npm link` is not enough, because the framework packages',
1352
- '> depend on each other by version and npm would try to fetch those from the',
1353
- '> registry too.',
1354
- ''
1355
- ]
1356
- : []),
1357
1457
  '```bash',
1358
1458
  'npm install',
1359
1459
  'npm run langium:generate # generate the AST from the grammar',
@@ -1368,25 +1468,26 @@ function readme(composition) {
1368
1468
  // nowhere else.
1369
1469
  '```bash',
1370
1470
  '# Grammar / AST reflection',
1371
- 'npx hydranium-cli reflect --services ./lib/services.js',
1471
+ 'npx hydranium-cli reflect --services ./lib/node/services.js',
1372
1472
  '',
1373
1473
  '# Grammar-convention lint (CI gate)',
1374
- 'npx hydranium-cli lint-grammar --services ./lib/services.js',
1474
+ 'npx hydranium-cli lint-grammar --services ./lib/node/services.js',
1375
1475
  '',
1376
1476
  '# Validate a workspace of model files (non-zero exit on errors)',
1377
- 'npx hydranium-cli validate --services ./lib/services.js <workspace-dir>',
1477
+ 'npx hydranium-cli validate --services ./lib/node/services.js ../workspace',
1378
1478
  ...(data
1379
1479
  ? [
1380
1480
  '',
1381
1481
  '# Data-head operations. `--server` is a command line the CLI spawns and then',
1382
1482
  "# drives the data protocol over the child's stdin/stdout, so it has to name",
1383
- '# `data-server-main.js` — `main.js` gives stdio to LSP and answers these',
1384
- '# methods with "Unhandled method".',
1483
+ ...(composition.heads.includes('lsp')
1484
+ ? ['# `data-server-main.js` — `main.js` gives stdio to LSP and answers these', '# methods with "Unhandled method".']
1485
+ : ['# `data-server-main.js` — `main.js` serves the data head on a socket.']),
1385
1486
  '#',
1386
1487
  '# The workspace goes to the ENTRY, not to `--cwd`: `--cwd` re-roots the child,',
1387
1488
  '# so a relative entry path is refused by name (an absolute one is fine).',
1388
- 'npx hydranium-cli projects --server "node ./lib/data-server-main.js <workspace-dir>"',
1389
- `npx hydranium-cli query --server "node ./lib/data-server-main.js <workspace-dir>" --uri <file:// URI of a .${primaryExtension} file>`
1489
+ 'npx hydranium-cli projects --server "node ./lib/data-server-main.js ../workspace"',
1490
+ `npx hydranium-cli query --server "node ./lib/data-server-main.js ../workspace" --uri <file:// URI of ../workspace/nodes.${primaryExtension}>`
1390
1491
  ]
1391
1492
  : []),
1392
1493
  '```',
@@ -1397,8 +1498,8 @@ function readme(composition) {
1397
1498
  ...(multi ? ['- `src/grammar/common.langium` — shared terminals, imported by each grammar (not a language)'] : []),
1398
1499
  '- `src/language-server/__PROJECT_ID__-module.ts` — `create__NAME__Services` DI wiring',
1399
1500
  ...composition.grammars.map(grammar => `- \`src/language-server/${grammar.grammarId}-serializer.ts\` — emits \`${grammar.grammar}\` back to text (the framework defaults this to a throw)`),
1400
- '- `src/services.ts` — zero-arg `createServices()` for the headless CLI',
1401
- '- `src/index.ts` — the package entry (`main`): DI factory + generated AST',
1501
+ "- `src/node/services.ts` — zero-arg `createServices()` for the headless CLI, the package's `./node` entry",
1502
+ `- \`src/index.ts\` — the package root, loadable in a browser worker: DI factory + generated AST${diagramCount === 0 ? '' : diagramCount === 1 ? ' + diagram module' : ' + diagram modules'}`,
1402
1503
  `- \`src/main.ts\` — starts ${composition.heads.join(' + ')}, the \`__PROJECT_ID__\` bin entry`,
1403
1504
  ...(data
1404
1505
  ? [
@@ -1408,7 +1509,8 @@ function readme(composition) {
1408
1509
  : []),
1409
1510
  ...composition.grammars
1410
1511
  .filter(grammar => grammar.diagram)
1411
- .map(grammar => `- \`src/glsp/${grammar.grammarId}/\` — the \`${grammar.grammar}\` diagram: type ids, state, storage, submission handler, GModel factory, configuration, create-node handler, DI module`),
1512
+ .map(grammar => `- \`src/glsp/${grammar.grammarId}/\` — the \`${grammar.grammar}\` diagram: state, storage, submission handler, GModel factory, configuration, create-node handler, DI module`),
1513
+ `- \`../protocol/\` — the \`${memberPackageName(composition, 'protocol')}\` package: what this server and its clients both import (language ids, ${headPortConstants(composition).length === 0 ? '' : composition.heads.includes('lsp') ? 'port commands, ' : 'port variables, '}${composition.grammars.some(grammar => grammar.diagram) ? 'diagram ids, ' : ''}the transfer model), written partly by \`npm run generate\``,
1412
1514
  '- `test/services.test.ts` — the DI tree composes; grows as your language does',
1413
1515
  '- `test/parsing.test.ts` / `linking.test.ts` / `validating.test.ts` — the three tiers a new language breaks first',
1414
1516
  '- `test/serialization.test.ts` — each grammar round-trips through its serializer',
@@ -1461,6 +1563,24 @@ function readme(composition) {
1461
1563
  'atomic set and depends on a single physical copy, so a floating range can',
1462
1564
  'silently resolve a second one.',
1463
1565
  '',
1566
+ '`vscode-languageserver` is pinned to `10.0.1`, and',
1567
+ '`vscode-languageserver-protocol` and `vscode-jsonrpc` to the `3.18.1` and',
1568
+ '`9.0.0` it pins, so Langium, the LSP connection and the framework share one',
1569
+ 'copy of each. With two, an error a Langium handler returns, such as a request',
1570
+ 'for a document the server does not have, reaches the client as a result, and a',
1571
+ 'framework error your own LSP handler throws loses its code. The Langium 4.4',
1572
+ 'upgrade',
1573
+ '([eclipse-emfcloud/hydranium#142](https://github.com/eclipse-emfcloud/hydranium/issues/142))',
1574
+ 'replaces these pins.',
1575
+ '',
1576
+ ...(pinsVitest(composition)
1577
+ ? [
1578
+ '`vitest` is held below 4.1, whose peer set crashes the npm every Node 22',
1579
+ 'release bundles. Raise it once every npm that installs this project is',
1580
+ '11.6 or later.',
1581
+ ''
1582
+ ]
1583
+ : []),
1464
1584
  '## If your repo gates license headers',
1465
1585
  '',
1466
1586
  'The emitted `.ts` files carry no copyright header — the scaffold cannot know',
@@ -1479,10 +1599,9 @@ function readme(composition) {
1479
1599
  /**
1480
1600
  * The GLSP files for ONE grammar's diagram.
1481
1601
  *
1482
- * Per grammar rather than per project because a diagram type has exactly one
1483
- * grammar — `AbstractHydraniumGlspDiagramModule.declareLanguage()` returns that
1484
- * grammar's `LanguageMetaData`, and it is bound on the SESSION container so a
1485
- * grammar with no diagram can sit on the same server.
1602
+ * Per grammar rather than per project because the state, storage and GModel
1603
+ * factory are typed over that grammar's model, and a grammar with no diagram
1604
+ * can sit on the same server.
1486
1605
  *
1487
1606
  * The index and the computed-bounds handler are framework classes used
1488
1607
  * unmodified; storage and the submission handler are thin typed subclasses,
@@ -1496,152 +1615,11 @@ function readme(composition) {
1496
1615
  */
1497
1616
  function glspFiles(composition, grammar) {
1498
1617
  const columns = columnsFor(composition);
1499
- const upper = screamingSnake(grammar.grammarId);
1500
- const dir = `src/glsp/${grammar.grammarId}`;
1501
- const render = (text) => grammarTier(project(text, composition), grammar);
1502
- const types = `// GLSP diagram-type and element-type ids for the __GRAMMAR__ diagram.
1503
- //
1504
- // **Authoritative half of a client/server contract.** Every id here has to be
1505
- // registered on the client too: sprotty's registries are exact-key maps with no
1506
- // prefix fallback, so an id the client does not know yields an element with none
1507
- // of a node's features, rendered by \`MissingView\` with only a
1508
- // \`no registered view for type '…'\` console warning. Nothing fails server-side.
1509
- //
1510
- // The ids stay namespaced under the GLSP defaults (\`node:\` / \`edge:\`) so a
1511
- // reader can tell a node id from an edge id at a glance.
1512
-
1513
- import { DefaultTypes } from '@eclipse-glsp/server';
1514
-
1515
- /**
1516
- * The GLSP diagram type — the string GLSP routes every per-diagram-type request
1517
- * by. Mirrored by the client; a mismatch silently DROPS the request rather than
1518
- * reporting an unknown diagram type. Same value as the language id, so the two
1519
- * cannot drift apart as a diagram gains element types.
1520
- */
1521
- export const ${upper}_DIAGRAM_TYPE = '__LANGUAGE_ID__';
1522
-
1523
- /** A \`__NODE_RULE__\` — the starter grammar's one named, referenceable node. */
1524
- export const ${upper}_NODE_TYPE = \`\${DefaultTypes.NODE}:__GRAMMAR_ID__-node\`;
1525
-
1526
- /** A resolved \`target\` reference, drawn as a connection between two nodes. */
1527
- export const ${upper}_EDGE_TYPE = \`\${DefaultTypes.EDGE}:__GRAMMAR_ID__-target\`;
1528
- `;
1529
- const state = `// GLSP state for the __GRAMMAR__ diagram.
1530
- //
1531
- // \`FullTextHydraniumGlspState\` is the simplest of the framework source-model
1532
- // strategies (the others project a structured transfer model, over one document
1533
- // or several): the source model is the whole document text, serialised through
1534
- // the per-URI \`Serializer\` and round-tripped by re-parsing. Both seams resolve
1535
- // through shared services, so narrowing the root type is all an adopter adds.
1536
- //
1537
- // It cannot field-merge — every concurrent edit on a whole-document model is a
1538
- // same-document collision, so undo / redo degrade to drop-on-divergence. Move to
1539
- // \`ReconcilingTransferHydraniumGlspState\` when you need field-level undo.
1540
-
1541
- import { FullTextHydraniumGlspState } from '@hydranium/glsp-server';
1542
- import { injectable } from 'inversify';
1543
- import type { __ENTRY_RULE__ } from '../../language-server/ast.js';
1544
-
1545
- @injectable()
1546
- export class __GRAMMAR__GlspState extends FullTextHydraniumGlspState<__ENTRY_RULE__> {}
1547
- `;
1548
- const storage = `// Source-model storage for the __GRAMMAR__ diagram, inheriting both framework
1549
- // defaults: \`loadSourceModel\` (open + settle + \`setSourceRoot\`) and
1550
- // \`saveSourceModel\` (through \`ModelService.save\` → the per-URI \`Serializer\` →
1551
- // the multi-client text store → \`WritableFileSystemProvider\`).
1552
- //
1553
- // The subclass exists so a bespoke load or save has a stable place to land.
1554
-
1555
- import { type FullTextSourceModel, HydraniumGlspStorage } from '@hydranium/glsp-server';
1556
- import { injectable } from 'inversify';
1557
- import type { __ENTRY_RULE__ } from '../../language-server/ast.js';
1558
-
1559
- @injectable()
1560
- export class __GRAMMAR__GlspStorage extends HydraniumGlspStorage<__ENTRY_RULE__, FullTextSourceModel> {}
1561
- `;
1562
- const submission = `// Submission handler for the __GRAMMAR__ diagram.
1563
- //
1564
- // Inherits the framework's \`readyEvent = IntegrityService.SettledState\`, which
1565
- // is load-bearing rather than incidental: the GModel factory resolves
1566
- // \`target.ref\`, so it needs a fully-linked AST. Without the gate those reads
1567
- // fire mid-build and warn about resolution before scopes are computed.
1568
- //
1569
- // Only \`formatSourceRoot\` is overridden, so the submit log names the model and
1570
- // its node count instead of a bare \`$type\`.
1571
-
1572
- import { ModelState } from '@eclipse-glsp/server';
1573
- import { type FullTextSourceModel, HydraniumGlspSubmissionHandler } from '@hydranium/glsp-server';
1574
- import { inject, injectable } from 'inversify';
1575
- import type { __ENTRY_RULE__ } from '../../language-server/ast.js';
1576
- import type { __GRAMMAR__GlspState } from './state.js';
1577
-
1578
- @injectable()
1579
- export class __GRAMMAR__SubmissionHandler extends HydraniumGlspSubmissionHandler<__ENTRY_RULE__, FullTextSourceModel> {
1580
- @inject(ModelState) declare protected modelState: __GRAMMAR__GlspState;
1581
-
1582
- protected override formatSourceRoot(root: __ENTRY_RULE__ | undefined): string {
1583
- return root ? \`__ENTRY_RULE__ nodes=\${root.nodes.length}\` : 'none';
1584
- }
1585
- }
1586
- `;
1587
- const factory = `// AST → GModel for the __GRAMMAR__ diagram: one node per \`__NODE_RULE__\`, one
1588
- // edge per resolved \`target\` reference.
1589
- //
1590
- // Ids come from the index rather than being composed here, so the id strategy
1591
- // stays in one place. Nodes are emitted before edges, because a GLSP edge
1592
- // pointing at an id that does not exist fails client-side with a far less
1593
- // obvious error than a missing edge. An unresolved reference is SKIPPED rather
1594
- // than treated as an error: a dangling reference is ordinary editing state, and
1595
- // the LSP head already reports it as a diagnostic.
1596
-
1597
- import { DefaultTypes, GEdge, GGraph, GLabel, type GModelFactory, GNode, ModelState } from '@eclipse-glsp/server';
1598
- import { inject, injectable } from 'inversify';
1599
- import type { __ENTRY_RULE__ } from '../../language-server/ast.js';
1600
- import { ${upper}_EDGE_TYPE, ${upper}_NODE_TYPE } from './types.js';
1601
- import type { __GRAMMAR__GlspState } from './state.js';
1602
-
1603
- @injectable()
1604
- export class __GRAMMAR__GModelFactory implements GModelFactory {
1605
- @inject(ModelState) protected readonly modelState!: __GRAMMAR__GlspState;
1606
-
1607
- createModel(): void {
1608
- const root = this.modelState.sourceRoot;
1609
- const graph = GGraph.builder().id(this.modelState.sourceUri).build();
1610
- if (root) {
1611
- this.buildGraph(root, graph);
1612
- }
1613
- this.modelState.updateRoot(graph);
1614
- }
1615
-
1616
- protected buildGraph(root: __ENTRY_RULE__, graph: GGraph): void {
1617
- for (const node of root.nodes) {
1618
- const id = this.modelState.index.createId(node);
1619
- graph.children.push(
1620
- GNode.builder()
1621
- .id(id)
1622
- .type(${upper}_NODE_TYPE)
1623
- .add(GLabel.builder().id(\`\${id}_name\`).text(node.name).type(DefaultTypes.LABEL).build())
1624
- .build()
1625
- );
1626
- }
1627
- for (const node of root.nodes) {
1628
- const target = node.target?.ref;
1629
- if (!target) {
1630
- continue;
1631
- }
1632
- const sourceId = this.modelState.index.createId(node);
1633
- graph.children.push(
1634
- GEdge.builder()
1635
- .id(\`\${sourceId}_target\`)
1636
- .type(${upper}_EDGE_TYPE)
1637
- .sourceId(sourceId)
1638
- .targetId(this.modelState.index.createId(target))
1639
- .build()
1640
- );
1641
- }
1642
- }
1643
- }
1644
- `;
1618
+ const fixed = (file) => grammarTemplate(`server/src/glsp/__GRAMMAR_ID__/${file}`, composition, grammar);
1619
+ const computed = (file, text) => ({
1620
+ path: `server/src/glsp/${grammar.grammarId}/${file}`,
1621
+ content: render(text, composition, grammar)
1622
+ });
1645
1623
  const configuration = `// Diagram configuration for the __GRAMMAR__ diagram.
1646
1624
  //
1647
1625
  // **Every hint is \`false\`, and that is not the same as read-only.** The starter
@@ -1661,7 +1639,7 @@ export class __GRAMMAR__GModelFactory implements GModelFactory {
1661
1639
  import { type GModelElementConstructor } from '@eclipse-glsp/graph';
1662
1640
  ${importList(['type DiagramConfiguration', 'type EdgeTypeHint', 'ServerLayoutKind', 'type ShapeTypeHint', 'getDefaultMapping'], '@eclipse-glsp/server', columns)}
1663
1641
  import { injectable } from 'inversify';
1664
- import { ${upper}_EDGE_TYPE, ${upper}_NODE_TYPE } from './types.js';
1642
+ import { __GRAMMAR_ID_CONST___EDGE_TYPE, __GRAMMAR_ID_CONST___NODE_TYPE } from '__PROTOCOL_PACKAGE__';
1665
1643
 
1666
1644
  @injectable()
1667
1645
  export class __GRAMMAR__DiagramConfiguration implements DiagramConfiguration {
@@ -1673,7 +1651,7 @@ export class __GRAMMAR__DiagramConfiguration implements DiagramConfiguration {
1673
1651
 
1674
1652
  readonly shapeTypeHints: ShapeTypeHint[] = [
1675
1653
  {
1676
- elementTypeId: ${upper}_NODE_TYPE,
1654
+ elementTypeId: __GRAMMAR_ID_CONST___NODE_TYPE,
1677
1655
  deletable: false,
1678
1656
  reparentable: false,
1679
1657
  repositionable: false,
@@ -1683,105 +1661,18 @@ export class __GRAMMAR__DiagramConfiguration implements DiagramConfiguration {
1683
1661
 
1684
1662
  readonly edgeTypeHints: EdgeTypeHint[] = [
1685
1663
  {
1686
- elementTypeId: ${upper}_EDGE_TYPE,
1664
+ elementTypeId: __GRAMMAR_ID_CONST___EDGE_TYPE,
1687
1665
  deletable: false,
1688
1666
  repositionable: false,
1689
1667
  routable: false,
1690
- sourceElementTypeIds: [${upper}_NODE_TYPE],
1691
- targetElementTypeIds: [${upper}_NODE_TYPE]
1668
+ sourceElementTypeIds: [__GRAMMAR_ID_CONST___NODE_TYPE],
1669
+ targetElementTypeIds: [__GRAMMAR_ID_CONST___NODE_TYPE]
1692
1670
  }
1693
1671
  ];
1694
1672
  }
1695
- `;
1696
- const createHandler = `// The starter operation handler for the __GRAMMAR__ diagram: the tool-palette
1697
- // entry that creates a \`__NODE_RULE__\`.
1698
- //
1699
- // **Deleting this file and its \`configureOperationHandlers\` registration in
1700
- // \`diagram-module.ts\` gives a read-only viewer**, and nothing else has to change
1701
- // — creation is offered through the palette rather than through a type hint, so
1702
- // every hint in \`diagram-configuration.ts\` is already \`false\`. The scaffold emits
1703
- // the editable direction because that asymmetry runs one way: editable →
1704
- // read-only is a deletion the compiler checks, while read-only → editable is
1705
- // authoring against a seam you have not used yet.
1706
- //
1707
- // **It composes TEXT rather than mutating the AST.** The source model of
1708
- // \`FullTextHydraniumGlspState\` is the document text, and reading it back through
1709
- // \`state.sourceModel\` serialises the AST through the per-URI \`Serializer\`, which
1710
- // this scaffold does not bind — so that getter throws until you do. Appending a
1711
- // declaration to the text the parser last read needs no serializer, which is what
1712
- // makes a scaffolded diagram editable on day one. Bind a \`Serializer\` at
1713
- // \`services.serializer.Serializer\` and this becomes a
1714
- // \`HydraniumGlspRecordingCommand\` over \`state.sourceModel\` instead — the same
1715
- // binding the diagram's own save action needs.
1716
- //
1717
- // **The drop location is discarded.** \`needsClientLayout\` is \`true\` and the
1718
- // starter grammar persists no bounds, so there is nowhere to put a coordinate: the
1719
- // node is appended at the end of the document and the client places it.
1720
- //
1721
- // Like \`types.ts\`, \`gmodel-factory.ts\` and \`diagram-configuration.ts\`, this file
1722
- // knows the starter grammar's concrete syntax — the \`node\` keyword below is that
1723
- // grammar's. Replacing the grammar means replacing these four together.
1724
-
1725
- import { type Command, type CreateNodeOperation, JsonCreateNodeOperationHandler, type MaybePromise } from '@eclipse-glsp/server';
1726
- import { findNextUnique } from '@hydranium/protocol';
1727
- import { injectable } from 'inversify';
1728
- import { ${upper}_NODE_TYPE } from './types.js';
1729
- import type { __GRAMMAR__GlspState } from './state.js';
1730
-
1731
- /** Proposed name for a new node, uniquified against the ones the document already has. */
1732
- const NODE_NAME_STEM = 'Node';
1733
-
1734
- @injectable()
1735
- export class __GRAMMAR__CreateNodeOperationHandler extends JsonCreateNodeOperationHandler {
1736
- declare protected modelState: __GRAMMAR__GlspState;
1737
-
1738
- /** The palette's word for the thing it creates, so a noun rather than an action. */
1739
- override readonly label = '__NODE_RULE__';
1740
- elementTypeIds = [${upper}_NODE_TYPE];
1741
-
1742
- override createCommand(operation: CreateNodeOperation): MaybePromise<Command | undefined> {
1743
- if (!this.elementTypeIds.includes(operation.elementTypeId)) {
1744
- return undefined;
1745
- }
1746
- const state = this.modelState;
1747
- const before = this.documentText();
1748
- const after = this.withNode(
1749
- before,
1750
- findNextUnique(
1751
- NODE_NAME_STEM,
1752
- state.sourceRoot.nodes.map(node => node.name)
1753
- )
1754
- );
1755
- // Whole-document undo, which is all a full-text source model can offer: it
1756
- // has exactly one field, so there is nothing to merge a concurrent edit into.
1757
- return {
1758
- execute: () => state.updateSourceModel({ text: after }),
1759
- undo: () => state.updateSourceModel({ text: before }),
1760
- redo: () => state.updateSourceModel({ text: after })
1761
- };
1762
- }
1763
-
1764
- /** The text the captured source root was parsed from — the baseline an edit appends to. */
1765
- protected documentText(): string {
1766
- return this.modelState.sourceRoot.$document?.textDocument.getText() ?? '';
1767
- }
1768
-
1769
- /** \`text\` with one more node declaration, under exactly one trailing newline. */
1770
- protected withNode(text: string, name: string): string {
1771
- const body = text.trimEnd();
1772
- const declaration = \`node \${name}\`;
1773
- return body.length === 0 ? \`\${declaration}\\n\` : \`\${body}\\n\${declaration}\\n\`;
1774
- }
1775
- }
1776
1673
  `;
1777
1674
  const module = `// GLSP diagram module for __GRAMMAR__ — the DI wiring of one diagram type.
1778
1675
  //
1779
- // \`declareLanguage\` is the multi-grammar seam: it binds the grammar this diagram
1780
- // edits on the SESSION container, so \`modelState.diagramLanguage\` and the
1781
- // per-language lookups resolve to __LANGUAGE_ID__. That is what stops one
1782
- // diagram type from fighting over a process-wide binding when a project holds
1783
- // several grammars — a grammar with no diagram is registered on the same server.
1784
- //
1785
1676
  // \`configureActionHandlers\` REBINDS rather than adds: GLSP's own
1786
1677
  // \`DiagramModule\` already registers \`ComputedBoundsActionHandler\`, and two
1787
1678
  // handlers for one action would both run.
@@ -1803,23 +1694,17 @@ ${importList([
1803
1694
  'SourceModelStorage'
1804
1695
  ], '@eclipse-glsp/server', columns, true)}
1805
1696
  import { ComputedBoundsActionHandler } from '@eclipse-glsp/server';
1806
- import type { LanguageMetaData } from '@hydranium/langium';
1807
1697
  ${importList(['HydraniumGlspComputedBoundsActionHandler', 'AbstractHydraniumGlspDiagramModule', 'HydraniumGlspIndex'], '@hydranium/glsp-server', columns)}
1808
- import { __GRAMMAR__LanguageMetaData } from '../../language-server/generated/module.js';
1809
1698
  import { __GRAMMAR__CreateNodeOperationHandler } from './create-node-operation-handler.js';
1810
1699
  import { __GRAMMAR__DiagramConfiguration } from './diagram-configuration.js';
1811
1700
  import { __GRAMMAR__GModelFactory } from './gmodel-factory.js';
1812
1701
  import { __GRAMMAR__GlspState } from './state.js';
1813
1702
  import { __GRAMMAR__GlspStorage } from './storage.js';
1814
1703
  import { __GRAMMAR__SubmissionHandler } from './submission-handler.js';
1815
- import { ${upper}_DIAGRAM_TYPE } from './types.js';
1704
+ import { __GRAMMAR_ID_CONST___DIAGRAM_TYPE } from '__PROTOCOL_PACKAGE__';
1816
1705
 
1817
1706
  export class __GRAMMAR__DiagramModule extends AbstractHydraniumGlspDiagramModule {
1818
- readonly diagramType = ${upper}_DIAGRAM_TYPE;
1819
-
1820
- protected override declareLanguage(): LanguageMetaData {
1821
- return __GRAMMAR__LanguageMetaData;
1822
- }
1707
+ readonly diagramType = __GRAMMAR_ID_CONST___DIAGRAM_TYPE;
1823
1708
 
1824
1709
  protected override bindModelState(): BindingTarget<ModelState> {
1825
1710
  return { service: __GRAMMAR__GlspState };
@@ -1858,68 +1743,238 @@ export class __GRAMMAR__DiagramModule extends AbstractHydraniumGlspDiagramModule
1858
1743
  }
1859
1744
  `;
1860
1745
  return [
1861
- { path: `${dir}/types.ts`, content: render(types) },
1862
- { path: `${dir}/state.ts`, content: render(state) },
1863
- { path: `${dir}/storage.ts`, content: render(storage) },
1864
- { path: `${dir}/submission-handler.ts`, content: render(submission) },
1865
- { path: `${dir}/gmodel-factory.ts`, content: render(factory) },
1866
- { path: `${dir}/diagram-configuration.ts`, content: render(configuration) },
1867
- { path: `${dir}/create-node-operation-handler.ts`, content: render(createHandler) },
1868
- { path: `${dir}/diagram-module.ts`, content: render(module) }
1746
+ fixed('state.ts'),
1747
+ fixed('storage.ts'),
1748
+ fixed('submission-handler.ts'),
1749
+ fixed('gmodel-factory.ts'),
1750
+ computed('diagram-configuration.ts', configuration),
1751
+ fixed('create-node-operation-handler.ts'),
1752
+ computed('diagram-module.ts', module),
1753
+ grammarTemplate('server/test/__GRAMMAR_ID__-diagram.test.ts', composition, grammar)
1869
1754
  ];
1870
1755
  }
1871
1756
  /**
1872
- * Every file the scaffold emits, fully rendered.
1757
+ * The server member: the grammar, the language and its heads.
1873
1758
  *
1874
1759
  * Grammar-count-dependent emissions live here rather than in a template string:
1875
1760
  * one `.langium` file per grammar, and the shared `common.langium` fragment only
1876
1761
  * when there is more than one grammar to share it.
1877
1762
  */
1878
- export function buildInitTemplates(composition) {
1763
+ function serverFiles(composition) {
1879
1764
  const multi = composition.grammars.length > 1;
1880
1765
  const grammarFiles = composition.grammars.map(grammar => ({
1881
- path: `src/grammar/${grammar.grammarId}.langium`,
1766
+ path: `server/src/grammar/${grammar.grammarId}.langium`,
1882
1767
  content: render(grammarFile(multi), composition, grammar)
1883
1768
  }));
1769
+ // The shared lexical fragment, only for several grammars: each declaring its
1770
+ // own `WS` / `ID` lets the token sets drift apart.
1884
1771
  if (multi) {
1885
- grammarFiles.unshift({ path: 'src/grammar/common.langium', content: COMMON_GRAMMAR });
1772
+ grammarFiles.unshift(projectTemplate('server/src/grammar/common.langium', composition));
1886
1773
  }
1887
1774
  const diagramFiles = composition.grammars.filter(grammar => grammar.diagram).flatMap(grammar => glspFiles(composition, grammar));
1888
1775
  return [
1889
- { path: 'package.json', content: packageJson(composition) },
1890
- { path: 'langium-config.json', content: langiumConfig(composition) },
1891
- { path: 'tsconfig.json', content: tsconfigJson(composition) },
1892
- { path: 'tsconfig.test.json', content: TSCONFIG_TEST },
1893
- { path: 'vitest.config.ts', content: VITEST_CONFIG },
1894
- {
1895
- path: '.gitignore',
1896
- content: composition.packaging.workspace === undefined ? GITIGNORE : GITIGNORE_WORKSPACE_MEMBER
1897
- },
1898
- { path: 'README.md', content: readme(composition) },
1776
+ { path: 'server/package.json', content: packageJson(composition) },
1777
+ { path: 'server/langium-config.json', content: langiumConfig(composition) },
1778
+ { path: 'server/tsconfig.json', content: serverTsconfigJson(composition) },
1779
+ // `isolatedModules` is what makes this check agree with the transform that
1780
+ // actually runs the tests. `npm test` is `typecheck:test && vitest run`, and
1781
+ // vitest compiles each file on its own — a transform with no type
1782
+ // information, which cannot tell a re-exported TYPE from a re-exported value.
1783
+ // Without the flag `tsc` accepts `export { SomeType } from './x'` that the
1784
+ // transform then emits as a real import of a symbol that does not exist at
1785
+ // runtime, so the typecheck passes and the suite fails with a confusing
1786
+ // missing-export.
1787
+ projectTemplate('server/tsconfig.test.json', composition),
1788
+ // Found by name, unlike `tsconfig.test.json`. Under vite 8, vitest's transform
1789
+ // applies a tsconfig only to the files its own `include` lists, so without this
1790
+ // a test declaring a decorated class fails to load.
1791
+ projectTemplate('server/test/tsconfig.json', composition),
1792
+ projectTemplate('server/vitest.config.ts', composition),
1793
+ // Written for a workspace member too: its root may not ignore `lib/` (a `yo
1794
+ // langium` root ignores `out/`), and a rule the root repeats costs nothing.
1795
+ ignoreFile('server'),
1796
+ { path: 'server/README.md', content: readme(composition) },
1899
1797
  ...grammarFiles,
1900
- { path: `src/language-server/${composition.projectId}-module.ts`, content: moduleFile(composition) },
1901
- { path: 'src/language-server/ast.ts', content: render(AST, composition, composition.grammars[0]) },
1902
- ...composition.grammars.map(grammar => ({
1903
- path: `src/language-server/${grammar.grammarId}-serializer.ts`,
1904
- content: render(SERIALIZER, composition, grammar)
1905
- })),
1906
- { path: 'src/index.ts', content: indexFile(composition) },
1907
- { path: 'src/services.ts', content: project(SERVICES, composition) },
1908
- { path: 'src/main.ts', content: mainFile(composition) },
1909
- ...(composition.heads.includes('data') ? [{ path: 'src/data-server-main.ts', content: dataServerMainFile(composition) }] : []),
1910
- ...(portCommandNames(composition).length ? [{ path: 'src/head-ports.ts', content: headPortsFile(composition) }] : []),
1798
+ { path: `server/src/language-server/${composition.projectId}-module.ts`, content: moduleFile(composition) },
1799
+ grammarTemplate('server/src/language-server/ast.ts', composition, composition.grammars[0]),
1800
+ // Per grammar, because `services.serializer.Serializer` is a per-language slot
1801
+ // and a serializer is grammar-shaped by definition. That is also why the
1802
+ // framework refuses to default it: its `UnboundSerializer` throws, naming the
1803
+ // binding to add, rather than guessing a syntax.
1804
+ ...composition.grammars.map(grammar => grammarTemplate('server/src/language-server/__GRAMMAR_ID__-serializer.ts', composition, grammar)),
1805
+ { path: 'server/src/index.ts', content: indexFile(composition) },
1806
+ projectTemplate('server/src/node/services.ts', composition),
1807
+ { path: 'server/src/main.ts', content: mainFile(composition) },
1808
+ ...(composition.heads.includes('data') ? [{ path: 'server/src/data-server-main.ts', content: dataServerMainFile(composition) }] : []),
1911
1809
  ...diagramFiles,
1912
- { path: 'test/services.test.ts', content: servicesTest(composition) },
1913
- { path: 'test/parsing.test.ts', content: grammarTest(composition, PARSING_HEADER, [], PARSING_SUITE) },
1810
+ { path: 'server/test/services.test.ts', content: servicesTest(composition) },
1811
+ { path: 'server/test/parsing.test.ts', content: grammarTest(composition, PARSING_HEADER, [], PARSING_SUITE) },
1914
1812
  {
1915
- path: 'test/linking.test.ts',
1813
+ path: 'server/test/linking.test.ts',
1916
1814
  content: grammarTest(composition, LINKING_HEADER, ["import { AstUtils } from '@hydranium/langium';"], LINKING_SUITE)
1917
1815
  },
1918
1816
  {
1919
- path: 'test/validating.test.ts',
1817
+ path: 'server/test/validating.test.ts',
1920
1818
  content: grammarTest(composition, VALIDATING_HEADER, ["import { DiagnosticSeverity } from 'vscode-languageserver';"], VALIDATING_SUITE)
1921
1819
  },
1922
- { path: 'test/serialization.test.ts', content: serializationTest(composition) }
1820
+ { path: 'server/test/serialization.test.ts', content: serializationTest(composition) }
1821
+ ];
1822
+ }
1823
+ /**
1824
+ * The protocol member's `package.json`.
1825
+ *
1826
+ * Its `build` runs the server's `generate` first, because the server writes
1827
+ * this package's `generated-hydranium/` modules from the grammar: a runner that
1828
+ * builds in dependency order reaches this package before the server, and would
1829
+ * otherwise compile modules that do not exist yet. A full build therefore
1830
+ * generates twice; the second pass rewrites nothing.
1831
+ */
1832
+ function protocolPackageJson(composition) {
1833
+ const member = forMember(composition, 'protocol');
1834
+ const dependencies = {};
1835
+ for (const head of composition.heads) {
1836
+ Object.assign(dependencies, PROTOCOL_THIRD_PARTY_DEPENDENCIES[head]);
1837
+ }
1838
+ const dependencyLines = Object.keys(dependencies).length === 0 ? '' : ` "dependencies": {\n${sortedBlock(dependencies, ' ')}\n },\n`;
1839
+ return `{
1840
+ "name": "${memberPackageName(composition, 'protocol')}",
1841
+ "version": "0.0.0",
1842
+ ${composition.packaging.private ? ' "private": true,\n' : ''} "description": "What the ${composition.name} language server and its clients share: language ids, wire types and diagram ids.",
1843
+ "license": "UNLICENSED",
1844
+ "author": "",
1845
+ "files": [
1846
+ "lib",
1847
+ "src"
1848
+ ],
1849
+ "type": "module",
1850
+ "main": "lib/index.js",
1851
+ "types": "lib/index.d.ts",
1852
+ "scripts": {
1853
+ "build": "npm --prefix ../server run generate && tsc -b",
1854
+ "clean": "rimraf lib tsconfig.tsbuildinfo",
1855
+ ${lintScript(member, 'src')} "watch": "tsc -b --watch"
1856
+ },
1857
+ ${dependencyLines} "devDependencies": {
1858
+ "rimraf": "^5.0.0",
1859
+ "typescript": "^7.0.2"
1860
+ },
1861
+ "engines": {
1862
+ "node": ">=22.13"
1863
+ }
1864
+ }
1865
+ `;
1866
+ }
1867
+ /** The protocol member's root entry, one re-export per module. */
1868
+ function protocolIndexFile(composition) {
1869
+ const modules = [
1870
+ './generated-hydranium/languages.js',
1871
+ './generated-hydranium/transfer-model.js',
1872
+ ...(headPortConstants(composition).length ? ['./head-ports.js'] : []),
1873
+ ...composition.grammars.filter(grammar => grammar.diagram).map(grammar => `./${grammar.grammarId}-diagram-types.js`)
1874
+ ];
1875
+ const text = `// What the __NAME__ server and its clients both import. A client depends on
1876
+ // this package instead of the server, so it never installs Langium to reach a
1877
+ // constant or a wire type.
1878
+ //
1879
+ // The server's \`generate\` writes \`generated-hydranium/\` from the grammar;
1880
+ // commit those modules with the grammar change that produced them.
1881
+
1882
+ ${modules.map(module => `export * from '${module}';`).join('\n')}
1883
+ `;
1884
+ return project(text, composition);
1885
+ }
1886
+ /** The protocol member's README. */
1887
+ function protocolReadme(composition) {
1888
+ const lines = [
1889
+ `# ${memberPackageName(composition, 'protocol')}`,
1890
+ '',
1891
+ 'What the __NAME__ language server and its clients both import, so a client',
1892
+ 'never depends on the server or installs Langium.',
1893
+ '',
1894
+ '- `src/generated-hydranium/languages.ts` — each language id and its file extensions',
1895
+ '- `src/generated-hydranium/transfer-model.ts` — the wire types of the model, references as plain strings',
1896
+ ...(headPortConstants(composition).length === 0
1897
+ ? []
1898
+ : composition.heads.includes('lsp')
1899
+ ? ['- `src/head-ports.ts` — the LSP requests a host sends to find the socket heads']
1900
+ : ["- `src/head-ports.ts` — the environment variables naming the socket heads' ports"]),
1901
+ ...composition.grammars
1902
+ .filter(grammar => grammar.diagram)
1903
+ .map(grammar => `- \`src/${grammar.grammarId}-diagram-types.ts\` — the \`${grammar.grammar}\` diagram's type ids`),
1904
+ '',
1905
+ "The server's `npm run generate` writes `src/generated-hydranium/` from the",
1906
+ 'grammar, and `npm run build` here runs it first. Commit the generated',
1907
+ 'modules with the grammar change that produced them.',
1908
+ ''
1909
+ ];
1910
+ return project(lines.join('\n'), composition);
1911
+ }
1912
+ /** The protocol member: everything the server and its clients both import. */
1913
+ function protocolFiles(composition) {
1914
+ return [
1915
+ { path: 'protocol/package.json', content: protocolPackageJson(composition) },
1916
+ { path: 'protocol/tsconfig.json', content: tsconfigJson(forMember(composition, 'protocol'), PROTOCOL_COMPILER_OPTIONS, '["src"]') },
1917
+ ignoreFile('protocol'),
1918
+ { path: 'protocol/README.md', content: protocolReadme(composition) },
1919
+ { path: 'protocol/src/index.ts', content: protocolIndexFile(composition) },
1920
+ ...(headPortConstants(composition).length ? [{ path: 'protocol/src/head-ports.ts', content: headPortsFile(composition) }] : []),
1921
+ ...composition.grammars
1922
+ .filter(grammar => grammar.diagram)
1923
+ .map(grammar => grammarTemplate('protocol/src/__GRAMMAR_ID__-diagram-types.ts', composition, grammar))
1924
+ ];
1925
+ }
1926
+ /**
1927
+ * The starter model files every host and the CLI open, two per language: one
1928
+ * declaring nodes, one referencing a node in the other file.
1929
+ *
1930
+ * Named by extension alone, since `init` refuses two grammars sharing one.
1931
+ */
1932
+ function workspaceFiles(composition) {
1933
+ return composition.grammars.flatMap(grammar => [
1934
+ grammarTemplate('workspace/nodes.__EXTENSION__', composition, grammar),
1935
+ grammarTemplate('workspace/references.__EXTENSION__', composition, grammar)
1936
+ ]);
1937
+ }
1938
+ /**
1939
+ * A standalone project's root: the members as npm workspaces.
1940
+ *
1941
+ * Always private: it is never published, whatever `--public` makes of the
1942
+ * members. npm runs a script across the members in the order listed here, not
1943
+ * by their dependencies; listing protocol first is safe because its `build`
1944
+ * runs the server's `generate` before compiling.
1945
+ */
1946
+ function rootFiles(composition) {
1947
+ const content = `{
1948
+ "name": "${composition.projectId}-root",
1949
+ "version": "0.0.0",
1950
+ "private": true,
1951
+ "workspaces": [
1952
+ "protocol",
1953
+ "server"
1954
+ ],
1955
+ "scripts": {
1956
+ "build": "npm run build --workspaces",
1957
+ "clean": "npm run clean --workspaces --if-present",
1958
+ "test": "npm test --workspaces --if-present"
1959
+ },
1960
+ "engines": {
1961
+ "node": ">=22.13"
1962
+ }
1963
+ }
1964
+ `;
1965
+ return [{ path: 'package.json', content }, ignoreFile('')];
1966
+ }
1967
+ /**
1968
+ * Every file the scaffold emits, fully rendered, at its path in the project
1969
+ * directory. A workspace member's project gets no root of its own: it joins the
1970
+ * surrounding workspace, whose manifest `init` prints the changes for.
1971
+ */
1972
+ export function buildInitTemplates(composition) {
1973
+ return [
1974
+ ...(composition.packaging.workspace === undefined ? rootFiles(composition) : []),
1975
+ ...protocolFiles(composition),
1976
+ ...serverFiles(composition),
1977
+ ...workspaceFiles(composition)
1923
1978
  ];
1924
1979
  }
1925
1980
  //# sourceMappingURL=init-templates.js.map