@hydranium/cli 1.0.0-next.18 → 1.0.0-next.181

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 (106) hide show
  1. package/README.md +14 -0
  2. package/bin/hydranium-cli.js +13 -0
  3. package/lib/cli-version.js.map +1 -1
  4. package/lib/cli.js +0 -0
  5. package/lib/commands/analyze-heap-args.js.map +1 -1
  6. package/lib/commands/analyze-heap.d.ts +3 -0
  7. package/lib/commands/analyze-heap.d.ts.map +1 -1
  8. package/lib/commands/analyze-heap.js +2 -1
  9. package/lib/commands/analyze-heap.js.map +1 -1
  10. package/lib/commands/ast-ground-truth-args.js.map +1 -1
  11. package/lib/commands/ast-ground-truth.js +1 -1
  12. package/lib/commands/ast-ground-truth.js.map +1 -1
  13. package/lib/commands/generate-ast-builder.d.ts +49 -0
  14. package/lib/commands/generate-ast-builder.d.ts.map +1 -0
  15. package/lib/commands/generate-ast-builder.js +200 -0
  16. package/lib/commands/generate-ast-builder.js.map +1 -0
  17. package/lib/commands/generate-transfer-model-args.d.ts.map +1 -1
  18. package/lib/commands/generate-transfer-model-args.js +11 -1
  19. package/lib/commands/generate-transfer-model-args.js.map +1 -1
  20. package/lib/commands/generate-transfer-model-config.d.ts.map +1 -1
  21. package/lib/commands/generate-transfer-model-config.js +3 -1
  22. package/lib/commands/generate-transfer-model-config.js.map +1 -1
  23. package/lib/commands/generate-transfer-model.d.ts +9 -0
  24. package/lib/commands/generate-transfer-model.d.ts.map +1 -1
  25. package/lib/commands/generate-transfer-model.js +16 -3
  26. package/lib/commands/generate-transfer-model.js.map +1 -1
  27. package/lib/commands/harness-args.js.map +1 -1
  28. package/lib/commands/headless-harness.d.ts +18 -3
  29. package/lib/commands/headless-harness.d.ts.map +1 -1
  30. package/lib/commands/headless-harness.js +12 -4
  31. package/lib/commands/headless-harness.js.map +1 -1
  32. package/lib/commands/init-args.js.map +1 -1
  33. package/lib/commands/init-prompt.d.ts.map +1 -1
  34. package/lib/commands/init-templates.d.ts +0 -9
  35. package/lib/commands/init-templates.d.ts.map +1 -1
  36. package/lib/commands/init-templates.js +78 -55
  37. package/lib/commands/init-templates.js.map +1 -1
  38. package/lib/commands/init-wizard.js.map +1 -1
  39. package/lib/commands/init-workspace.d.ts +7 -3
  40. package/lib/commands/init-workspace.d.ts.map +1 -1
  41. package/lib/commands/init-workspace.js +11 -6
  42. package/lib/commands/init-workspace.js.map +1 -1
  43. package/lib/commands/init.d.ts +4 -2
  44. package/lib/commands/init.d.ts.map +1 -1
  45. package/lib/commands/init.js +7 -10
  46. package/lib/commands/init.js.map +1 -1
  47. package/lib/commands/lint-grammar-args.js.map +1 -1
  48. package/lib/commands/lint-grammar-report.js.map +1 -1
  49. package/lib/commands/lint-grammar.js +1 -1
  50. package/lib/commands/lint-grammar.js.map +1 -1
  51. package/lib/commands/measure-memory-args.js.map +1 -1
  52. package/lib/commands/measure-memory-driver.js +3 -2
  53. package/lib/commands/measure-memory-driver.js.map +1 -1
  54. package/lib/commands/measure-memory.js +1 -1
  55. package/lib/commands/measure-memory.js.map +1 -1
  56. package/lib/commands/model-docs-args.js.map +1 -1
  57. package/lib/commands/model-docs.js +1 -1
  58. package/lib/commands/model-docs.js.map +1 -1
  59. package/lib/commands/projects-args.js.map +1 -1
  60. package/lib/commands/query-args.js.map +1 -1
  61. package/lib/commands/reflect-args.js.map +1 -1
  62. package/lib/commands/reflect-report.js.map +1 -1
  63. package/lib/commands/reflect.js +1 -1
  64. package/lib/commands/reflect.js.map +1 -1
  65. package/lib/commands/save-args.js.map +1 -1
  66. package/lib/commands/save.d.ts +5 -0
  67. package/lib/commands/save.d.ts.map +1 -1
  68. package/lib/commands/save.js +29 -4
  69. package/lib/commands/save.js.map +1 -1
  70. package/lib/commands/validate-args.js.map +1 -1
  71. package/lib/commands/validate-report.js.map +1 -1
  72. package/lib/commands/validate.js +1 -1
  73. package/lib/commands/validate.js.map +1 -1
  74. package/lib/commands/watch-args.js.map +1 -1
  75. package/lib/commands/watch.d.ts.map +1 -1
  76. package/lib/commands/watch.js +18 -0
  77. package/lib/commands/watch.js.map +1 -1
  78. package/lib/driver-heap.d.ts +52 -0
  79. package/lib/driver-heap.d.ts.map +1 -0
  80. package/lib/driver-heap.js +51 -0
  81. package/lib/driver-heap.js.map +1 -0
  82. package/lib/index.d.ts +1 -0
  83. package/lib/index.d.ts.map +1 -1
  84. package/lib/index.js +1 -0
  85. package/lib/index.js.map +1 -1
  86. package/package.json +35 -34
  87. package/src/commands/analyze-heap.ts +4 -1
  88. package/src/commands/ast-ground-truth.ts +1 -1
  89. package/src/commands/generate-ast-builder.ts +246 -0
  90. package/src/commands/generate-transfer-model-args.ts +11 -1
  91. package/src/commands/generate-transfer-model-config.ts +3 -1
  92. package/src/commands/generate-transfer-model.ts +25 -3
  93. package/src/commands/headless-harness.ts +22 -4
  94. package/src/commands/init-templates.ts +81 -56
  95. package/src/commands/init-workspace.ts +17 -7
  96. package/src/commands/init.ts +12 -13
  97. package/src/commands/lint-grammar.ts +1 -1
  98. package/src/commands/measure-memory-driver.ts +3 -2
  99. package/src/commands/measure-memory.ts +1 -1
  100. package/src/commands/model-docs.ts +1 -1
  101. package/src/commands/reflect.ts +1 -1
  102. package/src/commands/save.ts +45 -4
  103. package/src/commands/validate.ts +1 -1
  104. package/src/commands/watch.ts +24 -1
  105. package/src/driver-heap.ts +65 -0
  106. package/src/index.ts +1 -0
