@hydranium/cli 1.0.0-next.4 → 1.0.0-next.400

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