@hydranium/cli 1.0.0-next.42 → 1.0.0-next.420

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 (371) hide show
  1. package/README.md +125 -204
  2. package/bin/hydranium-cli.js +13 -0
  3. package/heap-analysis/heap/core/load.mjs +2 -2
  4. package/heap-analysis/heap/langium/langium.classifier.mjs +22 -11
  5. package/lib/cli-version.js.map +1 -1
  6. package/lib/cli.js +11 -2
  7. package/lib/cli.js.map +1 -1
  8. package/lib/commands/analyze-heap-args.d.ts.map +1 -1
  9. package/lib/commands/analyze-heap-args.js +1 -2
  10. package/lib/commands/analyze-heap-args.js.map +1 -1
  11. package/lib/commands/analyze-heap.d.ts +3 -0
  12. package/lib/commands/analyze-heap.d.ts.map +1 -1
  13. package/lib/commands/analyze-heap.js +2 -1
  14. package/lib/commands/analyze-heap.js.map +1 -1
  15. package/lib/commands/ast-ground-truth-args.d.ts +3 -2
  16. package/lib/commands/ast-ground-truth-args.d.ts.map +1 -1
  17. package/lib/commands/ast-ground-truth-args.js +12 -10
  18. package/lib/commands/ast-ground-truth-args.js.map +1 -1
  19. package/lib/commands/ast-ground-truth-driver.js +4 -2
  20. package/lib/commands/ast-ground-truth-driver.js.map +1 -1
  21. package/lib/commands/ast-ground-truth.js +1 -1
  22. package/lib/commands/ast-ground-truth.js.map +1 -1
  23. package/lib/commands/check-skills-args.d.ts +11 -0
  24. package/lib/commands/check-skills-args.d.ts.map +1 -0
  25. package/lib/commands/check-skills-args.js +71 -0
  26. package/lib/commands/check-skills-args.js.map +1 -0
  27. package/lib/commands/client-session.d.ts +17 -0
  28. package/lib/commands/client-session.d.ts.map +1 -0
  29. package/lib/commands/client-session.js +27 -0
  30. package/lib/commands/client-session.js.map +1 -0
  31. package/lib/commands/create-args.d.ts +17 -0
  32. package/lib/commands/create-args.d.ts.map +1 -0
  33. package/lib/commands/create-args.js +53 -0
  34. package/lib/commands/create-args.js.map +1 -0
  35. package/lib/commands/create.d.ts +38 -0
  36. package/lib/commands/create.d.ts.map +1 -0
  37. package/lib/commands/create.js +43 -0
  38. package/lib/commands/create.js.map +1 -0
  39. package/lib/commands/delete-args.d.ts +17 -0
  40. package/lib/commands/delete-args.d.ts.map +1 -0
  41. package/lib/commands/delete-args.js +49 -0
  42. package/lib/commands/delete-args.js.map +1 -0
  43. package/lib/commands/delete.d.ts +30 -0
  44. package/lib/commands/delete.d.ts.map +1 -0
  45. package/lib/commands/delete.js +32 -0
  46. package/lib/commands/delete.js.map +1 -0
  47. package/lib/commands/generate-ast-builder.d.ts +1 -1
  48. package/lib/commands/generate-ast-builder.d.ts.map +1 -1
  49. package/lib/commands/generate-ast-builder.js +2 -11
  50. package/lib/commands/generate-ast-builder.js.map +1 -1
  51. package/lib/commands/generate-languages.d.ts +35 -0
  52. package/lib/commands/generate-languages.d.ts.map +1 -0
  53. package/lib/commands/generate-languages.js +105 -0
  54. package/lib/commands/generate-languages.js.map +1 -0
  55. package/lib/commands/generate-transfer-builder.d.ts +33 -0
  56. package/lib/commands/generate-transfer-builder.d.ts.map +1 -0
  57. package/lib/commands/generate-transfer-builder.js +86 -0
  58. package/lib/commands/generate-transfer-builder.js.map +1 -0
  59. package/lib/commands/generate-transfer-model-args.d.ts.map +1 -1
  60. package/lib/commands/generate-transfer-model-args.js +20 -5
  61. package/lib/commands/generate-transfer-model-args.js.map +1 -1
  62. package/lib/commands/generate-transfer-model-config.d.ts.map +1 -1
  63. package/lib/commands/generate-transfer-model-config.js +8 -3
  64. package/lib/commands/generate-transfer-model-config.js.map +1 -1
  65. package/lib/commands/generate-transfer-model.d.ts +41 -9
  66. package/lib/commands/generate-transfer-model.d.ts.map +1 -1
  67. package/lib/commands/generate-transfer-model.js +225 -68
  68. package/lib/commands/generate-transfer-model.js.map +1 -1
  69. package/lib/commands/generated-module-path.d.ts +16 -0
  70. package/lib/commands/generated-module-path.d.ts.map +1 -0
  71. package/lib/commands/generated-module-path.js +21 -0
  72. package/lib/commands/generated-module-path.js.map +1 -0
  73. package/lib/commands/harness-args.d.ts +46 -43
  74. package/lib/commands/harness-args.d.ts.map +1 -1
  75. package/lib/commands/harness-args.js +170 -29
  76. package/lib/commands/harness-args.js.map +1 -1
  77. package/lib/commands/headless-harness.d.ts +37 -20
  78. package/lib/commands/headless-harness.d.ts.map +1 -1
  79. package/lib/commands/headless-harness.js +71 -19
  80. package/lib/commands/headless-harness.js.map +1 -1
  81. package/lib/commands/init-args.d.ts +5 -18
  82. package/lib/commands/init-args.d.ts.map +1 -1
  83. package/lib/commands/init-args.js +131 -35
  84. package/lib/commands/init-args.js.map +1 -1
  85. package/lib/commands/init-hosts.d.ts +24 -0
  86. package/lib/commands/init-hosts.d.ts.map +1 -0
  87. package/lib/commands/init-hosts.js +37 -0
  88. package/lib/commands/init-hosts.js.map +1 -0
  89. package/lib/commands/init-prompt.d.ts +3 -3
  90. package/lib/commands/init-prompt.d.ts.map +1 -1
  91. package/lib/commands/init-prompt.js +4 -5
  92. package/lib/commands/init-prompt.js.map +1 -1
  93. package/lib/commands/init-skills.d.ts +41 -0
  94. package/lib/commands/init-skills.d.ts.map +1 -0
  95. package/lib/commands/init-skills.js +110 -0
  96. package/lib/commands/init-skills.js.map +1 -0
  97. package/lib/commands/init-templates.d.ts +20 -14
  98. package/lib/commands/init-templates.d.ts.map +1 -1
  99. package/lib/commands/init-templates.js +3107 -759
  100. package/lib/commands/init-templates.js.map +1 -1
  101. package/lib/commands/init-wizard.d.ts +10 -0
  102. package/lib/commands/init-wizard.d.ts.map +1 -1
  103. package/lib/commands/init-wizard.js +76 -31
  104. package/lib/commands/init-wizard.js.map +1 -1
  105. package/lib/commands/init-workspace.d.ts +13 -11
  106. package/lib/commands/init-workspace.d.ts.map +1 -1
  107. package/lib/commands/init-workspace.js +21 -7
  108. package/lib/commands/init-workspace.js.map +1 -1
  109. package/lib/commands/init.d.ts +61 -24
  110. package/lib/commands/init.d.ts.map +1 -1
  111. package/lib/commands/init.js +244 -50
  112. package/lib/commands/init.js.map +1 -1
  113. package/lib/commands/kebab.d.ts +15 -0
  114. package/lib/commands/kebab.d.ts.map +1 -0
  115. package/lib/commands/kebab.js +17 -0
  116. package/lib/commands/kebab.js.map +1 -0
  117. package/lib/commands/lint-grammar-args.d.ts +3 -2
  118. package/lib/commands/lint-grammar-args.d.ts.map +1 -1
  119. package/lib/commands/lint-grammar-args.js +14 -11
  120. package/lib/commands/lint-grammar-args.js.map +1 -1
  121. package/lib/commands/lint-grammar-driver.js +2 -1
  122. package/lib/commands/lint-grammar-driver.js.map +1 -1
  123. package/lib/commands/lint-grammar-report.js.map +1 -1
  124. package/lib/commands/lint-grammar.js +1 -1
  125. package/lib/commands/lint-grammar.js.map +1 -1
  126. package/lib/commands/measure-memory-args.d.ts +3 -2
  127. package/lib/commands/measure-memory-args.d.ts.map +1 -1
  128. package/lib/commands/measure-memory-args.js +26 -14
  129. package/lib/commands/measure-memory-args.js.map +1 -1
  130. package/lib/commands/measure-memory-driver.js +5 -3
  131. package/lib/commands/measure-memory-driver.js.map +1 -1
  132. package/lib/commands/measure-memory.d.ts +1 -1
  133. package/lib/commands/measure-memory.d.ts.map +1 -1
  134. package/lib/commands/measure-memory.js +6 -3
  135. package/lib/commands/measure-memory.js.map +1 -1
  136. package/lib/commands/message-catalogue-args.d.ts +16 -0
  137. package/lib/commands/message-catalogue-args.d.ts.map +1 -0
  138. package/lib/commands/message-catalogue-args.js +53 -0
  139. package/lib/commands/message-catalogue-args.js.map +1 -0
  140. package/lib/commands/message-catalogue.d.ts +21 -0
  141. package/lib/commands/message-catalogue.d.ts.map +1 -0
  142. package/lib/commands/message-catalogue.js +48 -0
  143. package/lib/commands/message-catalogue.js.map +1 -0
  144. package/lib/commands/model-docs-args.d.ts +4 -3
  145. package/lib/commands/model-docs-args.d.ts.map +1 -1
  146. package/lib/commands/model-docs-args.js +13 -11
  147. package/lib/commands/model-docs-args.js.map +1 -1
  148. package/lib/commands/model-docs-driver.js +2 -1
  149. package/lib/commands/model-docs-driver.js.map +1 -1
  150. package/lib/commands/model-docs.js +1 -1
  151. package/lib/commands/model-docs.js.map +1 -1
  152. package/lib/commands/projects-args.d.ts.map +1 -1
  153. package/lib/commands/projects-args.js +5 -5
  154. package/lib/commands/projects-args.js.map +1 -1
  155. package/lib/commands/projects.d.ts +3 -4
  156. package/lib/commands/projects.d.ts.map +1 -1
  157. package/lib/commands/projects.js +2 -2
  158. package/lib/commands/projects.js.map +1 -1
  159. package/lib/commands/query-args.d.ts.map +1 -1
  160. package/lib/commands/query-args.js +6 -5
  161. package/lib/commands/query-args.js.map +1 -1
  162. package/lib/commands/query.d.ts +6 -5
  163. package/lib/commands/query.d.ts.map +1 -1
  164. package/lib/commands/query.js +5 -4
  165. package/lib/commands/query.js.map +1 -1
  166. package/lib/commands/reflect-args.d.ts +3 -2
  167. package/lib/commands/reflect-args.d.ts.map +1 -1
  168. package/lib/commands/reflect-args.js +14 -11
  169. package/lib/commands/reflect-args.js.map +1 -1
  170. package/lib/commands/reflect-driver.js +2 -1
  171. package/lib/commands/reflect-driver.js.map +1 -1
  172. package/lib/commands/reflect-report.js.map +1 -1
  173. package/lib/commands/reflect.js +1 -1
  174. package/lib/commands/reflect.js.map +1 -1
  175. package/lib/commands/save-args.d.ts.map +1 -1
  176. package/lib/commands/save-args.js +7 -4
  177. package/lib/commands/save-args.js.map +1 -1
  178. package/lib/commands/save.d.ts +8 -4
  179. package/lib/commands/save.d.ts.map +1 -1
  180. package/lib/commands/save.js +12 -23
  181. package/lib/commands/save.js.map +1 -1
  182. package/lib/commands/skills.d.ts +22 -0
  183. package/lib/commands/skills.d.ts.map +1 -0
  184. package/lib/commands/skills.js +129 -0
  185. package/lib/commands/skills.js.map +1 -0
  186. package/lib/commands/validate-args.d.ts +3 -2
  187. package/lib/commands/validate-args.d.ts.map +1 -1
  188. package/lib/commands/validate-args.js +14 -11
  189. package/lib/commands/validate-args.js.map +1 -1
  190. package/lib/commands/validate-driver.js +2 -1
  191. package/lib/commands/validate-driver.js.map +1 -1
  192. package/lib/commands/validate-report.js.map +1 -1
  193. package/lib/commands/validate.js +1 -1
  194. package/lib/commands/validate.js.map +1 -1
  195. package/lib/commands/watch-args.d.ts.map +1 -1
  196. package/lib/commands/watch-args.js +6 -4
  197. package/lib/commands/watch-args.js.map +1 -1
  198. package/lib/commands/watch.d.ts +3 -4
  199. package/lib/commands/watch.d.ts.map +1 -1
  200. package/lib/commands/watch.js +5 -2
  201. package/lib/commands/watch.js.map +1 -1
  202. package/lib/driver-heap.d.ts +52 -0
  203. package/lib/driver-heap.d.ts.map +1 -0
  204. package/lib/driver-heap.js +51 -0
  205. package/lib/driver-heap.js.map +1 -0
  206. package/lib/index.d.ts +3 -0
  207. package/lib/index.d.ts.map +1 -1
  208. package/lib/index.js +3 -0
  209. package/lib/index.js.map +1 -1
  210. package/lib/log-level.d.ts +21 -11
  211. package/lib/log-level.d.ts.map +1 -1
  212. package/lib/log-level.js +25 -14
  213. package/lib/log-level.js.map +1 -1
  214. package/lib/spawn-data-server.d.ts +9 -4
  215. package/lib/spawn-data-server.d.ts.map +1 -1
  216. package/lib/spawn-data-server.js +5 -4
  217. package/lib/spawn-data-server.js.map +1 -1
  218. package/lib/stderr-connection-logger.d.ts +12 -0
  219. package/lib/stderr-connection-logger.d.ts.map +1 -0
  220. package/lib/stderr-connection-logger.js +16 -0
  221. package/lib/stderr-connection-logger.js.map +1 -0
  222. package/lib/testing/echo-server.js +9 -1
  223. package/lib/testing/echo-server.js.map +1 -1
  224. package/package.json +99 -41
  225. package/src/cli.ts +11 -2
  226. package/src/commands/analyze-heap-args.ts +1 -2
  227. package/src/commands/analyze-heap.ts +4 -1
  228. package/src/commands/ast-ground-truth-args.ts +16 -13
  229. package/src/commands/ast-ground-truth-driver.ts +4 -2
  230. package/src/commands/ast-ground-truth.ts +1 -1
  231. package/src/commands/check-skills-args.ts +70 -0
  232. package/src/commands/client-session.ts +33 -0
  233. package/src/commands/create-args.ts +68 -0
  234. package/src/commands/create.ts +83 -0
  235. package/src/commands/delete-args.ts +64 -0
  236. package/src/commands/delete.ts +61 -0
  237. package/src/commands/generate-ast-builder.ts +2 -12
  238. package/src/commands/generate-languages.ts +132 -0
  239. package/src/commands/generate-transfer-builder.ts +101 -0
  240. package/src/commands/generate-transfer-model-args.ts +20 -5
  241. package/src/commands/generate-transfer-model-config.ts +8 -3
  242. package/src/commands/generate-transfer-model.ts +297 -66
  243. package/src/commands/generated-module-path.ts +22 -0
  244. package/src/commands/harness-args.ts +163 -33
  245. package/src/commands/headless-harness.ts +89 -28
  246. package/src/commands/init-args.ts +135 -35
  247. package/src/commands/init-hosts.ts +51 -0
  248. package/src/commands/init-prompt.ts +5 -6
  249. package/src/commands/init-skills.ts +129 -0
  250. package/src/commands/init-templates.ts +3300 -785
  251. package/src/commands/init-wizard.ts +97 -32
  252. package/src/commands/init-workspace.ts +31 -16
  253. package/src/commands/init.ts +311 -65
  254. package/src/commands/kebab.ts +17 -0
  255. package/src/commands/lint-grammar-args.ts +18 -14
  256. package/src/commands/lint-grammar-driver.ts +2 -1
  257. package/src/commands/lint-grammar.ts +1 -1
  258. package/src/commands/measure-memory-args.ts +31 -17
  259. package/src/commands/measure-memory-driver.ts +5 -3
  260. package/src/commands/measure-memory.ts +6 -3
  261. package/src/commands/message-catalogue-args.ts +55 -0
  262. package/src/commands/message-catalogue.ts +56 -0
  263. package/src/commands/model-docs-args.ts +17 -14
  264. package/src/commands/model-docs-driver.ts +2 -1
  265. package/src/commands/model-docs.ts +1 -1
  266. package/src/commands/projects-args.ts +5 -4
  267. package/src/commands/projects.ts +4 -6
  268. package/src/commands/query-args.ts +6 -4
  269. package/src/commands/query.ts +9 -9
  270. package/src/commands/reflect-args.ts +18 -14
  271. package/src/commands/reflect-driver.ts +2 -1
  272. package/src/commands/reflect.ts +1 -1
  273. package/src/commands/save-args.ts +7 -3
  274. package/src/commands/save.ts +24 -27
  275. package/src/commands/skills.ts +146 -0
  276. package/src/commands/validate-args.ts +18 -14
  277. package/src/commands/validate-driver.ts +2 -1
  278. package/src/commands/validate.ts +1 -1
  279. package/src/commands/watch-args.ts +6 -3
  280. package/src/commands/watch.ts +7 -6
  281. package/src/driver-heap.ts +65 -0
  282. package/src/index.ts +3 -0
  283. package/src/log-level.ts +42 -14
  284. package/src/spawn-data-server.ts +17 -5
  285. package/src/stderr-connection-logger.ts +18 -0
  286. package/src/testing/echo-server.ts +11 -2
  287. package/templates/client/gitignore +3 -0
  288. package/templates/client/src/__GRAMMAR_ID__-diagram-module.ts +82 -0
  289. package/templates/editorconfig +15 -0
  290. package/templates/gitignore +1 -0
  291. package/templates/host-browser/esbuild.mjs +82 -0
  292. package/templates/host-browser/gitignore +4 -0
  293. package/templates/host-browser/hydranium-mark.svg +40 -0
  294. package/templates/host-browser/index.html +287 -0
  295. package/templates/host-browser/playwright.config.mts +24 -0
  296. package/templates/host-browser/src/data-status.ts +15 -0
  297. package/templates/host-browser/src/diagram-save.ts +60 -0
  298. package/templates/host-browser/src/diagram.ts +121 -0
  299. package/templates/host-browser/src/editor.ts +116 -0
  300. package/templates/host-browser/src/log.ts +14 -0
  301. package/templates/host-browser/src/monaco-editor-core-worker.d.ts +7 -0
  302. package/templates/host-browser/src/monaco-editor-worker.ts +10 -0
  303. package/templates/host-browser/src/posix-path-shim.ts +23 -0
  304. package/templates/host-browser/src/shown-file.ts +61 -0
  305. package/templates/host-browser/src/workspace.ts +5 -0
  306. package/templates/host-browser/tsconfig.e2e.json +16 -0
  307. package/templates/host-browser/tsconfig.page.json +16 -0
  308. package/templates/host-browser/tsconfig.worker.json +16 -0
  309. package/templates/host-theia-browser/esbuild.mjs +24 -0
  310. package/templates/host-theia-browser/gitignore +9 -0
  311. package/templates/host-theia-browser/playwright.config.mts +42 -0
  312. package/templates/host-theia-browser/start.mjs +50 -0
  313. package/templates/host-theia-browser/tsconfig.e2e.json +16 -0
  314. package/templates/host-theia-electron/esbuild.mjs +27 -0
  315. package/templates/host-theia-electron/gitignore +8 -0
  316. package/templates/host-theia-electron/playwright.config.mts +12 -0
  317. package/templates/host-theia-electron/start.mjs +47 -0
  318. package/templates/host-theia-electron/tsconfig.e2e.json +16 -0
  319. package/templates/host-vscode/esbuild.mjs +38 -0
  320. package/templates/host-vscode/gitignore +7 -0
  321. package/templates/host-vscode/playwright.config.mts +14 -0
  322. package/templates/host-vscode/src/__GRAMMAR_ID__-diagram-editor.ts +19 -0
  323. package/templates/host-vscode/src/show-model-command.ts +24 -0
  324. package/templates/host-vscode/src/webview/__GRAMMAR_ID__-diagram.ts +18 -0
  325. package/templates/host-vscode/src/webview/diagram.css +40 -0
  326. package/templates/host-vscode/start.mjs +22 -0
  327. package/templates/host-vscode/test/content-server.cjs +40 -0
  328. package/templates/host-vscode/test/install-vscode.mts +21 -0
  329. package/templates/host-vscode/test/virtual-documents.spec.mts +28 -0
  330. package/templates/host-vscode/test/vscode-version.mts +10 -0
  331. package/templates/host-vscode/tsconfig.e2e.json +17 -0
  332. package/templates/host-vscode/tsconfig.json +19 -0
  333. package/templates/host-vscode/tsconfig.webview.json +17 -0
  334. package/templates/oxlintrc.json +4 -0
  335. package/templates/protocol/gitignore +3 -0
  336. package/templates/protocol/src/__GRAMMAR_ID__-diagram-types.ts +47 -0
  337. package/templates/server/gitignore +6 -0
  338. package/templates/server/src/glsp/__GRAMMAR_ID__/create-node-operation-handler.ts +62 -0
  339. package/templates/server/src/glsp/__GRAMMAR_ID__/delete-operation-handler.ts +63 -0
  340. package/templates/server/src/glsp/__GRAMMAR_ID__/gmodel-factory.ts +144 -0
  341. package/templates/server/src/glsp/__GRAMMAR_ID__/state.ts +18 -0
  342. package/templates/server/src/glsp/__GRAMMAR_ID__/storage.ts +16 -0
  343. package/templates/server/src/glsp/__GRAMMAR_ID__/submission-handler.ts +24 -0
  344. package/templates/server/src/grammar/common.langium +12 -0
  345. package/templates/server/src/language-server/__GRAMMAR_ID__-semantic-tokens.ts +10 -0
  346. package/templates/server/src/language-server/__GRAMMAR_ID__-serializer.ts +53 -0
  347. package/templates/server/src/language-server/__GRAMMAR_ID__-validation.ts +30 -0
  348. package/templates/server/src/language-server/ast.ts +23 -0
  349. package/templates/server/src/node/services.ts +16 -0
  350. package/templates/server/test/__GRAMMAR_ID__-diagram.test.ts +403 -0
  351. package/templates/server/test/__GRAMMAR_ID__-semantic-tokens.test.ts +34 -0
  352. package/templates/server/test/tsconfig.json +4 -0
  353. package/templates/server/tsconfig.test.json +10 -0
  354. package/templates/server/vitest.config.ts +14 -0
  355. package/templates/theia/gitignore +3 -0
  356. package/templates/theia/src/browser/__GRAMMAR_ID__-diagram-frontend-module.ts +65 -0
  357. package/templates/theia/src/browser/__PROJECT_ID__-connection-preload-module.ts +15 -0
  358. package/templates/theia/src/browser/__PROJECT_ID__-data-connection.ts +15 -0
  359. package/templates/theia/src/browser/__PROJECT_ID__-data-port.ts +11 -0
  360. package/templates/theia/src/browser/__PROJECT_ID__-glsp-client-contribution.ts +13 -0
  361. package/templates/theia/src/browser/__PROJECT_ID__-show-model-command.ts +51 -0
  362. package/templates/theia/src/common/__PROJECT_ID__-glsp-contribution.ts +3 -0
  363. package/templates/theia/src/node/__PROJECT_ID__-connection-backend-module.ts +12 -0
  364. package/templates/theia/src/node/__PROJECT_ID__-data-backend-module.ts +15 -0
  365. package/templates/theia/src/node/__PROJECT_ID__-glsp-backend-module.ts +16 -0
  366. package/templates/theia/style/diagram.css +20 -0
  367. package/templates/theia/tsconfig.json +24 -0
  368. package/templates/vscode-servers/gitignore +4 -0
  369. package/templates/vscode-servers/tsconfig.json +19 -0
  370. package/templates/workspace/nodes.__EXTENSION__ +5 -0
  371. package/templates/workspace/references.__EXTENSION__ +2 -0
