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

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 (32) hide show
  1. package/lib/commands/generate-ast-builder.d.ts +49 -0
  2. package/lib/commands/generate-ast-builder.d.ts.map +1 -0
  3. package/lib/commands/generate-ast-builder.js +200 -0
  4. package/lib/commands/generate-ast-builder.js.map +1 -0
  5. package/lib/commands/generate-transfer-model-args.d.ts.map +1 -1
  6. package/lib/commands/generate-transfer-model-args.js +11 -1
  7. package/lib/commands/generate-transfer-model-args.js.map +1 -1
  8. package/lib/commands/generate-transfer-model-config.d.ts.map +1 -1
  9. package/lib/commands/generate-transfer-model-config.js +3 -1
  10. package/lib/commands/generate-transfer-model-config.js.map +1 -1
  11. package/lib/commands/generate-transfer-model.d.ts +9 -0
  12. package/lib/commands/generate-transfer-model.d.ts.map +1 -1
  13. package/lib/commands/generate-transfer-model.js +16 -3
  14. package/lib/commands/generate-transfer-model.js.map +1 -1
  15. package/lib/commands/init-templates.d.ts.map +1 -1
  16. package/lib/commands/init-templates.js +44 -10
  17. package/lib/commands/init-templates.js.map +1 -1
  18. package/lib/commands/watch.d.ts.map +1 -1
  19. package/lib/commands/watch.js +15 -0
  20. package/lib/commands/watch.js.map +1 -1
  21. package/lib/index.d.ts +1 -0
  22. package/lib/index.d.ts.map +1 -1
  23. package/lib/index.js +1 -0
  24. package/lib/index.js.map +1 -1
  25. package/package.json +11 -10
  26. package/src/commands/generate-ast-builder.ts +246 -0
  27. package/src/commands/generate-transfer-model-args.ts +11 -1
  28. package/src/commands/generate-transfer-model-config.ts +3 -1
  29. package/src/commands/generate-transfer-model.ts +25 -3
  30. package/src/commands/init-templates.ts +44 -10
  31. package/src/commands/watch.ts +21 -1
  32. package/src/index.ts +1 -0