@@ -82,16 +82,6 @@ export interface InitTemplate {
82
82
  content: string;
83
83
  }
84
84
 
85
- /**
86
- * The version every framework package carries before the first release.
87
- *
88
- * A scaffold made by a CLI still at this version pins a range the registry
89
- * cannot serve, so the emitted README keeps its yalc note and `init` keeps its
90
- * install warning while — and only while — it holds: from a published CLI the
91
- * derived pins resolve and either note would be false as printed.
92
- */
93
- export const UNPUBLISHED_FRAMEWORK_VERSION = '0.0.0';
94
-
95
85
  /**
96
86
  * The version `init` pins every `@hydranium/*` dependency at: the scaffolding
97
87
  * CLI's own.
@@ -177,7 +167,7 @@ function columnsFor(composition: InitComposition): number {
177
167
 
178
168
  /**
179
169
  * A named import on one line while it fits the column budget, else one symbol
180
- * per line. Emitted files are supposed to pass a prettier check as-is, and the
170
+ * per line. Emitted files are supposed to pass a format check as-is, and the
181
171
  * generated-module import grows by one symbol per grammar.
182
172
  */
183
173
  function importList(symbols: readonly string[], from: string, columns: number, typeOnly = false): string {
@@ -260,7 +250,7 @@ function dependencyBlock(composition: InitComposition, indent: string): string {
260
250
  // npm convention, and `npm install` rewrites `package.json` at two spaces
261
251
  // regardless — so three would not survive the adopter's first install.
262
252
  //
263
- // The KEY ORDER is `prettier-plugin-packagejson`'s canonical one, `private`
253
+ // The KEY ORDER is the one Oxfmt's `sortPackageJson` canonicalises to, `private`
264
254
  // before `license` included. Any other order is rewritten by the scaffolded
265
255
  // project's own first `format` run — a diff on a file the adopter never touched
266
256
  // — and it makes a byte-compare against a formatted copy of this emission
@@ -291,16 +281,9 @@ const PACKAGE_JSON = `{
291
281
  "version": "0.0.0",
292
282
  __PRIVATE__ "description": "__NAME__ language server, built with the Hydranium framework.",
293
283
  "keywords": [
294
- "hydranium",
295
- "langium",
296
- "language-server",
297
- "__PROJECT_ID__"
298
- ],
284
+ __KEYWORDS__ ],
299
285
  "license": "UNLICENSED",
300
286
  "author": "",
301
- "type": "module",
302
- "main": "lib/index.js",
303
- "types": "lib/index.d.ts",
304
287
  "bin": {
305
288
  __BIN__ },
306
289
  "files": [
@@ -308,11 +291,14 @@ __BIN__ },
308
291
  "src",
309
292
  "syntaxes"
310
293
  ],
294
+ "type": "module",
295
+ "main": "lib/index.js",
296
+ "types": "lib/index.d.ts",
311
297
  "scripts": {
312
298
  "build": "npm run generate && tsc",
313
- "clean": "rimraf lib syntaxes src/language-server/generated src/language-server/generated-transfer tsconfig.tsbuildinfo",
299
+ "clean": "rimraf lib syntaxes src/language-server/generated src/language-server/generated-hydranium tsconfig.tsbuildinfo",
314
300
  "generate": "npm run langium:generate && npm run generate:transfer-model",
315
- "generate:transfer-model": "hydranium-cli generate-transfer-model --ast-file src/language-server/generated/ast.ts --augmentation-file src/language-server/ast.ts --out-file src/language-server/generated-transfer/transfer-model.ts --element-type-name __NAME__Element --terminals-name __NAME__Terminals --regen-command \\"Run: __NPM_RUN__ generate:transfer-model\\"",
301
+ "generate:transfer-model": "hydranium-cli generate-transfer-model --ast-file src/language-server/generated/ast.ts --augmentation-file src/language-server/ast.ts --out-file src/language-server/generated-hydranium/transfer-model.ts --ast-builder-file src/language-server/generated-hydranium/ast-builder.ts --element-type-name __NAME__Element --terminals-name __NAME__Terminals --regen-command \\"Run: __NPM_RUN__ generate:transfer-model\\"",
316
302
  "langium:generate": "langium generate",
317
303
  "langium:watch": "langium generate --watch",
318
304
  __LINT__ "start": "node lib/main.js --stdio",
@@ -328,7 +314,7 @@ __DEPENDENCIES__
328
314
  "@types/node": "^22.0.0",
329
315
  "langium-cli": "4.3.0",
330
316
  "rimraf": "^5.0.0",
331
- "typescript": "^5.8.0",
317
+ "typescript": "^7.0.2",
332
318
  "vitest": "^4.0.0"
333
319
  },
334
320
  "engines": {
@@ -359,7 +345,7 @@ function npmRun(composition: InitComposition): string {
359
345
  * name. Without the second key a scaffolded project has no command line that
360
346
  * reaches its own data head at all.
361
347
  *
362
- * Alphabetical, because `prettier-plugin-packagejson` sorts `bin` and any other
348
+ * Alphabetical, because Oxfmt's `sortPackageJson` sorts `bin` and any other
363
349
  * order is rewritten by the scaffolded project's first `format` run.
364
350
  */
365
351
  function binBlock(composition: InitComposition): string {
@@ -374,13 +360,29 @@ function binBlock(composition: InitComposition): string {
374
360
  function packageJson(composition: InitComposition): string {
375
361
  const { scope } = composition.packaging;
376
362
  const packageName = scope === undefined ? composition.projectId : `${scope}/${composition.projectId}`;
377
- // Emitted only where a root eslint config says the repo lints. The absence is
363
+ const keywords =
364
+ ['hydranium', 'langium', 'language-server', composition.projectId]
365
+ .sort()
366
+ .map(keyword => ` ${JSON.stringify(keyword)}`)
367
+ .join(',\n') + '\n';
368
+ // Emitted only where a root lint config says the repo lints. The absence is
378
369
  // the dangerous direction: a task runner runs a script only where one is
379
370
  // declared, so a package with no `lint` is SKIPPED rather than reported, and
380
371
  // that reads as a clean lint. A wrong invocation is one visible line to edit.
381
- const lint = composition.packaging.workspace?.eslintConfig === undefined ? '' : ' "lint": "eslint src test --max-warnings 0",\n';
372
+ const workspace = composition.packaging.workspace;
373
+ const oxlintConfig =
374
+ workspace?.oxlintConfig === undefined
375
+ ? undefined
376
+ : `${'../'.repeat(workspace.targetPath.split('/').length)}${workspace.oxlintConfig}`;
377
+ const lint =
378
+ workspace?.oxlintConfig !== undefined
379
+ ? ` "lint": "oxlint --report-unused-disable-directives --format stylish --config ${oxlintConfig} src test --max-warnings 0",\n`
380
+ : workspace?.eslintConfig !== undefined
381
+ ? ' "lint": "eslint src test --max-warnings 0",\n'
382
+ : '';
382
383
  return project(PACKAGE_JSON, composition)
383
384
  .replace('__PACKAGE_NAME__', packageName)
385
+ .replace('__KEYWORDS__', keywords)
384
386
  .replace('__BIN__', binBlock(composition))
385
387
  .replace('__LINT__', lint)
386
388
  .replace('__PRIVATE__', composition.packaging.private ? ' "private": true,\n' : '')
@@ -439,6 +441,11 @@ const TSCONFIG_COMPILER_OPTIONS: ReadonlyArray<readonly [string, JsonValue]> = [
439
441
  ['outDir', 'lib'],
440
442
  ['strict', true],
441
443
  ['esModuleInterop', true],
444
+ // A server that renders its own user-facing messages loads a catalogue, and
445
+ // a catalogue is JSON. Without this the import does not resolve at all, and
446
+ // without the `include` glob below a composite project rejects it with
447
+ // TS6307 naming neither cause.
448
+ ['resolveJsonModule', true],
442
449
  ['skipLibCheck', true],
443
450
  ['declaration', true],
444
451
  ['experimentalDecorators', true],
@@ -480,7 +487,7 @@ function tsconfigJson(composition: InitComposition): string {
480
487
  ];
481
488
  const extendsLine = workspace?.baseTsconfig === undefined ? '' : ` "extends": "${workspace.baseTsconfig}",\n`;
482
489
  const body = options.map(([key, value]) => ` "${key}": ${JSON.stringify(value)}`).join(',\n');
483
- return `{\n${extendsLine} "compilerOptions": {\n${body}\n },\n "include": ["src"]\n}\n`;
490
+ return `{\n${extendsLine} "compilerOptions": {\n${body}\n },\n "include": ["src", "src/**/*.json"]\n}\n`;
484
491
  }
485
492
 
486
493
  // `isolatedModules` is what makes this check agree with the transform that
@@ -498,7 +505,7 @@ const TSCONFIG_TEST = `{
498
505
  "isolatedModules": true,
499
506
  "types": ["node"]
500
507
  },
501
- "include": ["src", "test"]
508
+ "include": ["src", "test", "src/**/*.json"]
502
509
  }
503
510
  `;
504
511
 
@@ -708,13 +715,35 @@ export type __NAME__Services = LangiumServices &
708
715
  LspServerAddedServices & {
709
716
  shared: __NAME__SharedServices;
710
717
  };
718
+
719
+ /** What a host or a test may vary about this composition. */
720
+ export interface __NAME__Options {
721
+ /**
722
+ * Shared modules layered in after the framework's own bindings.
723
+ *
724
+ * The framework constructs most shared services with no options —
725
+ * \`DocumentBuilder: services => new HydraniumDocumentBuilder(services)\` — so
726
+ * rebinding the slot is the only way to boot one configured differently, and
727
+ * a factory that hard-codes its composition leaves a test nowhere to do it.
728
+ * That is what this is for: pass a module binding \`workspace.DocumentBuilder\`
729
+ * to exercise a builder option, or to substitute a subclass.
730
+ *
731
+ * Composed LAST, after \`__NAME__SharedModule\`, so it wins over every other
732
+ * tier including this file's own bindings — which is what makes it usable for
733
+ * a slot the adopter overrides. Production code should not reach for it.
734
+ */
735
+ readonly extraSharedModules?: ReadonlyArray<Module<__NAME__SharedServices, DeepPartial<__NAME__SharedServices>>>;
736
+ }
711
737
  ${configurationRoot}
712
738
  ${sharedModule}
713
739
 
714
740
  ${languageModules}
715
741
 
716
742
  /** Compose the Langium DI tree for __NAME__ — returns the shared + language services. */
717
- export function create__NAME__Services(context: Partial<ServerModuleContext> = EmptyFileSystem): {
743
+ export function create__NAME__Services(
744
+ context: Partial<ServerModuleContext> = EmptyFileSystem,
745
+ options: __NAME__Options = {}
746
+ ): {
718
747
  shared: __NAME__SharedServices;
719
748
  ${returnType}
720
749
  } {
@@ -724,7 +753,8 @@ ${returnType}
724
753
  sharedModules: {
725
754
  generated: __NAME__GeneratedSharedModule,
726
755
  adopter: __NAME__SharedModule,
727
- extra: [createLspServerSharedModule(fullContext)]
756
+ extra: [createLspServerSharedModule(fullContext)],
757
+ overrides: options.extraSharedModules
728
758
  },
729
759
  languageModules: {
730
760
  generated: ${primary.grammar}GeneratedModule,
@@ -791,9 +821,9 @@ const SERIALIZER = `// Concrete-syntax emitter for __GRAMMAR__ — the parser's
791
821
  //
792
822
  // Without it every structured write fails: the framework's default binding at
793
823
  // \`services.serializer.Serializer\` THROWS, because a concrete syntax is
794
- // language knowledge no framework can derive. \`ModelService.update\` / \`save\`,
795
- // the data head's \`saveModelDocument\` and a GLSP \`SaveModelAction\` all reach
796
- // it.
824
+ // language knowledge no framework can derive. A client session's \`update\` /
825
+ // \`save\`, the data head's \`saveModelDocument\` and a GLSP \`SaveModelAction\`
826
+ // all reach it.
797
827
  //
798
828
  // \`AbstractSerializer\`'s generic property walk lays out FORMAT-structured
799
829
  // output — its YAML and JSON subclasses are what it exists for — and cannot
@@ -980,7 +1010,7 @@ import { startGlspServer } from '@hydranium/glsp-server/node';
980
1010
  // \`TransferElement\` structurally, so naming the AST type here compiles fine and
981
1011
  // silently tells every typed client that a reference is a resolvable object
982
1012
  // rather than a name.
983
- ${importList(roots, './language-server/generated-transfer/transfer-model.js', columns, true)}
1013
+ ${importList(roots, './language-server/generated-hydranium/transfer-model.js', columns, true)}
984
1014
  `
985
1015
  : '';
986
1016
 
@@ -1063,11 +1093,17 @@ void glspServer;
1063
1093
  // than a library entry — import \`./index.js\` instead to compose the language.
1064
1094
 
1065
1095
  ${reflectImport}${glspImports}${importList(coreNodeSymbols, '@hydranium/core/node', columns)}
1066
- ${data ? "import { DataServer } from '@hydranium/data-server';\n" : ''}import { startLanguageServer } from '@hydranium/langium/lsp';
1096
+ ${data ? "import { DataServer } from '@hydranium/data-server';\n" : ''}// The framework's entry point, NOT Langium's \`@hydranium/langium/lsp\` one. It is
1097
+ // signature-compatible and delegates straight through; what it adds first is
1098
+ // \`assertLspHeadComposed\`, which fails the start if the LSP head's SHARED module
1099
+ // was never composed. Reaching for Langium's is the natural mistake and it is
1100
+ // silent: the server boots, links and completes, while echo suppression, the
1101
+ // didChangeContent debounce and the last-client-close rebuild are all inert.
1102
+ import { startLanguageServer, withHydraniumLspFeatures } from '@hydranium/core/lsp';
1067
1103
  import { ProposedFeatures, createConnection } from 'vscode-languageserver/node';
1068
1104
  ${diagramImports}${transferImports}import { create__NAME__Services } from './language-server/__PROJECT_ID__-module.js';
1069
1105
  ${portCommands === '' ? '' : '\n' + portCommands}
1070
- const connection = createConnection(ProposedFeatures.all);
1106
+ const connection = createConnection(withHydraniumLspFeatures(ProposedFeatures.all));
1071
1107
  const { shared } = create__NAME__Services({ connection, ...NodeFileSystem });
1072
1108
  startLanguageServer(shared);
1073
1109
  ${dataBlock}${glspBlock}`;
@@ -1117,7 +1153,7 @@ function dataServerMainFile(composition: InitComposition): string {
1117
1153
  ${importList(['NodeFileSystem', 'startStdioServer'], '@hydranium/core/node', columns)}
1118
1154
  import { DataServer } from '@hydranium/data-server';
1119
1155
  // The TRANSFER root${plural}, not the AST one${plural} — same reasoning as \`main.ts\`.
1120
- ${importList(roots, './language-server/generated-transfer/transfer-model.js', columns, true)}
1156
+ ${importList(roots, './language-server/generated-hydranium/transfer-model.js', columns, true)}
1121
1157
  import { create__NAME__Services } from './language-server/__PROJECT_ID__-module.js';
1122
1158
 
1123
1159
  const { shared } = create__NAME__Services({ ...NodeFileSystem });
@@ -1236,8 +1272,8 @@ function serializationTest(composition: InitComposition): string {
1236
1272
  const transferTypes = composition.grammars.map(grammar => `${grammar.entryRule} as Transfer${grammar.entryRule}`);
1237
1273
  const text = `// Round-trips each grammar through its serializer: parse the source, serialize
1238
1274
  // the model, compare the text. The serializer is the parser's inverse, so this
1239
- // is the assertion that keeps \`ModelService.update\` / \`save\` writing files the
1240
- // language server can read back.
1275
+ // is the assertion that keeps a client session's \`update\` / \`save\` writing
1276
+ // files the language server can read back.
1241
1277
  //
1242
1278
  // The transfer case is the one that catches the mistake worth catching.
1243
1279
  // \`ModelService.modelToText\` short-circuits only a RAW STRING, so a typed model
@@ -1247,7 +1283,7 @@ function serializationTest(composition: InitComposition): string {
1247
1283
  import { parseHelper } from '@hydranium/core/testing';
1248
1284
  import { describe, expect, it } from 'vitest';
1249
1285
  ${importList(astTypes, '../src/language-server/ast.js', columns, true)}
1250
- ${importList(transferTypes, '../src/language-server/generated-transfer/transfer-model.js', columns, true)}
1286
+ ${importList(transferTypes, '../src/language-server/generated-hydranium/transfer-model.js', columns, true)}
1251
1287
  import { createServices } from '../src/services.js';
1252
1288
 
1253
1289
  ${composition.grammars.map(grammar => render(SERIALIZATION_SUITE, composition, grammar)).join('\n\n')}
@@ -1416,20 +1452,6 @@ function readme(composition: InitComposition): string {
1416
1452
  '',
1417
1453
  '## Getting started',
1418
1454
  '',
1419
- // Conditional on the pin this scaffold actually carries — see
1420
- // `UNPUBLISHED_FRAMEWORK_VERSION`.
1421
- ...(composition.frameworkVersion === UNPUBLISHED_FRAMEWORK_VERSION
1422
- ? [
1423
- '> **Pre-publish note.** `@hydranium/*` is not on npm yet, so the `0.0.0`',
1424
- '> pins below are placeholders and `npm install` will fail with a 404 until',
1425
- '> the framework is released. Until then, supply the packages from a local',
1426
- '> framework checkout with [yalc](https://github.com/wclr/yalc) — a plain',
1427
- '> `file:` path or `npm link` is not enough, because the framework packages',
1428
- '> depend on each other by version and npm would try to fetch those from the',
1429
- '> registry too.',
1430
- ''
1431
- ]
1432
- : []),
1433
1455
  '```bash',
1434
1456
  'npm install',
1435
1457
  'npm run langium:generate # generate the AST from the grammar',
@@ -1633,8 +1655,11 @@ export class __GRAMMAR__GlspState extends FullTextHydraniumGlspState<__ENTRY_RUL
1633
1655
 
1634
1656
  const storage = `// Source-model storage for the __GRAMMAR__ diagram, inheriting both framework
1635
1657
  // defaults: \`loadSourceModel\` (open + settle + \`setSourceRoot\`) and
1636
- // \`saveSourceModel\` (through \`ModelService.save\` → the per-URI \`Serializer\` →
1637
- // the multi-client text store → \`WritableFileSystemProvider\`).
1658
+ // \`saveSourceModel\` (flush the store's text for the primary and every tracked
1659
+ // secondary through \`AstDocumentManager.save\` → \`WritableFileSystemProvider\`).
1660
+ //
1661
+ // No serializer runs on the save path — a save persists what the diagram's
1662
+ // operations already wrote to the store.
1638
1663
  //
1639
1664
  // The subclass exists so a bespoke load or save has a stable place to land.
1640
1665
 
@@ -186,10 +186,12 @@ export interface WorkspaceDetection {
186
186
  readonly baseTsconfig?: string;
187
187
  /** That tsconfig's `compilerOptions`, so the emitted one can drop what it inherits. */
188
188
  readonly baseCompilerOptions?: Readonly<Record<string, JsonValue>>;
189
- /** The root prettier config's `printWidth`, so emitted sources wrap where the repo wraps. */
189
+ /** The root formatter config's `printWidth`, so emitted sources wrap where the repo wraps. */
190
190
  readonly printWidth?: number;
191
191
  /** Filename of the root eslint config, when there is one. Absent means the repo does not lint. */
192
192
  readonly eslintConfig?: string;
193
+ /** Filename of the root Oxlint config, when present. */
194
+ readonly oxlintConfig?: string;
193
195
  }
194
196
 
195
197
  /**
@@ -339,7 +341,7 @@ export function findBaseTsconfig(
339
341
  }
340
342
 
341
343
  /**
342
- * The repo's prettier `printWidth`, so the emitted sources wrap where the repo
344
+ * The repo's formatter `printWidth`, so the emitted sources wrap where the repo
343
345
  * wraps instead of at the scaffold's own default.
344
346
  *
345
347
  * **Why this is worth detecting at all.** There is no width that is stable for
@@ -360,19 +362,21 @@ export function findBaseTsconfig(
360
362
  * adopter's formatter rewraps on first commit. So a heuristic that is usually
361
363
  * right strictly improves on no detection, and can break nothing.
362
364
  */
363
- export function findPrettierPrintWidth(rootDir: string, probe: WorkspaceProbe): number | undefined {
365
+ export function findFormatterPrintWidth(rootDir: string, probe: WorkspaceProbe): number | undefined {
364
366
  const manifest = probe.readJson(path.join(rootDir, 'package.json'));
365
367
  if (isJsonObject(manifest) && isJsonObject(manifest.prettier) && typeof manifest.prettier.printWidth === 'number') {
366
368
  return manifest.prettier.printWidth;
367
369
  }
368
370
  const present = probe.listFiles(rootDir);
369
- for (const file of present.filter(name => name === '.prettierrc' || name === '.prettierrc.json')) {
371
+ for (const file of present.filter(name => name === '.oxfmtrc.json' || name === '.prettierrc' || name === '.prettierrc.json')) {
370
372
  const parsed = probe.readJson(path.join(rootDir, file));
371
373
  if (isJsonObject(parsed) && typeof parsed.printWidth === 'number') {
372
374
  return parsed.printWidth;
373
375
  }
374
376
  }
375
- for (const file of present.filter(name => /^(\.prettierrc\.[cm]?[jt]s|prettier\.config\.[cm]?[jt]s)$/.test(name))) {
377
+ for (const file of present.filter(name =>
378
+ /^(\.prettierrc\.[cm]?[jt]s|prettier\.config\.[cm]?[jt]s|oxfmt\.config\.[cm]?[jt]s)$/.test(name)
379
+ )) {
376
380
  const width = /\bprintWidth\s*:\s*(\d+)/.exec(probe.readText(path.join(rootDir, file)) ?? '');
377
381
  if (width) {
378
382
  return Number(width[1]);
@@ -398,6 +402,11 @@ export function findEslintConfig(rootDir: string, probe: WorkspaceProbe): string
398
402
  return probe.listFiles(rootDir).find(name => /^eslint\.config\.[cm]?[jt]s$/.test(name) || /^\.eslintrc(\..+)?$/.test(name));
399
403
  }
400
404
 
405
+ /** Detect Oxlint without evaluating executable configuration files. */
406
+ export function findOxlintConfig(rootDir: string, probe: WorkspaceProbe): string | undefined {
407
+ return probe.listFiles(rootDir).find(name => /^(oxlint\.config\.[cm]?[jt]s|\.oxlintrc\.jsonc?)$/.test(name));
408
+ }
409
+
401
410
  /** POSIX-style relative path, which is what both `extends` and `--prefix` want on every platform. */
402
411
  function posixRelative(from: string, to: string): string {
403
412
  return path.relative(from, to).split(path.sep).join('/');
@@ -436,7 +445,8 @@ export function detectWorkspace(targetDir: string, probe: WorkspaceProbe): Works
436
445
  scope: inferPackageScope(rootDir, workspaces, probe),
437
446
  baseTsconfig: base && posixRelative(absoluteTarget, path.join(rootDir, base.file)),
438
447
  baseCompilerOptions: base?.compilerOptions,
439
- printWidth: findPrettierPrintWidth(rootDir, probe),
440
- eslintConfig: findEslintConfig(rootDir, probe)
448
+ printWidth: findFormatterPrintWidth(rootDir, probe),
449
+ eslintConfig: findEslintConfig(rootDir, probe),
450
+ oxlintConfig: findOxlintConfig(rootDir, probe)
441
451
  };
442
452
  }
@@ -9,7 +9,7 @@
9
9
 
10
10
  import * as fs from 'node:fs';
11
11
  import * as path from 'node:path';
12
- import { buildInitTemplates, readFrameworkVersion, UNPUBLISHED_FRAMEWORK_VERSION } from './init-templates.js';
12
+ import { buildInitTemplates, readFrameworkVersion } from './init-templates.js';
13
13
  import { createNodeWorkspaceProbe, detectWorkspace, type JsonValue, type WorkspaceProbe } from './init-workspace.js';
14
14
 
15
15
  /**
@@ -128,10 +128,12 @@ export interface InitWorkspacePlacement {
128
128
  readonly baseTsconfig?: string;
129
129
  /** That tsconfig's `compilerOptions`, so the emitted one can omit what it would inherit unchanged. */
130
130
  readonly baseCompilerOptions?: Readonly<Record<string, JsonValue>>;
131
- /** The workspace's prettier `printWidth`, so emitted sources wrap where the repo wraps. */
131
+ /** The workspace's formatter `printWidth`, so emitted sources wrap where the repo wraps. */
132
132
  readonly printWidth?: number;
133
133
  /** Filename of the workspace's eslint config; its presence is what makes the scaffold emit a `lint` script. */
134
134
  readonly eslintConfig?: string;
135
+ /** Filename of the workspace Oxlint config, when present. */
136
+ readonly oxlintConfig?: string;
135
137
  }
136
138
 
137
139
  /**
@@ -466,7 +468,8 @@ export function resolveInitPackaging(targetDir: string, options: InitPackagingOp
466
468
  baseTsconfig: detection.baseTsconfig,
467
469
  baseCompilerOptions: detection.baseCompilerOptions,
468
470
  printWidth: detection.printWidth,
469
- eslintConfig: detection.eslintConfig
471
+ eslintConfig: detection.eslintConfig,
472
+ oxlintConfig: detection.oxlintConfig
470
473
  }
471
474
  };
472
475
  }
@@ -515,10 +518,12 @@ export function runInit(options: InitCommandOptions): void {
515
518
  }
516
519
  write(" .gitignore holds `syntaxes/` only — the workspace root's covers the rest.\n");
517
520
  if (detection.printWidth !== undefined) {
518
- write(` Sources wrapped at ${detection.printWidth} columns, from ${detection.rootDir}'s prettier config.\n`);
521
+ write(` Sources wrapped at ${detection.printWidth} columns, from ${detection.rootDir}'s formatter config.\n`);
519
522
  }
520
- if (detection.eslintConfig !== undefined) {
521
- write(` A lint script was added — ${detection.eslintConfig} says this repo lints. Adjust it if your invocation differs.\n`);
523
+ if (detection.oxlintConfig !== undefined || detection.eslintConfig !== undefined) {
524
+ write(
525
+ ` A lint script was added — ${detection.oxlintConfig ?? detection.eslintConfig} says this repo lints. Adjust it if your invocation differs.\n`
526
+ );
522
527
  }
523
528
  // Printed rather than applied: `init` writes inside the target directory
524
529
  // and nowhere else, so a scaffold can never surprise-edit a manifest that
@@ -536,13 +541,7 @@ export function runInit(options: InitCommandOptions): void {
536
541
  write('\n');
537
542
  write('Next steps:\n');
538
543
  write(` cd ${options.targetDir}\n`);
539
- // The warning is conditional on the pins this scaffold actually carries — see
540
- // `UNPUBLISHED_FRAMEWORK_VERSION`.
541
- write(
542
- composition.frameworkVersion === UNPUBLISHED_FRAMEWORK_VERSION
543
- ? ' npm install # 404s until @hydranium/* is published — see README\n'
544
- : ' npm install\n'
545
- );
544
+ write(' npm install\n');
546
545
  write(' npm run build # langium generate + tsc\n');
547
546
  write(' npm test # the scaffolded DI-composition test\n');
548
547
  // `npx`, not a bare invocation: `@hydranium/cli` is a devDependency of the
@@ -53,5 +53,5 @@ export function buildLintGrammarDriverArgs(options: LintGrammarCommandOptions):
53
53
  * `createServices` module, keeping the binary language-agnostic.
54
54
  */
55
55
  export function runLintGrammar(options: LintGrammarCommandOptions): Promise<void> {
56
- return runDriverChild(['--max-old-space-size=8192', DRIVER, ...buildLintGrammarDriverArgs(options)], options);
56
+ return runDriverChild([DRIVER, ...buildLintGrammarDriverArgs(options)], options);
57
57
  }
@@ -9,8 +9,9 @@
9
9
 
10
10
  /*
11
11
  * Spawned child of the `measure-memory` subcommand. Launched by the parent with
12
- * `node --expose-gc --max-old-space-size=8192 <this> --services <module> <workspace> ...`
13
- * so the underlying harness gets post-GC readings. It dynamic-imports the head's
12
+ * `node [heap ceiling] --expose-gc <this> --services <module> <workspace> ...`
13
+ * so the underlying harness gets post-GC readings. The ceiling is whatever
14
+ * `driverHeapArgs` decides, which is nothing at all inside a container. It dynamic-imports the head's
14
15
  * services module and runs the framework's `measureModelMemory`, streaming the
15
16
  * baseline / after-build / churn lines to stdout. Never imported — only spawned.
16
17
  */
@@ -139,5 +139,5 @@ export function buildDriverArgs(options: MeasureMemoryCommandOptions): string[]
139
139
  * module and reports the baseline / after-build / churn lines on stdout.
140
140
  */
141
141
  export function runMeasureMemory(options: MeasureMemoryCommandOptions): Promise<void> {
142
- return runDriverChild(['--expose-gc', '--max-old-space-size=8192', DRIVER, ...buildDriverArgs(options)], options);
142
+ return runDriverChild(['--expose-gc', DRIVER, ...buildDriverArgs(options)], options);
143
143
  }
@@ -44,5 +44,5 @@ export function buildModelDocsDriverArgs(options: ModelDocsCommandOptions): stri
44
44
  * module, keeping the binary language-agnostic.
45
45
  */
46
46
  export function runModelDocs(options: ModelDocsCommandOptions): Promise<void> {
47
- return runDriverChild(['--max-old-space-size=8192', DRIVER, ...buildModelDocsDriverArgs(options)], options);
47
+ return runDriverChild([DRIVER, ...buildModelDocsDriverArgs(options)], options);
48
48
  }
@@ -47,5 +47,5 @@ export function buildReflectDriverArgs(options: ReflectCommandOptions): string[]
47
47
  * the binary language-agnostic.
48
48
  */
49
49
  export function runReflect(options: ReflectCommandOptions): Promise<void> {
50
- return runDriverChild(['--max-old-space-size=8192', DRIVER, ...buildReflectDriverArgs(options)], options);
50
+ return runDriverChild([DRIVER, ...buildReflectDriverArgs(options)], options);
51
51
  }
@@ -23,6 +23,11 @@ import { withDataServer } from '../spawn-data-server.js';
23
23
  *
24
24
  * `clientId` defaults to `'hydranium-cli'` — adopters wanting a richer
25
25
  * identity (per-user, per-script) override.
26
+ *
27
+ * The command saves as a client session under `clientId`: it registers the
28
+ * session, creates the document from the content when there is no file,
29
+ * opens it otherwise, saves, and ends the session, which leaves nothing open
30
+ * behind it.
26
31
  */
27
32
  export interface SaveCommandOptions {
28
33
  readonly serverCommand: string;
@@ -46,8 +51,7 @@ export async function runSave(options: SaveCommandOptions): Promise<void> {
46
51
  const model = await resolveContent(options.content, options.__readFileForTest);
47
52
 
48
53
  if (options.__proxyForTest) {
49
- const doc = await options.__proxyForTest.saveModelDocument({ uri: options.uri, clientId, model });
50
- write(`${JSON.stringify(doc)}\n`);
54
+ write(`${JSON.stringify(await saveOpened(options.__proxyForTest, options.uri, clientId, model))}\n`);
51
55
  return;
52
56
  }
53
57
 
@@ -59,12 +63,49 @@ export async function runSave(options: SaveCommandOptions): Promise<void> {
59
63
  env: options.logLevel ? logLevelEnv(options.logLevel) : undefined
60
64
  },
61
65
  async server => {
62
- const doc = await server.saveModelDocument({ uri: options.uri, clientId, model });
63
- write(`${JSON.stringify(doc)}\n`);
66
+ write(`${JSON.stringify(await saveOpened(server, options.uri, clientId, model))}\n`);
64
67
  }
65
68
  );
66
69
  }
67
70
 
71
+ /**
72
+ * Save `model` to `uri` as the session `clientId`, and end the session whether
73
+ * the save succeeded or not. An end that fails after a failed save is
74
+ * swallowed: the save's error is the one the user needs.
75
+ */
76
+ async function saveOpened(
77
+ server: Pick<
78
+ DataServerProtocol<TransferElement>,
79
+ 'createSession' | 'openModelDocument' | 'createModelDocument' | 'saveModelDocument' | 'closeSession'
80
+ >,
81
+ uri: string,
82
+ clientId: string,
83
+ model: string
84
+ ): Promise<unknown> {
85
+ await server.createSession({ clientId, label: 'hydranium-cli' });
86
+ let saved: unknown;
87
+ try {
88
+ // The server creates a document only when it finds no file and no
89
+ // client has it open; anything else is opened. An open that fails
90
+ // reports why the create failed too, since that may be the real cause.
91
+ await server.createModelDocument({ uri, clientId, text: model }).catch((created: unknown) =>
92
+ server.openModelDocument({ uri, clientId }).catch((opened: unknown) => {
93
+ throw new Error(`Cannot save ${uri}: ${describe(opened)} (creating it failed first: ${describe(created)})`, { cause: created });
94
+ })
95
+ );
96
+ saved = await server.saveModelDocument({ uri, clientId, model, basedOn: 'anything' });
97
+ } catch (error: unknown) {
98
+ await server.closeSession({ clientId }).catch(() => undefined);
99
+ throw error;
100
+ }
101
+ await server.closeSession({ clientId });
102
+ return saved;
103
+ }
104
+
105
+ function describe(error: unknown): string {
106
+ return error instanceof Error ? error.message : String(error);
107
+ }
108
+
68
109
  /**
69
110
  * Resolve the `--content` argument to its textual form. The `@<path>`
70
111
  * prefix reads from a file (parses as UTF-8); anything else is taken
@@ -54,5 +54,5 @@ export function buildValidateDriverArgs(options: ValidateCommandOptions): string
54
54
  * The child's exit code propagates to this process so shells and CI see the gate.
55
55
  */
56
56
  export function runValidate(options: ValidateCommandOptions): Promise<void> {
57
- return runDriverChild(['--max-old-space-size=8192', DRIVER, ...buildValidateDriverArgs(options)], options);
57
+ return runDriverChild([DRIVER, ...buildValidateDriverArgs(options)], options);
58
58
  }
@@ -8,7 +8,12 @@
8
8
  ********************************************************************************/
9
9
 
10
10
  import type { LogThreshold, TransferElement } from '@hydranium/protocol';
11
- import type { DataClientProtocol, DataServerProtocol, TransferDocumentUpdatedEvent } from '@hydranium/protocol/data';
11
+ import type {
12
+ DataClientProtocol,
13
+ DataServerProtocol,
14
+ TransferDocumentDeletedEvent,
15
+ TransferDocumentUpdatedEvent
16
+ } from '@hydranium/protocol/data';
12
17
  import { logLevelEnv } from '../log-level.js';
13
18
  import { spawnDataServer } from '../spawn-data-server.js';
14
19
 
@@ -132,6 +137,24 @@ export async function runWatch(options: WatchCommandOptions): Promise<void> {
132
137
  onDocumentSaved(): void {
133
138
  // Persistence is out of band for the per-URI update view.
134
139
  },
140
+ onDocumentDirtyChanged(): void {
141
+ // Out of band for the per-URI update view, as persistence is.
142
+ },
143
+ onDocumentDeleted(event: TransferDocumentDeletedEvent): void {
144
+ // In band, unlike the two neighbours: this is the end of the stream
145
+ // the view exists to show, and a consumer that never hears it waits
146
+ // forever for an update that cannot come. The line carries no
147
+ // `document`, so a reader must switch on shape rather than assume one.
148
+ if (event.uri !== options.uri) {
149
+ return;
150
+ }
151
+ write(`${JSON.stringify(event)}\n`);
152
+ },
153
+ onDocumentsBuilt(): void {
154
+ // Out of band for a per-URI view by construction: this notification
155
+ // carries only documents nobody watches, and this command watches the
156
+ // one URI it was given.
157
+ },
135
158
  onProjectsChanged(): void {
136
159
  // Project lifecycle is out of band for the per-URI update view.
137
160
  }