@@ -7,18 +7,22 @@
7
7
  * SPDX-License-Identifier: MIT
8
8
  ********************************************************************************/
9
9
 
10
- import * as fs from 'fs';
10
+ import * as fs from 'node:fs';
11
11
  import * as path from 'path';
12
12
  import { emitAstBuilder } from './generate-ast-builder.js';
13
+ import { emitLanguages } from './generate-languages.js';
14
+ import { emitTransferBuilder } from './generate-transfer-builder.js';
13
15
  import {
14
16
  type Node,
15
17
  type ObjectLiteralExpression,
16
18
  Project,
17
19
  SyntaxKind,
20
+ ts,
18
21
  type ClassDeclaration,
19
22
  type InterfaceDeclaration,
20
23
  type SourceFile
21
24
  } from 'ts-morph';
25
+ import { describeError } from '@hydranium/protocol';
22
26
 
23
27
  /**
24
28
  * Options for the transfer-model generator.
@@ -45,6 +49,28 @@ export interface GenerateTransferModelOptions {
45
49
  * and not the other has the two disagreeing about the same grammar.
46
50
  */
47
51
  astBuilderFile?: string;
52
+ /**
53
+ * Destination for a generated test module binding `makeTransferNodeBuilder`
54
+ * from `@hydranium/protocol/testing` to the transfer model, as `transferNode`,
55
+ * the way {@link astBuilderFile} binds `astNode`. Belongs in a test tree:
56
+ * production code has no use for a builder that fills grammar defaults.
57
+ *
58
+ * When it sits in another package than {@link outFile}, it imports the model
59
+ * by that package's name, so the package's main entry must re-export the
60
+ * generated model, and with it each type's constant: an interface the entry
61
+ * narrows hides the generated constant of the same name unless the entry
62
+ * re-exports that constant too, and the builder keeps the generated
63
+ * interface unless the entry declares `<LanguageId>TransferTypes` with the
64
+ * narrowed entry.
65
+ */
66
+ transferBuilderFile?: string;
67
+ /**
68
+ * Destination for a generated module stating each grammar's language id and
69
+ * file extensions, read from the `module.ts` beside {@link astFile}. Lets a
70
+ * package without Langium, such as one shared with clients, name the ids
71
+ * without copying them by hand.
72
+ */
73
+ languagesFile?: string;
48
74
  /** Name used for the base element type in the output. Defaults to `TransferElement`. */