@@ -29,6 +29,7 @@ export const GENERATE_TRANSFER_MODEL_FLAGS: readonly string[] = [
29
29
  '--ast-file',
30
30
  '--augmentation-file',
31
31
  '--out-file',
32
+ '--ast-builder-file',
32
33
  '--element-type-name',
33
34
  '--terminals-name',
34
35
  '--terminals-source-name',
@@ -48,7 +49,8 @@ export const GENERATE_TRANSFER_MODEL_VALUE_FLAGS: readonly string[] = GENERATE_T
48
49
  export const GENERATE_TRANSFER_MODEL_HELP: readonly string[] = [
49
50
  'Usage: hydranium-cli generate-transfer-model [options]',
50
51
  '',
51
- 'Generate a serializable transfer-model TypeScript file from a Langium AST. The',
52
+ 'Generate a serializable transfer-model TypeScript file from a Langium AST, and',
53
+ 'optionally an AST-node builder module bound to the same reflection. The',
52
54
  'required inputs (--ast-file / --augmentation-file / --out-file) may instead come',
53
55
  'from a --config JSON file, and --ast-file can be auto-discovered from a Langium',
54
56
  "config's `out` directory. Precedence, highest first: explicit flags, then the",
@@ -67,6 +69,11 @@ export const GENERATE_TRANSFER_MODEL_HELP: readonly string[] = [
67
69
  ' directory: it treats that directory as exclusively',
68
70
  ' its own, so every `langium generate` reports this',
69
71
  ' file as unexpected and offers to delete it.',
72
+ ' --ast-builder-file <path> Also emit an AST-node builder module here: one',
73
+ ' makeAstNodeBuilder binding per grammar, plus one',
74
+ ' spanning all of them. Omit if nothing constructs',
75
+ ' AST nodes. Same restriction as --out-file: keep it',
76
+ " out of langium-cli's own `out` directory.",
70
77
  ' --element-type-name <name> Base element type name in output. Default: TransferElement.',
71
78
  ' --terminals-name <name> Terminals const name in output. Default: ModelTerminals.',
72
79
  ' --terminals-source-name <n> Source-side terminals variable name. Default: <LanguageId>Terminals.',
@@ -120,6 +127,9 @@ export function parseGenerateOptions(args: string[], onError: UsageError = exitW
120
127
  case '--out-file':
121
128
  flags.outFile = next();
122
129
  break;
130
+ case '--ast-builder-file':
131
+ flags.astBuilderFile = next();
132
+ break;
123
133
  case '--element-type-name':
124
134
  flags.elementTypeName = next();
125
135
  break;
@@ -12,7 +12,7 @@ import * as path from 'node:path';
12
12
  import type { GenerateTransferModelOptions } from './generate-transfer-model.js';
13
13
 
14
14
  /** Path-valued option keys — resolved relative to a config/langium-config file's directory. */
15
- const PATH_KEYS = ['astFile', 'augmentationFile', 'outFile'] as const;
15
+ const PATH_KEYS = ['astFile', 'augmentationFile', 'outFile', 'astBuilderFile'] as const;
16
16
 
17
17
  /** Resolve `value` against `baseDir` when it is relative; absolute paths pass through. */
18
18
  function resolveAgainst(baseDir: string, value: string): string {
@@ -43,6 +43,7 @@ export function loadTransferModelConfig(configPath: string): Partial<GenerateTra
43
43
  'astFile',
44
44
  'augmentationFile',
45
45
  'outFile',
46
+ 'astBuilderFile',
46
47
  'elementTypeName',
47
48
  'terminalsName',
48
49
  'terminalsSourceName',
@@ -118,6 +119,7 @@ export function mergeTransferModelOptions(...sources: Array<Partial<GenerateTran
118
119
  astFile: astFile!,
119
120
  augmentationFile: augmentationFile!,
120
121
  outFile: outFile!,
122
+ astBuilderFile: pick('astBuilderFile'),
121
123
  elementTypeName: pick('elementTypeName'),
122
124
  terminalsName: pick('terminalsName'),
123
125
  terminalsSourceName: pick('terminalsSourceName'),
@@ -9,6 +9,7 @@
9
9
 
10
10
  import * as fs from 'fs';
11
11
  import * as path from 'path';
12
+ import { emitAstBuilder } from './generate-ast-builder.js';
12
13
  import {
13
14
  type Node,
14
15
  type ObjectLiteralExpression,
@@ -35,6 +36,15 @@ export interface GenerateTransferModelOptions {
35
36
  augmentationFile: string;
36
37
  /** Destination path for the generated transfer model. */
37
38
  outFile: string;
39
+ /**
40
+ * Destination for a generated AST-node builder module. Omitted by consumers
41
+ * that construct no AST nodes; the transfer model is emitted either way.
42
+ *
43
+ * Emitted from this command rather than its own because the AST source is
44
+ * already parsed here, and because a consumer that regenerates one artefact
45
+ * and not the other has the two disagreeing about the same grammar.
46
+ */
47
+ astBuilderFile?: string;
38
48
  /** Name used for the base element type in the output. Defaults to `TransferElement`. */
39
49
  elementTypeName?: string;
40
50
  /** Name used for the terminal-patterns const in the output. Defaults to `ModelTerminals`. */
@@ -396,10 +406,22 @@ export function generateTransferModel(options: GenerateTransferModelOptions): vo
396
406
  fs.mkdirSync(path.dirname(options.outFile), { recursive: true });
397
407
  if (fs.existsSync(options.outFile) && fs.readFileSync(options.outFile, 'utf-8') === content) {
398
408
  console.log('Transfer model is up to date.');
399
- return;
409
+ } else {
410
+ fs.writeFileSync(options.outFile, content, 'utf-8');
411
+ console.log(`Generated: ${options.outFile}`);
412
+ }
413
+
414
+ if (options.astBuilderFile !== undefined) {
415
+ const emitted = emitAstBuilder(astSource, {
416
+ outFile: options.astBuilderFile,
417
+ astFile: options.astFile,
418
+ languageId,
419
+ regenCommand
420
+ });
421
+ if (!emitted) {
422
+ console.warn(`Warning: no '${languageId}AstType' alias in ${options.astFile}; skipping the AST builder.`);
423
+ }
400
424
  }
401
- fs.writeFileSync(options.outFile, content, 'utf-8');
402
- console.log(`Generated: ${options.outFile}`);
403
425
  }
404
426
 
405
427
  // ---------------------------------------------------------------------------
@@ -310,9 +310,9 @@ __BIN__ },
310
310
  ],
311
311
  "scripts": {
312
312
  "build": "npm run generate && tsc",
313
- "clean": "rimraf lib syntaxes src/language-server/generated src/language-server/generated-transfer tsconfig.tsbuildinfo",
313
+ "clean": "rimraf lib syntaxes src/language-server/generated src/language-server/generated-hydranium tsconfig.tsbuildinfo",
314
314
  "generate": "npm run langium:generate && npm run generate:transfer-model",
315
- "generate:transfer-model": "hydranium-cli generate-transfer-model --ast-file src/language-server/generated/ast.ts --augmentation-file src/language-server/ast.ts --out-file src/language-server/generated-transfer/transfer-model.ts --element-type-name __NAME__Element --terminals-name __NAME__Terminals --regen-command \\"Run: __NPM_RUN__ generate:transfer-model\\"",
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-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
316
  "langium:generate": "langium generate",
317
317
  "langium:watch": "langium generate --watch",
318
318
  __LINT__ "start": "node lib/main.js --stdio",
@@ -439,6 +439,11 @@ const TSCONFIG_COMPILER_OPTIONS: ReadonlyArray<readonly [string, JsonValue]> = [
439
439
  ['outDir', 'lib'],
440
440
  ['strict', true],
441
441
  ['esModuleInterop', true],
442
+ // A server that renders its own user-facing messages loads a catalogue, and
443
+ // a catalogue is JSON. Without this the import does not resolve at all, and
444
+ // without the `include` glob below a composite project rejects it with
445
+ // TS6307 naming neither cause.
446
+ ['resolveJsonModule', true],
442
447
  ['skipLibCheck', true],
443
448
  ['declaration', true],
444
449
  ['experimentalDecorators', true],
@@ -480,7 +485,7 @@ function tsconfigJson(composition: InitComposition): string {
480
485
  ];
481
486
  const extendsLine = workspace?.baseTsconfig === undefined ? '' : ` "extends": "${workspace.baseTsconfig}",\n`;
482
487
  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`;
488
+ return `{\n${extendsLine} "compilerOptions": {\n${body}\n },\n "include": ["src", "src/**/*.json"]\n}\n`;
484
489
  }
485
490
 
486
491
  // `isolatedModules` is what makes this check agree with the transform that
@@ -498,7 +503,7 @@ const TSCONFIG_TEST = `{
498
503
  "isolatedModules": true,
499
504
  "types": ["node"]
500
505
  },
501
- "include": ["src", "test"]
506
+ "include": ["src", "test", "src/**/*.json"]
502
507
  }
503
508
  `;
504
509
 
@@ -708,13 +713,35 @@ export type __NAME__Services = LangiumServices &
708
713
  LspServerAddedServices & {
709
714
  shared: __NAME__SharedServices;
710
715
  };
716
+
717
+ /** What a host or a test may vary about this composition. */
718
+ export interface __NAME__Options {
719
+ /**
720
+ * Shared modules layered in after the framework's own bindings.
721
+ *
722
+ * The framework constructs most shared services with no options —
723
+ * \`DocumentBuilder: services => new HydraniumDocumentBuilder(services)\` — so
724
+ * rebinding the slot is the only way to boot one configured differently, and
725
+ * a factory that hard-codes its composition leaves a test nowhere to do it.
726
+ * That is what this is for: pass a module binding \`workspace.DocumentBuilder\`
727
+ * to exercise a builder option, or to substitute a subclass.
728
+ *
729
+ * Composed LAST, after \`__NAME__SharedModule\`, so it wins over every other
730
+ * tier including this file's own bindings — which is what makes it usable for
731
+ * a slot the adopter overrides. Production code should not reach for it.
732
+ */
733
+ readonly extraSharedModules?: ReadonlyArray<Module<__NAME__SharedServices, DeepPartial<__NAME__SharedServices>>>;
734
+ }
711
735
  ${configurationRoot}
712
736
  ${sharedModule}
713
737
 
714
738
  ${languageModules}
715
739
 
716
740
  /** Compose the Langium DI tree for __NAME__ — returns the shared + language services. */
717
- export function create__NAME__Services(context: Partial<ServerModuleContext> = EmptyFileSystem): {
741
+ export function create__NAME__Services(
742
+ context: Partial<ServerModuleContext> = EmptyFileSystem,
743
+ options: __NAME__Options = {}
744
+ ): {
718
745
  shared: __NAME__SharedServices;
719
746
  ${returnType}
720
747
  } {
@@ -724,7 +751,8 @@ ${returnType}
724
751
  sharedModules: {
725
752
  generated: __NAME__GeneratedSharedModule,
726
753
  adopter: __NAME__SharedModule,
727
- extra: [createLspServerSharedModule(fullContext)]
754
+ extra: [createLspServerSharedModule(fullContext)],
755
+ overrides: options.extraSharedModules
728
756
  },
729
757
  languageModules: {
730
758
  generated: ${primary.grammar}GeneratedModule,
@@ -980,7 +1008,7 @@ import { startGlspServer } from '@hydranium/glsp-server/node';
980
1008
  // \`TransferElement\` structurally, so naming the AST type here compiles fine and
981
1009
  // silently tells every typed client that a reference is a resolvable object
982
1010
  // rather than a name.
983
- ${importList(roots, './language-server/generated-transfer/transfer-model.js', columns, true)}
1011
+ ${importList(roots, './language-server/generated-hydranium/transfer-model.js', columns, true)}
984
1012
  `
985
1013
  : '';
986
1014
 
@@ -1063,7 +1091,13 @@ void glspServer;
1063
1091
  // than a library entry — import \`./index.js\` instead to compose the language.
1064
1092
 
1065
1093
  ${reflectImport}${glspImports}${importList(coreNodeSymbols, '@hydranium/core/node', columns)}
1066
- ${data ? "import { DataServer } from '@hydranium/data-server';\n" : ''}import { startLanguageServer } from '@hydranium/langium/lsp';
1094
+ ${data ? "import { DataServer } from '@hydranium/data-server';\n" : ''}// The framework's entry point, NOT Langium's \`@hydranium/langium/lsp\` one. It is
1095
+ // signature-compatible and delegates straight through; what it adds first is
1096
+ // \`assertLspHeadComposed\`, which fails the start if the LSP head's SHARED module
1097
+ // was never composed. Reaching for Langium's is the natural mistake and it is
1098
+ // silent: the server boots, links and completes, while echo suppression, the
1099
+ // didChangeContent debounce and the last-client-close rebuild are all inert.
1100
+ import { startLanguageServer } from '@hydranium/core/lsp';
1067
1101
  import { ProposedFeatures, createConnection } from 'vscode-languageserver/node';
1068
1102
  ${diagramImports}${transferImports}import { create__NAME__Services } from './language-server/__PROJECT_ID__-module.js';
1069
1103
  ${portCommands === '' ? '' : '\n' + portCommands}
@@ -1117,7 +1151,7 @@ function dataServerMainFile(composition: InitComposition): string {
1117
1151
  ${importList(['NodeFileSystem', 'startStdioServer'], '@hydranium/core/node', columns)}
1118
1152
  import { DataServer } from '@hydranium/data-server';
1119
1153
  // 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)}
1154
+ ${importList(roots, './language-server/generated-hydranium/transfer-model.js', columns, true)}
1121
1155
  import { create__NAME__Services } from './language-server/__PROJECT_ID__-module.js';
1122
1156
 
1123
1157
  const { shared } = create__NAME__Services({ ...NodeFileSystem });
@@ -1247,7 +1281,7 @@ function serializationTest(composition: InitComposition): string {
1247
1281
  import { parseHelper } from '@hydranium/core/testing';
1248
1282
  import { describe, expect, it } from 'vitest';
1249
1283
  ${importList(astTypes, '../src/language-server/ast.js', columns, true)}
1250
- ${importList(transferTypes, '../src/language-server/generated-transfer/transfer-model.js', columns, true)}
1284
+ ${importList(transferTypes, '../src/language-server/generated-hydranium/transfer-model.js', columns, true)}
1251
1285
  import { createServices } from '../src/services.js';
1252
1286
 
1253
1287
  ${composition.grammars.map(grammar => render(SERIALIZATION_SUITE, composition, grammar)).join('\n\n')}
@@ -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,21 @@ 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
+ onDocumentDeleted(event: TransferDocumentDeletedEvent): void {
141
+ // In band, unlike the two neighbours: this is the end of the stream
142
+ // the view exists to show, and a consumer that never hears it waits
143
+ // forever for an update that cannot come. The line carries no
144
+ // `document`, so a reader must switch on shape rather than assume one.
145
+ if (event.uri !== options.uri) {
146
+ return;
147
+ }
148
+ write(`${JSON.stringify(event)}\n`);
149
+ },
150
+ onDocumentsBuilt(): void {
151
+ // Out of band for a per-URI view by construction: this notification
152
+ // carries only documents nobody watches, and this command watches the
153
+ // one URI it was given.
154
+ },
135
155
  onProjectsChanged(): void {
136
156
  // Project lifecycle is out of band for the per-URI update view.
137
157
  }
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  * SPDX-License-Identifier: MIT
8
8
  ********************************************************************************/
9
9
 
10
+ export * from './commands/generate-ast-builder.js';
10
11
  export * from './commands/generate-transfer-model.js';
11
12
  export * from './commands/projects.js';
12
13
  export * from './commands/query.js';