49
75
  elementTypeName?: string;
50
76
  /** Name used for the terminal-patterns const in the output. Defaults to `ModelTerminals`. */
@@ -86,6 +112,15 @@ interface AugmentedProperty {
86
112
 
87
113
  interface ReflectionEntry {
88
114
  superTypes: string[];
115
+ properties: ReflectionProperty[];
116
+ }
117
+
118
+ /** One grammar property of a reflection entry; `defaultValue` keeps the source text of the literal. */
119
+ interface ReflectionProperty {
120
+ name: string;
121
+ defaultValue?: string;
122
+ optional: boolean;
123
+ referenceType?: string;
89
124
  }
90
125
 
91
126
  const DEFAULT_ELEMENT_TYPE_NAME = 'TransferElement';
@@ -98,38 +133,72 @@ const DEFAULT_REGEN_COMMAND = 'Run the hydranium-cli generate-transfer-model com
98
133
  */
99
134
  const NAMESPACED_TERMINALS_NAME = 'Terminals';
100
135
 
136
+ /** How often `--watch` polls its input files. */
137
+ const WATCH_POLL_INTERVAL_MS = 250;
138
+ const WATCH_DEBOUNCE_MS = 150;
139
+
101
140
  /**
102
- * Generate once, then re-generate whenever the AST or augmentation file changes,
141
+ * Generate once, then re-generate whenever an input file changes (the AST, the
142
+ * augmentation file, and `module.ts` when a languages file is written),
103
143
  * until `signal` aborts (the CLI wires this to SIGINT). Regeneration is debounced
104
144
  * so an editor's multi-event save fires a single run, and a generation error is
105
145
  * logged without tearing down the watch. Resolves when the signal aborts.
146
+ * An already-aborted signal resolves immediately without generating or watching.
147
+ *
148
+ * Polled by path rather than watched through OS events: langium-cli removes
149
+ * its output directory and writes the files back, which an event watch, Node's
150
+ * and chokidar's alike, stops hearing after the first run. A run that finds an
151
+ * input missing is skipped, since the file's return is a change of its own, so
152
+ * nothing generates from a half-written output.
106
153
  */
107
154
  export function watchTransferModel(options: GenerateTransferModelOptions, context: { signal: AbortSignal }): Promise<void> {
155
+ if (context.signal.aborted) {
156
+ return Promise.resolve();
157
+ }
108
158
  const runSafely = (): void => {
109
159
  try {
110
160
  generateTransferModel(options);
111
161
  } catch (error: unknown) {
112
- console.error(`Transfer-model generation failed: ${error instanceof Error ? error.message : String(error)}`);
162
+ console.error(`Transfer-model generation failed: ${describeError(error)}`);
113
163
  }
114
164
  };
115
165
  runSafely();
116
- console.log(`Watching ${options.astFile} and ${options.augmentationFile} for changes. Press Ctrl-C to stop.`);
166
+ // A grammar's language id and extensions reach only `module.ts`, so the
167
+ // languages module would go stale without it.
168
+ const watched = [
169
+ options.astFile,
170
+ options.augmentationFile,
171
+ ...(options.languagesFile !== undefined ? [languagesModuleFile(options)] : [])
172
+ ];
117
173
 
118
174
  let timer: NodeJS.Timeout | undefined;
119
- const scheduleRerun = (): void => {
175
+ const rerun = (): void => {
176
+ if (watched.every(file => fs.existsSync(file))) {
177
+ runSafely();
178
+ }
179
+ };
180
+ // The poll reports every stat it takes; a replaced file has a new inode, a
181
+ // rewritten one a new mtime, a removed or returning one zeroed stats.
182
+ const onPoll = (current: fs.Stats, previous: fs.Stats): void => {
183
+ if (current.ino === previous.ino && current.mtimeMs === previous.mtimeMs && current.size === previous.size) {
184
+ return;
185
+ }
120
186
  if (timer) {
121
187
  clearTimeout(timer);
122
188
  }
123
- timer = setTimeout(runSafely, 150);
189
+ timer = setTimeout(rerun, WATCH_DEBOUNCE_MS);
124
190
  };
125
- const watchers = [options.astFile, options.augmentationFile].map(file => fs.watch(file, scheduleRerun));
191
+ for (const file of watched) {
192
+ fs.watchFile(file, { interval: WATCH_POLL_INTERVAL_MS }, onPoll);
193
+ }
194
+ console.log(`Watching ${watched.join(', ')} for changes. Press Ctrl-C to stop.`);
126
195
 
127
196
  return new Promise<void>(resolve => {
128
197
  context.signal.addEventListener(
129
198
  'abort',
130
199
  () => {
131
- for (const watcher of watchers) {
132
- watcher.close();
200
+ for (const file of watched) {
201
+ fs.unwatchFile(file, onPoll);
133
202
  }
134
203
  if (timer) {
135
204
  clearTimeout(timer);
@@ -141,6 +210,11 @@ export function watchTransferModel(options: GenerateTransferModelOptions, contex
141
210
  });
142
211
  }
143
212
 
213
+ /** The langium-cli `module.ts` beside the AST file, which the languages module is read from. */
214
+ function languagesModuleFile(options: GenerateTransferModelOptions): string {
215
+ return path.join(path.dirname(options.astFile), 'module.ts');
216
+ }
217
+
144
218
  /** Run the transfer-model generator. */
145
219
  export function generateTransferModel(options: GenerateTransferModelOptions): void {
146
220
  const elementTypeName = options.elementTypeName ?? DEFAULT_ELEMENT_TYPE_NAME;
@@ -178,15 +252,19 @@ export function generateTransferModel(options: GenerateTransferModelOptions): vo
178
252
  astInterfaces.set(iface.getName(), iface);
179
253
  }
180
254
 
181
- const typeAliases: Array<{ name: string; definition: string }> = [];
255
+ const typeAliases: Array<{ name: string; definition: string; members: string[] }> = [];
182
256
  for (const alias of astSource.getTypeAliases()) {
183
257
  const name = alias.getName();
184
258
  if (skipTypeAliases.has(name)) {
185
259
  continue;
186
260
  }
187
- const typeText = alias.getTypeNode()?.getText();
188
- if (typeText) {
189
- typeAliases.push({ name, definition: mapType(resolveNamespacedStringUnion(astSource, typeText), elementTypeName) });
261
+ const typeNode = alias.getTypeNode();
262
+ if (typeNode) {
263
+ typeAliases.push({
264
+ name,
265
+ definition: mapType(resolveNamespacedStringUnion(astSource, typeNode.getText()), elementTypeName),
266
+ members: unionMembers(typeNode).map(member => mapType(member, elementTypeName))
267
+ });
190
268
  }
191
269
  }
192
270
 
@@ -203,7 +281,6 @@ export function generateTransferModel(options: GenerateTransferModelOptions): vo
203
281
  // 7. Classify interfaces: abstract (union $type) vs concrete (literal $type).
204
282
  const interfaceNames: string[] = [];
205
283
  const abstractTypes = new Set<string>();
206
- const concreteTypes = new Set<string>();
207
284
 
208
285
  for (const name of Object.keys(reflectionTypes)) {
209
286
  const iface = astInterfaces.get(name);
@@ -215,8 +292,6 @@ export function generateTransferModel(options: GenerateTransferModelOptions): vo
215
292
  const typeText = iface.getProperty('$type')?.getTypeNode()?.getText() ?? '';
216
293
  if (typeText.includes('|')) {
217
294
  abstractTypes.add(name);
218
- } else {
219
- concreteTypes.add(name);
220
295
  }
221
296
  }
222
297
 
@@ -258,12 +333,13 @@ export function generateTransferModel(options: GenerateTransferModelOptions): vo
258
333
  out.push('}');
259
334
  out.push('');
260
335
 
261
- // Type constants for concrete types.
336
+ // Langium's per-type constant under the same name, for every reflection type
337
+ // the model emits, abstract ones and unions included.
338
+ const reflectedTypes = Object.keys(reflectionTypes).filter(name => astInterfaces.has(name) || typeAliasNames.has(name));
262
339
  out.push('// --- Type Constants ---');
263
- for (const name of sorted) {
264
- if (concreteTypes.has(name)) {
265
- out.push(`export const ${name}Type = '${name}';`);
266
- }
340
+ for (const name of reflectedTypes) {
341
+ const keys = reflectionTypes[name].properties.map(property => `${property.name}: '${property.name}'`);
342
+ out.push(`export const ${name} = { ${[`$type: '${name}'`, ...keys].join(', ')} } as const;`);
267
343
  }
268
344
  out.push('');
269
345
 
@@ -274,7 +350,7 @@ export function generateTransferModel(options: GenerateTransferModelOptions): vo
274
350
  out.push(`export type ${name} = ${definition};`);
275
351
  const literals = extractStringLiterals(definition);
276
352
  if (literals) {
277
- const items = literals.map(l => `'${l}'`).join(', ');
353
+ const items = literals.map(quote).join(', ');
278
354
  out.push(`export const ${name}Values = [${items}] as const;`);
279
355
  }
280
356
  }
@@ -305,7 +381,7 @@ export function generateTransferModel(options: GenerateTransferModelOptions): vo
305
381
  const typeValue = iface.getProperty('$type')?.getTypeNode()?.getText() ?? `'${name}'`;
306
382
  out.push(` readonly $type: ${typeValue};`);
307
383
  } else {
308
- out.push(` readonly $type: typeof ${name}Type;`);
384
+ out.push(` readonly $type: '${name}';`);
309
385
  }
310
386
 
311
387
  const emitted = new Set<string>(['$type']);
@@ -367,24 +443,24 @@ export function generateTransferModel(options: GenerateTransferModelOptions): vo
367
443
  const checks = members.map(m => `item.$type === '${m}'`).join(' || ');
368
444
  out.push(` return is${elementTypeName}(item) && (${checks});`);
369
445
  } else {
370
- out.push(` return is${elementTypeName}(item) && item.$type === ${name}Type;`);
446
+ out.push(` return is${elementTypeName}(item) && item.$type === '${name}';`);
371
447
  }
372
448
  out.push('}');
373
449
  out.push('');
374
450
  }
375
451
 
376
- for (const { name, definition } of typeAliases) {
452
+ for (const { name, definition, members } of typeAliases) {
377
453
  const literals = extractStringLiterals(definition);
378
454
  if (literals) {
379
- const checks = literals.map(l => `item === '${l}'`).join(' || ');
455
+ const checks = literals.map(l => `item === ${quote(l)}`).join(' || ') || 'false';
380
456
  out.push(`export function is${name}(item: unknown): item is ${name} {`);
381
457
  out.push(` return ${checks};`);
382
458
  out.push('}');
383
459
  out.push('');
384
460
  } else {
385
- // Union (or single) of named members and/or Langium data-type primitives. Each member maps
386
- // to its own check (typeof / instanceof / named-guard) via typeAliasMemberGuard.
387
- const members = definition.split('|').map(s => s.trim());
461
+ // Union (or single) of named members, Langium data-type primitives and the string
462
+ // literals a data-type rule's keywords add beside them. Each member maps to its own
463
+ // check via typeAliasMemberGuard.
388
464
  const checks = members.map(typeAliasMemberGuard).join(' || ');
389
465
  out.push(`export function is${name}(item: unknown): item is ${name} {`);
390
466
  out.push(` return ${checks};`);
@@ -393,6 +469,9 @@ export function generateTransferModel(options: GenerateTransferModelOptions): vo
393
469
  }
394
470
  }
395
471
 
472
+ out.push(...renderTypeMap(languageId, reflectedTypes));
473
+ out.push(...renderReflection(languageId, elementTypeName, reflectedTypes, reflectionTypes));
474
+
396
475
  // Coverage check: warn on reflection types not emitted.
397
476
  const processedSet = new Set(sorted);
398
477
  for (const name of Object.keys(reflectionTypes)) {
@@ -422,6 +501,107 @@ export function generateTransferModel(options: GenerateTransferModelOptions): vo
422
501
  console.warn(`Warning: no '${languageId}AstType' alias in ${options.astFile}; skipping the AST builder.`);
423
502
  }
424
503
  }
504
+
505
+ if (options.transferBuilderFile !== undefined) {
506
+ emitTransferBuilder({ outFile: options.transferBuilderFile, transferModelFile: options.outFile, languageId, regenCommand });
507
+ }
508
+
509
+ if (options.languagesFile !== undefined) {
510
+ const moduleSource = project.addSourceFileAtPath(languagesModuleFile(options));
511
+ emitLanguages(moduleSource, { outFile: options.languagesFile, regenCommand });
512
+ }
513
+ }
514
+
515
+ /**
516
+ * `<LanguageId>TransferTypes`, each `$type` to its interface, so an encoder or a
517
+ * builder can type a node by the grammar the way Langium's `<LanguageId>AstType`
518
+ * does on the AST side.
519
+ */
520
+ function renderTypeMap(languageId: string, types: readonly string[]): string[] {
521
+ return ['// --- Type Map ---', `export type ${languageId}TransferTypes = {`, ...types.map(name => ` ${name}: ${name};`), '};', ''];
522
+ }
523
+
524
+ /**
525
+ * `<LanguageId>TransferReflection`, the grammar metadata with the method names
526
+ * Langium's `AstReflection` uses. A plain object with no framework import, so
527
+ * the file stays self-contained, and nothing references it but its importers:
528
+ * the `is` guards test `$type` directly, so a guard never pulls it in. `types`
529
+ * keeps its literal type, which is how the transfer-node builder tells which
530
+ * fields the grammar defaults.
531
+ */
532
+ function renderReflection(
533
+ languageId: string,
534
+ elementTypeName: string,
535
+ types: readonly string[],
536
+ reflectionTypes: Record<string, ReflectionEntry>
537
+ ): string[] {
538
+ const metaData = `${languageId}TransferTypeMetaData`;
539
+ // Index signatures, not `Readonly<Record<…>>`: a grammar type may be named
540
+ // `Record` or `Readonly`, which Langium does not reserve as it does `Object`.
541
+ const out = [
542
+ '// --- Reflection ---',
543
+ `export interface ${metaData} {`,
544
+ ' readonly name: string;',
545
+ ' readonly properties: {',
546
+ ' readonly [name: string]: { readonly name: string; readonly defaultValue?: unknown; readonly optional?: boolean; readonly referenceType?: string };',
547
+ ' };',
548
+ ' readonly superTypes: readonly string[];',
549
+ '}',
550
+ '',
551
+ `export const ${languageId}TransferReflection = {`,
552
+ ' types: {'
553
+ ];
554
+ for (const name of types) {
555
+ const { properties, superTypes } = reflectionTypes[name];
556
+ const propertyEntries = properties.map(property => {
557
+ const fields = [`name: '${property.name}'`];
558
+ if (property.defaultValue !== undefined) {
559
+ fields.push(`defaultValue: ${property.defaultValue}`);
560
+ }
561
+ if (property.optional) {
562
+ fields.push('optional: true');
563
+ }
564
+ if (property.referenceType !== undefined) {
565
+ fields.push(`referenceType: '${property.referenceType}'`);
566
+ }
567
+ return `${property.name}: { ${fields.join(', ')} }`;
568
+ });
569
+ const supers = superTypes.map(superType => `'${superType}'`).join(', ');
570
+ const propertiesText = propertyEntries.length > 0 ? `{ ${propertyEntries.join(', ')} }` : '{}';
571
+ out.push(` ${name}: { name: '${name}', properties: ${propertiesText}, superTypes: [${supers}] },`);
572
+ }
573
+ out.push(
574
+ ` } as const satisfies { readonly [type: string]: ${metaData} },`,
575
+ '',
576
+ ' getAllTypes(): string[] {',
577
+ ' return Object.keys(this.types);',
578
+ ' },',
579
+ '',
580
+ // An own-property check, not `types[type] ?? …`: a name the object
581
+ // prototype carries, `toString` say, would read the inherited member and
582
+ // make `isSubtype` throw. `hasOwnProperty.call` rather than `Object.hasOwn`
583
+ // because the adopter's protocol package may target a lib without it.
584
+ ` getTypeMetaData(type: string): ${metaData} {`,
585
+ ' return Object.prototype.hasOwnProperty.call(this.types, type)',
586
+ ` ? (this.types as { readonly [type: string]: ${metaData} })[type]`,
587
+ ' : { name: type, properties: {}, superTypes: [] };',
588
+ ' },',
589
+ '',
590
+ ' isSubtype(subtype: string, supertype: string): boolean {',
591
+ ' return subtype === supertype || this.getTypeMetaData(subtype).superTypes.some(type => this.isSubtype(type, supertype));',
592
+ ' },',
593
+ '',
594
+ ' getAllSubTypes(type: string): string[] {',
595
+ ' return this.getAllTypes().filter(candidate => this.isSubtype(candidate, type));',
596
+ ' },',
597
+ '',
598
+ ' isInstance(item: unknown, type: string): boolean {',
599
+ ` return is${elementTypeName}(item) && this.isSubtype(item.$type, type);`,
600
+ ' }',
601
+ '};',
602
+ ''
603
+ );
604
+ return out;
425
605
  }
426
606
 
427
607
  // ---------------------------------------------------------------------------
@@ -462,10 +642,11 @@ function findAstReflectionClass(source: SourceFile): ClassDeclaration {
462
642
  }
463
643
 
464
644
  /**
465
- * Extract `superTypes` for each entry of the reflection class's `types` property
466
- * initializer. We rely on the Langium convention that `X.$type === 'X'`, so a
467
- * supertype expression resolves to the literal type-name string by stripping
468
- * `.$type`.
645
+ * Extract `superTypes` and the grammar properties for each entry of the
646
+ * reflection class's `types` property initializer. We rely on the Langium
647
+ * convention that `X.$type === 'X'` and `X.p === 'p'`, so a type expression
648
+ * resolves to the literal type-name string by stripping `.$type`, and a
649
+ * property's name is its key.
469
650
  */
470
651
  function parseReflectionTypes(reflectionClass: ClassDeclaration): Record<string, ReflectionEntry> {
471
652
  const result: Record<string, ReflectionEntry> = {};
@@ -487,29 +668,35 @@ function parseReflectionTypes(reflectionClass: ClassDeclaration): Record<string,
487
668
  const value = entry.getLastChildByKind(SyntaxKind.ObjectLiteralExpression);
488
669
  const supersProp = value?.getProperty('superTypes');
489
670
  const supersInit = supersProp?.getFirstChildByKind(SyntaxKind.ArrayLiteralExpression);
490
- const superTypes: string[] = [];
491
- if (supersInit) {
492
- for (const element of supersInit.getElements()) {
493
- const text = element.getText();
494
- const dotMatch = /^([A-Za-z_$][\w$]*)\.\$type$/.exec(text);
495
- if (dotMatch) {
496
- superTypes.push(dotMatch[1]);
497
- continue;
498
- }
499
- // Bare identifier or quoted string fall-back.
500
- const stringMatch = /^['"](.+)['"]$/.exec(text);
501
- if (stringMatch) {
502
- superTypes.push(stringMatch[1]);
503
- } else {
504
- superTypes.push(text);
505
- }
506
- }
507
- }
508
- result[name] = { superTypes };
671
+ const superTypes = supersInit?.getElements().map(element => typeNameOf(element.getText())) ?? [];
672
+ result[name] = { superTypes, properties: parseReflectionProperties(value) };
509
673
  }
510
674
  return result;
511
675
  }
512
676
 
677
+ function parseReflectionProperties(entry: ObjectLiteralExpression | undefined): ReflectionProperty[] {
678
+ const propertiesInit = entry?.getProperty('properties')?.getFirstChildByKind(SyntaxKind.ObjectLiteralExpression);
679
+ const properties: ReflectionProperty[] = [];
680
+ for (const property of propertiesInit?.getChildrenOfKind(SyntaxKind.PropertyAssignment) ?? []) {
681
+ const meta = property.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression);
682
+ const field = (key: string): string | undefined =>
683
+ meta?.getProperty(key)?.asKind(SyntaxKind.PropertyAssignment)?.getInitializer()?.getText();
684
+ const referenceType = field('referenceType');
685
+ properties.push({
686
+ name: property.getName(),
687
+ defaultValue: field('defaultValue'),
688
+ optional: field('optional') === 'true',
689
+ referenceType: referenceType === undefined ? undefined : typeNameOf(referenceType)
690
+ });
691
+ }
692
+ return properties;
693
+ }
694
+
695
+ /** `X.$type` or a quoted `'X'` to `X`; any other text passes through. */
696
+ function typeNameOf(text: string): string {
697
+ return /^([A-Za-z_$][\w$]*)\.\$type$/.exec(text)?.[1] ?? /^['"](.+)['"]$/.exec(text)?.[1] ?? text;
698
+ }
699
+
513
700
  /**
514
701
  * Collect terminal regex literals from the AST file.
515
702
  *
@@ -647,12 +834,16 @@ function mapType(text: string, elementTypeName: string): string {
647
834
 
648
835
  /**
649
836
  * Type-guard expression (against an `item: unknown`) for one member of a non-literal type-alias
650
- * union. Covers the full set of Langium data-type primitives: the `typeof`-checkable ones
651
- * (`string`/`number`/`boolean`/`bigint`) use `typeof`; `Date` — the one Langium primitive `typeof`
652
- * reports as `'object'` — uses `instanceof`; any other member is a named type whose generated
653
- * `is<Member>` guard is called.
837
+ * union. A string literal is compared as written, so the escapes Langium wrote stay valid.
838
+ * Covers the full set of Langium data-type primitives. The `typeof`-checkable ones
839
+ * (`string`/`number`/`boolean`/`bigint`) use `typeof`, and `Date`, the one Langium primitive
840
+ * `typeof` reports as `'object'`, uses `instanceof`. Any other member is a named type whose
841
+ * generated `is<Member>` guard is called.
654
842
  */
655
843
  export function typeAliasMemberGuard(member: string): string {
844
+ if (member.startsWith("'") || member.startsWith('"')) {
845
+ return `item === ${member}`;
846
+ }
656
847
  switch (member) {
657
848
  case 'string':
658
849
  case 'number':
@@ -676,10 +867,11 @@ export function typeAliasMemberGuard(member: string): string {
676
867
  * name.
677
868
  *
678
869
  * Each referenced member is inlined and de-duplicated, preserving first-seen
679
- * order. The definition is returned UNCHANGED when it holds no
680
- * namespace-qualified reference (the single-language case) or when any referenced
681
- * member is not a pure string-literal union — passing it through rather than
682
- * emitting a half-resolved union.
870
+ * order; when no member holds a literal, the union is `never`. The definition
871
+ * is returned UNCHANGED when it holds no namespace-qualified reference (the
872
+ * single-language case) or when any referenced member is not a pure
873
+ * string-literal union — passing it through rather than emitting a
874
+ * half-resolved union.
683
875
  */
684
876
  export function resolveNamespacedStringUnion(astSource: SourceFile, definition: string): string {
685
877
  const references = [...definition.matchAll(/\b([A-Za-z_]\w*)\.([A-Za-z_]\w*)\b/g)];
@@ -701,16 +893,55 @@ export function resolveNamespacedStringUnion(astSource: SourceFile, definition:
701
893
  }
702
894
  }
703
895
  }
704
- return literals.map(literal => `'${literal}'`).join(' | ');
896
+ return literals.length > 0 ? literals.map(quote).join(' | ') : 'never';
705
897
  }
706
898
 
707
- /** Extract string literals from a union definition, or undefined if not pure literals. */
899
+ /**
900
+ * The members of a type-alias union as TypeScript parses them. If a data-type rule groups
901
+ * alternatives in parentheses, the alias Langium generates contains the same group, as in
902
+ * `'a' | ('b' | Keyword)`, so each group is flattened into its members. Splitting the text on
903
+ * `|` breaks such a group and any keyword that contains `|`, such as `'||'`, and the
904
+ * resulting guard does not compile.
905
+ */
906
+ function unionMembers(typeNode: Node): string[] {
907
+ if (typeNode.isKind(SyntaxKind.UnionType)) {
908
+ return typeNode.getTypeNodes().flatMap(member => unionMembers(member));
909
+ }
910
+ if (typeNode.isKind(SyntaxKind.ParenthesizedType)) {
911
+ return unionMembers(typeNode.getTypeNode());
912
+ }
913
+ return [typeNode.getText()];
914
+ }
915
+
916
+ /**
917
+ * The values of a union of string literals, or undefined when the definition holds anything
918
+ * else. The TypeScript scanner decodes each literal, so a keyword with a quote in it reads the
919
+ * same whichever quotes and escapes Langium wrote it with. Langium writes the keyword names of
920
+ * a language without keywords as `never`, which adds no value.
921
+ */
708
922
  function extractStringLiterals(definition: string): string[] | undefined {
709
- const stringLiteral = '(?:\'[^\']*\'|"[^"]*")';
710
- if (!new RegExp(`^\\|?\\s*${stringLiteral}(\\s*\\|\\s*${stringLiteral})*$`).test(definition.trim())) {
711
- return undefined;
923
+ const scanner = ts.createScanner(ts.ScriptTarget.Latest, true, ts.LanguageVariant.Standard, definition);
924
+ const literals: string[] = [];
925
+ let members = 0;
926
+ for (let token = scanner.scan(); token !== ts.SyntaxKind.EndOfFileToken; token = scanner.scan()) {
927
+ if (token === ts.SyntaxKind.StringLiteral) {
928
+ literals.push(scanner.getTokenValue());
929
+ members++;
930
+ } else if (token === ts.SyntaxKind.NeverKeyword) {
931
+ members++;
932
+ } else if (token !== ts.SyntaxKind.BarToken) {
933
+ return undefined;
934
+ }
712
935
  }
713
- return [...definition.matchAll(/'([^']*)'|"([^"]*)"/g)].map(m => m[1] ?? m[2]);
936
+ return members > 0 ? literals : undefined;
937
+ }
938
+
939
+ /**
940
+ * `value` as a string literal in the single quotes the generated file uses. A keyword can
941
+ * contain a quote or a backslash, and either one ends or escapes the literal unless escaped.
942
+ */
943
+ function quote(value: string): string {
944
+ return `'${value.replaceAll('\\', '\\\\').replaceAll("'", "\\'")}'`;
714
945
  }
715
946
 
716
947
  function topologicalSort(names: string[], getDeps: (n: string) => string[]): string[] {
@@ -0,0 +1,22 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 EclipseSource and others.
3
+ *
4
+ * This program and the accompanying materials are made available under the
5
+ * terms of the MIT License which is available in the project root.
6
+ *
7
+ * SPDX-License-Identifier: MIT
8
+ ********************************************************************************/
9
+
10
+ import * as path from 'path';
11
+
12
+ /**
13
+ * Module specifier for `targetFile` as seen from the generated module
14
+ * `outFile`'s own directory. Emitted with a `.js` extension and posix
15
+ * separators because the output is ESM TypeScript, where the specifier names
16
+ * the compiled sibling.
17
+ */
18
+ export function relativeImportPath(outFile: string, targetFile: string): string {
19
+ const relative = path.relative(path.dirname(path.resolve(outFile)), path.resolve(targetFile));
20
+ const posix = relative.split(path.sep).join('/').replace(/\.ts$/, '.js');
21
+ return posix.startsWith('.') ? posix : `./${posix}`;
22
+ }