@hydranium/cli 1.0.0-next.406 → 1.0.0-next.411

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 (54) hide show
  1. package/README.md +23 -5
  2. package/lib/cli.js +2 -0
  3. package/lib/cli.js.map +1 -1
  4. package/lib/commands/ast-ground-truth-args.js +2 -2
  5. package/lib/commands/ast-ground-truth-args.js.map +1 -1
  6. package/lib/commands/harness-args.d.ts +6 -5
  7. package/lib/commands/harness-args.d.ts.map +1 -1
  8. package/lib/commands/harness-args.js +66 -7
  9. package/lib/commands/harness-args.js.map +1 -1
  10. package/lib/commands/init-templates.d.ts.map +1 -1
  11. package/lib/commands/init-templates.js +64 -19
  12. package/lib/commands/init-templates.js.map +1 -1
  13. package/lib/commands/init.d.ts.map +1 -1
  14. package/lib/commands/init.js +3 -1
  15. package/lib/commands/init.js.map +1 -1
  16. package/lib/commands/lint-grammar-args.js +2 -2
  17. package/lib/commands/lint-grammar-args.js.map +1 -1
  18. package/lib/commands/measure-memory-args.js +2 -2
  19. package/lib/commands/measure-memory-args.js.map +1 -1
  20. package/lib/commands/message-catalogue-args.d.ts +16 -0
  21. package/lib/commands/message-catalogue-args.d.ts.map +1 -0
  22. package/lib/commands/message-catalogue-args.js +53 -0
  23. package/lib/commands/message-catalogue-args.js.map +1 -0
  24. package/lib/commands/message-catalogue.d.ts +21 -0
  25. package/lib/commands/message-catalogue.d.ts.map +1 -0
  26. package/lib/commands/message-catalogue.js +48 -0
  27. package/lib/commands/message-catalogue.js.map +1 -0
  28. package/lib/commands/model-docs-args.js +2 -2
  29. package/lib/commands/model-docs-args.js.map +1 -1
  30. package/lib/commands/reflect-args.js +2 -2
  31. package/lib/commands/reflect-args.js.map +1 -1
  32. package/lib/commands/validate-args.js +2 -2
  33. package/lib/commands/validate-args.js.map +1 -1
  34. package/lib/index.d.ts +1 -0
  35. package/lib/index.d.ts.map +1 -1
  36. package/lib/index.js +1 -0
  37. package/lib/index.js.map +1 -1
  38. package/package.json +19 -18
  39. package/src/cli.ts +2 -0
  40. package/src/commands/ast-ground-truth-args.ts +2 -2
  41. package/src/commands/harness-args.ts +69 -7
  42. package/src/commands/init-templates.ts +68 -19
  43. package/src/commands/init.ts +5 -1
  44. package/src/commands/lint-grammar-args.ts +2 -2
  45. package/src/commands/measure-memory-args.ts +2 -2
  46. package/src/commands/message-catalogue-args.ts +55 -0
  47. package/src/commands/message-catalogue.ts +56 -0
  48. package/src/commands/model-docs-args.ts +2 -2
  49. package/src/commands/reflect-args.ts +2 -2
  50. package/src/commands/validate-args.ts +2 -2
  51. package/src/index.ts +1 -0
  52. package/templates/host-browser/src/editor.ts +14 -0
  53. package/templates/server/src/language-server/__GRAMMAR_ID__-semantic-tokens.ts +10 -0
  54. package/templates/server/test/__GRAMMAR_ID__-semantic-tokens.test.ts +34 -0
@@ -596,6 +596,9 @@ __DEPENDENCIES__
596
596
  },
597
597
  "engines": {
598
598
  "node": ">=22.13"
599
+ },
600
+ "hydranium": {
601
+ "services": "./lib/node/services.js"
599
602
  }
600
603
  }
601
604
  `;
@@ -896,8 +899,9 @@ export const __CONFIG_ROOT_CONST___CONFIGURATION_ROOT = '__PROJECT_ID__';
896
899
  };`
897
900
  : `const __NAME__SharedModule: Module<__NAME__SharedServices, PartialLangiumSharedServices> = {};`;
898
901
 
899
- const serializerImports = composition.grammars
902
+ const languageImports = composition.grammars
900
903
  .flatMap(grammar => [
904
+ `import { ${grammar.grammar}SemanticTokenProvider } from './${grammar.grammarId}-semantic-tokens.js';`,
901
905
  `import { ${grammar.grammar}Serializer } from './${grammar.grammarId}-serializer.js';`,
902
906
  `import { ${grammar.grammar}ValidationContribution } from './${grammar.grammarId}-validation.js';`
903
907
  ])
@@ -909,7 +913,12 @@ export const __CONFIG_ROOT_CONST___CONFIGURATION_ROOT = '__PROJECT_ID__';
909
913
  // serializer — and that one throws on a `$type` it has no emitter for.
910
914
  const languageModules = composition.grammars
911
915
  .map(
912
- grammar => `const ${grammar.grammar}LanguageModule: Module<__NAME__Services, PartialLangiumServices & DeepPartial<ServerAddedServices>> = {
916
+ grammar => `const ${grammar.grammar}LanguageModule = (
917
+ options: __NAME__Options
918
+ ): Module<__NAME__Services, PartialLangiumServices & DeepPartial<ServerAddedServices>> => ({
919
+ lsp: {
920
+ SemanticTokenProvider: services => new ${grammar.grammar}SemanticTokenProvider(services, options)
921
+ },
913
922
  serializer: {
914
923
  Serializer: services => new ${grammar.grammar}Serializer(services)
915
924
  },
@@ -918,13 +927,15 @@ export const __CONFIG_ROOT_CONST___CONFIGURATION_ROOT = '__PROJECT_ID__';
918
927
  names: () => new ${grammar.grammar}ValidationContribution()
919
928
  }
920
929
  }
921
- };`
930
+ });`
922
931
  )
923
932
  .join('\n\n');
924
933
 
925
934
  const additionalLanguages = multi
926
935
  ? `,\n additionalLanguages: ${arrayLiteral(
927
- additional.map(grammar => `{ generated: ${grammar.grammar}GeneratedModule, adopter: () => ${grammar.grammar}LanguageModule }`),
936
+ additional.map(
937
+ grammar => `{ generated: ${grammar.grammar}GeneratedModule, adopter: () => ${grammar.grammar}LanguageModule(options) }`
938
+ ),
928
939
  ' ',
929
940
  columns
930
941
  )}`
@@ -940,8 +951,8 @@ export const __CONFIG_ROOT_CONST___CONFIGURATION_ROOT = '__PROJECT_ID__';
940
951
  // overrides via Langium's \`inject()\` (through the framework's
941
952
  // \`createIntegrationServices\`). Each adopter module binds its language's
942
953
  // \`Serializer\`, which the framework cannot default because a concrete syntax
943
- // is grammar knowledge, and its validation checks. Everything else (scope,
944
- // naming, project management, build pipeline) boots on the framework defaults;
954
+ // is grammar knowledge, its semantic tokens and its validation checks. Scope,
955
+ // naming, project management and the build pipeline use the framework defaults;
945
956
  // scope computation and AST extensions go in these same modules.
946
957
  //
947
958
  // Note which generated symbol comes from which name: the SHARED module is
@@ -965,7 +976,12 @@ export const __CONFIG_ROOT_CONST___CONFIGURATION_ROOT = '__PROJECT_ID__';
965
976
  }
966
977
 
967
978
  ${importList(
968
- ['createLspServerLanguageModule', 'createLspServerSharedModule', 'type LspServerAddedServices', 'type LspServerAddedSharedServices'],
979
+ [
980
+ 'createLspServerLanguageModule',
981
+ 'createLspServerSharedModule',
982
+ 'type HydraniumSemanticTokenProviderOptions',
983
+ 'type LspServerAddedSharedServices'
984
+ ],
969
985
  '@hydranium/core/lsp',
970
986
  columns
971
987
  )}
@@ -973,22 +989,21 @@ import { createIntegrationServices, type ServerAddedServices, type ServerModuleC
973
989
  import { type DeepPartial, EmptyFileSystem, type Module } from '@hydranium/langium';
974
990
  import { type LangiumServices, type PartialLangiumServices, type PartialLangiumSharedServices } from '@hydranium/langium/lsp';
975
991
  ${importList(generatedImports, './generated/module.js', columns)}
976
- ${serializerImports}
992
+ ${languageImports}
977
993
 
978
994
  export type __NAME__SharedServices = ServerSharedServices & LspServerAddedSharedServices;
979
995
  export type __NAME__Services = LangiumServices &
980
- ServerAddedServices &
981
- LspServerAddedServices & {
996
+ ServerAddedServices & {
982
997
  shared: __NAME__SharedServices;
983
998
  };
984
999
 
985
1000
  /** What a host or a test may vary about this composition. */
986
- export interface __NAME__Options {
1001
+ export interface __NAME__Options extends HydraniumSemanticTokenProviderOptions {
987
1002
  /**
988
1003
  * Shared modules layered in after the framework's own bindings.
989
1004
  *
990
1005
  * The framework constructs most shared services with no options —
991
- * \`DocumentBuilder: services => new HydraniumDocumentBuilder(services)\` — so
1006
+ * \`DocumentBuilder: services => new DefaultHydraniumDocumentBuilder(services)\` — so
992
1007
  * rebinding the slot is the only way to boot one configured differently, and
993
1008
  * a factory that hard-codes its composition leaves a test nowhere to do it.
994
1009
  * That is what this is for: pass a module binding \`workspace.DocumentBuilder\`
@@ -1024,7 +1039,7 @@ ${returnType}
1024
1039
  },
1025
1040
  languageModules: {
1026
1041
  generated: ${primary.grammar}GeneratedModule,
1027
- adopter: () => ${primary.grammar}LanguageModule,
1042
+ adopter: () => ${primary.grammar}LanguageModule(options),
1028
1043
  extra: [createLspServerLanguageModule(fullContext)]
1029
1044
  }${additionalLanguages}
1030
1045
  });
@@ -1783,13 +1798,13 @@ function readme(composition: InitComposition): string {
1783
1798
  // nowhere else.
1784
1799
  '```bash',
1785
1800
  '# Grammar / AST reflection',
1786
- 'npx hydranium-cli reflect --services ./lib/node/services.js',
1801
+ 'npx hydranium-cli reflect',
1787
1802
  '',
1788
1803
  '# Grammar-convention lint (CI gate)',
1789
- 'npx hydranium-cli lint-grammar --services ./lib/node/services.js',
1804
+ 'npx hydranium-cli lint-grammar',
1790
1805
  '',
1791
1806
  '# Validate a workspace of model files (non-zero exit on errors)',
1792
- 'npx hydranium-cli validate --services ./lib/node/services.js ../workspace',
1807
+ 'npx hydranium-cli validate ../workspace',
1793
1808
  ...(data
1794
1809
  ? [
1795
1810
  '',
@@ -1814,6 +1829,7 @@ function readme(composition: InitComposition): string {
1814
1829
  '- `src/language-server/__PROJECT_ID__-module.ts` — `create__NAME__Services` DI wiring',
1815
1830
  ...composition.grammars.flatMap(grammar => [
1816
1831
  `- \`src/language-server/${grammar.grammarId}-serializer.ts\` — emits \`${grammar.grammar}\` back to text (the framework defaults this to a throw)`,
1832
+ `- \`src/language-server/${grammar.grammarId}-semantic-tokens.ts\` — colours declarations and references; the browser enables keywords and comments too`,
1817
1833
  `- \`src/language-server/${grammar.grammarId}-validation.ts\` — the \`${grammar.grammar}\` checks: a warning on a repeated name`
1818
1834
  ]),
1819
1835
  "- `src/node/services.ts` — zero-arg `createServices()` for the headless CLI, the package's `./node` entry",
@@ -2179,6 +2195,8 @@ function serverFiles(composition: InitComposition): InitFile[] {
2179
2195
  // binding to add, rather than guessing a syntax.
2180
2196
  ...composition.grammars.flatMap(grammar => [
2181
2197
  grammarTemplate('server/src/language-server/__GRAMMAR_ID__-serializer.ts', composition, grammar),
2198
+ grammarTemplate('server/src/language-server/__GRAMMAR_ID__-semantic-tokens.ts', composition, grammar),
2199
+ grammarTemplate('server/test/__GRAMMAR_ID__-semantic-tokens.test.ts', composition, grammar),
2182
2200
  grammarTemplate('server/src/language-server/__GRAMMAR_ID__-validation.ts', composition, grammar)
2183
2201
  ]),
2184
2202
  { path: 'server/src/index.ts', content: indexFile(composition) },
@@ -2549,14 +2567,14 @@ function hostWorkerFile(composition: InitComposition): string {
2549
2567
  ];
2550
2568
 
2551
2569
  const services = `const shared = create${composition.name}Services({ ...fileSystem }).shared;`;
2552
- const lspCall = `startLanguageServerInWorker(ports.lsp, connection => create${composition.name}Services({ connection, ...fileSystem }).shared);`;
2570
+ const lspCall = `startLanguageServerInWorker(ports.lsp, connection => create${composition.name}Services({ connection, ...fileSystem }, { highlightKeywords: true, highlightComments: true }).shared);`;
2553
2571
  const startServices = !lsp
2554
2572
  ? services
2555
2573
  : ` const shared = ${lspCall}`.length <= columns
2556
2574
  ? `const shared = ${lspCall}`
2557
2575
  : `const shared = startLanguageServerInWorker(
2558
2576
  ports.lsp,
2559
- connection => create${composition.name}Services({ connection, ...fileSystem }).shared
2577
+ connection => create${composition.name}Services({ connection, ...fileSystem }, { highlightKeywords: true, highlightComments: true }).shared
2560
2578
  );`;
2561
2579
  const dataServer = `connection => new DataServer<${roots.join(' | ')}>(connection, shared)`;
2562
2580
  const dataCall = `startDataServerInWorker(ports.data, shared, ${dataServer});`;
@@ -2844,7 +2862,35 @@ ${checks.join('\n')}
2844
2862
  ${edit}
2845
2863
  expect(errors).toEqual([]);
2846
2864
  });
2847
- ${guardTest}`;
2865
+ ${guardTest}${
2866
+ lsp
2867
+ ? `
2868
+ test('colours declarations, references, keywords and comments', async ({ page }) => {
2869
+ await page.goto('/');
2870
+ await expect(page.locator('#status')).toHaveText('ready');
2871
+ const editor = page.locator('#editor .view-lines');
2872
+ const second = editor.locator('span').filter({ hasText: /^Second$/ });
2873
+ const keyword = editor
2874
+ .locator('span')
2875
+ .filter({ hasText: /^node$/ })
2876
+ .first();
2877
+ for (const extension of [${composition.grammars.map(grammar => `'${grammar.extensions[0]}'`).join(', ')}]) {
2878
+ await page.locator('#file').selectOption(\`nodes.\${extension}\`);
2879
+ await expect(editor.locator('span').filter({ hasText: /^First$/ })).toHaveCSS('color', 'rgb(36, 122, 147)');
2880
+ await expect(second).toHaveCount(2);
2881
+ for (const target of await second.all()) {
2882
+ await expect(target).toHaveCSS('color', 'rgb(36, 122, 147)');
2883
+ }
2884
+ await expect(keyword).toHaveCSS('color', 'rgb(0, 0, 255)');
2885
+ await expect(editor.locator('.view-line > span > span').filter({ hasText: /A.second.node/ })).toHaveCSS('color', 'rgb(0, 128, 0)');
2886
+ await page.locator('#file').selectOption(\`references.\${extension}\`);
2887
+ await expect(editor.locator('span').filter({ hasText: /^Third$/ })).toHaveCSS('color', 'rgb(36, 122, 147)');
2888
+ await expect(second).toHaveCSS('color', 'rgb(36, 122, 147)');
2889
+ }
2890
+ });
2891
+ `
2892
+ : ''
2893
+ }`;
2848
2894
  }
2849
2895
 
2850
2896
  /** The browser host's README. */
@@ -4503,6 +4549,9 @@ ${sortedBlock(devDependencies, ' ')}
4503
4549
  },
4504
4550
  "engines": {
4505
4551
  "node": ">=22.13"
4552
+ },
4553
+ "hydranium": {
4554
+ "services": "./server/lib/node/services.js"
4506
4555
  }
4507
4556
  }
4508
4557
  `;
@@ -720,7 +720,11 @@ export function runInit(options: InitCommandOptions): void {
720
720
  }
721
721
  // `npx`, not a bare invocation: `@hydranium/cli` is a devDependency of the
722
722
  // scaffold, so the binary is on PATH inside an npm script and nowhere else.
723
- write(` npx hydranium-cli reflect --services ./${server}/lib/node/services.js\n`);
723
+ write(
724
+ detection === undefined
725
+ ? ' npx hydranium-cli reflect\n'
726
+ : ` npx hydranium-cli reflect --services ./${server}/lib/node/services.js\n`
727
+ );
724
728
  const hostDir = (host: InitHost): string => (detection === undefined ? `host-${host}` : `${detection.targetPath}/host-${host}`);
725
729
  // A standalone root runs a host as `start:<host>`. A surrounding workspace's root is not ours to add to.
726
730
  const start = (host: InitHost): string => (detection === undefined ? `npm run start:${host}` : `npm --prefix ${hostDir(host)} start`);
@@ -38,7 +38,7 @@ export const LINT_GRAMMAR_VALUE_FLAGS: readonly string[] = [...HARNESS_VALUE_FLA
38
38
 
39
39
  /** The `--help` text, as data, held to {@link LINT_GRAMMAR_FLAGS} by a test. */
40
40
  export const LINT_GRAMMAR_HELP: readonly string[] = [
41
- 'Usage: hydranium-cli lint-grammar --services <module> [--import <specifier>]... [--name-property <p>]... [--strict] [--json] [--log-level <lvl>] [--log-file <file>] [--log-file-level <lvl>]',
41
+ 'Usage: hydranium-cli lint-grammar [--services <module>] [--import <specifier>]... [--name-property <p>]... [--strict] [--json] [--log-level <lvl>] [--log-file <file>] [--log-file-level <lvl>]',
42
42
  '',
43
43
  "Check a head's grammar against the framework's conventions — a CI gate: the",
44
44
  'process exits non-zero when a violation is found. Read-only: no workspace is',
@@ -49,7 +49,7 @@ export const LINT_GRAMMAR_HELP: readonly string[] = [
49
49
  'zero-arg `createServices(): { shared }` thunk. Needs @hydranium/core.',
50
50
  '',
51
51
  'Options:',
52
- ' --services <module> ESM module exporting `createServices(): { shared }` (required).',
52
+ ' --services <module> ESM module exporting `createServices(): { shared }` (required) unless package.json sets hydranium.services.',
53
53
  ...importHelpLines(23),
54
54
  ' --name-property <p> Property that satisfies the nameability convention',
55
55
  ' (repeatable). Default: name.',
@@ -49,7 +49,7 @@ export const MEASURE_MEMORY_VALUE_FLAGS: readonly string[] = [...HARNESS_VALUE_F
49
49
 
50
50
  /** The `--help` text, as data, held to {@link MEASURE_MEMORY_FLAGS} by a test. */
51
51
  export const MEASURE_MEMORY_HELP: readonly string[] = [
52
- 'Usage: hydranium-cli measure-memory --services <module> [--import <specifier>]... <workspace> [options]',
52
+ 'Usage: hydranium-cli measure-memory [--services <module>] [--import <specifier>]... <workspace> [options]',
53
53
  '',
54
54
  'Measure model-store memory for a workspace. `<module>` is an ESM module that',
55
55
  'exports a zero-arg `createServices(): { shared }` thunk (the head wires its own',
@@ -57,7 +57,7 @@ export const MEASURE_MEMORY_HELP: readonly string[] = [
57
57
  'baseline / after-build / churn lines print on stdout. Needs @hydranium/core.',
58
58
  '',
59
59
  'Options:',
60
- ' --services <module> ESM module exporting `createServices(): { shared }` (required).',
60
+ ' --services <module> ESM module exporting `createServices(): { shared }` (required) unless package.json sets hydranium.services.',
61
61
  ...importHelpLines(22),
62
62
  ' <workspace> Workspace root (path or file URI) to build (required).',
63
63
  ' --edits <N> Rebuild-churn cycles to probe for retention. Default: 0 (skip).',
@@ -0,0 +1,55 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 CrossBreeze, 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 { assertRequired, exitWithUsage, helpRequested, printHelp, type UsageError } from './harness-args.js';
11
+ import { runMessageCatalogue, type MessageCatalogueCommandOptions } from './message-catalogue.js';
12
+
13
+ export const MESSAGE_CATALOGUE_FLAGS: readonly string[] = ['--barrel', '--out-file'];
14
+ export const MESSAGE_CATALOGUE_VALUE_FLAGS = MESSAGE_CATALOGUE_FLAGS;
15
+ export const MESSAGE_CATALOGUE_HELP: readonly string[] = [
16
+ 'Usage: hydranium-cli message-catalogue --barrel <module>... [--out-file <file>]',
17
+ '',
18
+ 'Emit a flat JSON starter catalogue mapping message codes to English defaults.',
19
+ 'Modules are resolved from the current project and must be built first.',
20
+ 'Repeat --barrel for each framework head and your own messages. No server starts.',
21
+ '',
22
+ 'Options:',
23
+ ' --barrel <module> Messages package specifier or ESM file (required). Repeatable.',
24
+ ' --out-file <file> Write JSON here instead of stdout, after all imports succeed.'
25
+ ];
26
+
27
+ export function parseMessageCatalogueArgs(args: string[], onError: UsageError = exitWithUsage): MessageCatalogueCommandOptions {
28
+ const barrels: string[] = [];
29
+ let outFile: string | undefined;
30
+ for (let index = 0; index < args.length; index++) {
31
+ const flag = args[index];
32
+ if (!MESSAGE_CATALOGUE_FLAGS.includes(flag)) {
33
+ return onError(`Unexpected argument: ${flag} (hydranium-cli message-catalogue --help)`);
34
+ }
35
+ const value = args[++index];
36
+ if (value === undefined || value.startsWith('--')) {
37
+ return onError(`Missing value for ${flag}`);
38
+ }
39
+ if (flag === '--barrel') {
40
+ barrels.push(value);
41
+ } else {
42
+ outFile = value;
43
+ }
44
+ }
45
+ assertRequired(barrels[0], '--barrel', 'message-catalogue', onError);
46
+ return { barrels, outFile };
47
+ }
48
+
49
+ export async function runMessageCatalogueCommand(args: string[]): Promise<void> {
50
+ if (helpRequested(args, MESSAGE_CATALOGUE_VALUE_FLAGS)) {
51
+ printHelp(MESSAGE_CATALOGUE_HELP);
52
+ return;
53
+ }
54
+ await runMessageCatalogue(parseMessageCatalogueArgs(args));
55
+ }
@@ -0,0 +1,56 @@
1
+ /********************************************************************************
2
+ * Copyright (c) 2026 CrossBreeze, 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 { collectMessages, type MessageCatalogue } from '@hydranium/protocol';
11
+ import { resolve as resolveModule } from 'import-meta-resolve';
12
+ import { isAbsolute, resolve } from 'node:path';
13
+ import { pathToFileURL } from 'node:url';
14
+ import { emitReport } from './headless-harness.js';
15
+
16
+ /**
17
+ * Enumerate declarations rather than scanning source. Conflicting defaults
18
+ * must fail before any output is written, or import order chooses the English.
19
+ */
20
+ export function createMessageCatalogue(barrels: readonly object[]): MessageCatalogue {
21
+ const entries = new Map<string, string>();
22
+ for (const barrel of barrels) {
23
+ for (const message of collectMessages(barrel)) {
24
+ const previous = entries.get(message.code);
25
+ if (previous !== undefined && previous !== message.text) {
26
+ throw new Error(`Conflicting English defaults for message '${message.code}'.`);
27
+ }
28
+ entries.set(message.code, message.text);
29
+ }
30
+ }
31
+ return Object.fromEntries([...entries].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)));
32
+ }
33
+
34
+ export interface MessageCatalogueCommandOptions {
35
+ readonly barrels: readonly string[];
36
+ readonly outFile?: string;
37
+ }
38
+
39
+ /** Resolve against the caller's project, including when the CLI is installed globally. */
40
+ export async function runMessageCatalogue(options: MessageCatalogueCommandOptions): Promise<void> {
41
+ const projectUrl = pathToFileURL(resolve('package.json')).href;
42
+ const barrels: object[] = [];
43
+ for (const specifier of options.barrels) {
44
+ const target =
45
+ specifier.startsWith('.') || isAbsolute(specifier) ? pathToFileURL(resolve(specifier)).href : resolveModule(specifier, projectUrl);
46
+ const barrel = (await import(target)) as object;
47
+ if (collectMessages(barrel).length === 0) {
48
+ throw new Error(`Message barrel '${specifier}' exports no declarations. Pass a built messages module.`);
49
+ }
50
+ barrels.push(barrel);
51
+ }
52
+ if (barrels.length === 0) {
53
+ throw new Error('Pass at least one --barrel messages module.');
54
+ }
55
+ emitReport(JSON.stringify(createMessageCatalogue(barrels), undefined, 2), options.outFile);
56
+ }
@@ -37,7 +37,7 @@ export const MODEL_DOCS_VALUE_FLAGS: readonly string[] = MODEL_DOCS_FLAGS;
37
37
 
38
38
  /** The `--help` text, as data, held to {@link MODEL_DOCS_FLAGS} by a test. */
39
39
  export const MODEL_DOCS_HELP: readonly string[] = [
40
- 'Usage: hydranium-cli model-docs --services <module> [--import <specifier>]... [--out-file <file>] [--log-level <lvl>] [--log-file <file>] [--log-file-level <lvl>]',
40
+ 'Usage: hydranium-cli model-docs [--services <module>] [--import <specifier>]... [--out-file <file>] [--log-level <lvl>] [--log-file <file>] [--log-file-level <lvl>]',
41
41
  '',
42
42
  'Generate a navigable Markdown reference of every AST node type — a type index,',
43
43
  'cross-linked super/sub types, a reverse "referenced by" index, and a per-language',
@@ -47,7 +47,7 @@ export const MODEL_DOCS_HELP: readonly string[] = [
47
47
  'zero-arg `createServices(): { shared }` thunk. Needs @hydranium/core.',
48
48
  '',
49
49
  'Options:',
50
- ' --services <module> ESM module exporting `createServices(): { shared }` (required).',
50
+ ' --services <module> ESM module exporting `createServices(): { shared }` (required) unless package.json sets hydranium.services.',
51
51
  ...importHelpLines(22),
52
52
  ' --out-file <file> Write the Markdown to this file instead of stdout. Written',
53
53
  ' only once the report exists, unlike a shell redirection,',
@@ -38,7 +38,7 @@ export const REFLECT_VALUE_FLAGS: readonly string[] = [...HARNESS_VALUE_FLAGS, .
38
38
 
39
39
  /** The `--help` text, as data, held to {@link REFLECT_FLAGS} by a test. */
40
40
  export const REFLECT_HELP: readonly string[] = [
41
- 'Usage: hydranium-cli reflect --services <module> [--import <specifier>]... [--json] [--out-file <file>] [--log-level <lvl>] [--log-file <file>] [--log-file-level <lvl>]',
41
+ 'Usage: hydranium-cli reflect [--services <module>] [--import <specifier>]... [--json] [--out-file <file>] [--log-level <lvl>] [--log-file <file>] [--log-file-level <lvl>]',
42
42
  '',
43
43
  "Dump a head's grammar/AST reflection — the type hierarchy, each language's",
44
44
  'terminals and entry rule, and every cross-reference target. Read-only: no',
@@ -47,7 +47,7 @@ export const REFLECT_HELP: readonly string[] = [
47
47
  'exporting a zero-arg `createServices(): { shared }` thunk. Needs @hydranium/core.',
48
48
  '',
49
49
  'Options:',
50
- ' --services <module> ESM module exporting `createServices(): { shared }` (required).',
50
+ ' --services <module> ESM module exporting `createServices(): { shared }` (required) unless package.json sets hydranium.services.',
51
51
  ...importHelpLines(22),
52
52
  ' --json Emit the raw JSON reflection instead of the Markdown report.',
53
53
  ' --out-file <file> Write the report to this file instead of stdout. Written',
@@ -38,7 +38,7 @@ export const VALIDATE_VALUE_FLAGS: readonly string[] = [...HARNESS_VALUE_FLAGS,
38
38
 
39
39
  /** The `--help` text, as data, held to {@link VALIDATE_FLAGS} by a test. */
40
40
  export const VALIDATE_HELP: readonly string[] = [
41
- 'Usage: hydranium-cli validate --services <module> [--import <specifier>]... <workspace> [--strict] [--json] [--out-file <file>] [--log-level <lvl>] [--log-file <file>] [--log-file-level <lvl>]',
41
+ 'Usage: hydranium-cli validate [--services <module>] [--import <specifier>]... <workspace> [--strict] [--json] [--out-file <file>] [--log-level <lvl>] [--log-file <file>] [--log-file-level <lvl>]',
42
42
  '',
43
43
  'Build a workspace headlessly and report its validation diagnostics — a CI gate:',
44
44
  'the process exits non-zero when any error is found. `<module>` is an ESM module',
@@ -46,7 +46,7 @@ export const VALIDATE_HELP: readonly string[] = [
46
46
  'filesystem inside). Needs @hydranium/core.',
47
47
  '',
48
48
  'Options:',
49
- ' --services <module> ESM module exporting `createServices(): { shared }` (required).',
49
+ ' --services <module> ESM module exporting `createServices(): { shared }` (required) unless package.json sets hydranium.services.',
50
50
  ...importHelpLines(22),
51
51
  ' <workspace> Workspace root (path or file URI) to validate (required).',
52
52
  ' --strict Also fail (non-zero exit) on warnings, not only errors.',
package/src/index.ts CHANGED
@@ -17,3 +17,4 @@ export * from './commands/save.js';
17
17
  export * from './commands/watch.js';
18
18
  export * from './package-name.js';
19
19
  export * from './spawn-data-server.js';
20
+ export * from './commands/message-catalogue.js';
@@ -6,6 +6,7 @@ import { initializeLanguageClient, type WorkerHeads } from '@hydranium/client-br
6
6
  import { MONACO_CLIENT_CAPABILITIES, MonacoLspClient } from '@hydranium/client-browser/monaco';
7
7
  import { describeError } from '@hydranium/protocol';
8
8
  import { ALL_LANGUAGES } from '__PROTOCOL_PACKAGE__';
9
+ import * as monaco from 'monaco-editor-core';
9
10
  import { PublishDiagnosticsNotification } from 'vscode-languageserver-protocol';
10
11
  import { appendLog, setStatus } from './log.js';
11
12
  import type { ShownFile } from './shown-file.js';
@@ -18,6 +19,18 @@ declare const __MONACO_WORKER_BUNDLE_URL__: string;
18
19
  // bundle emits, and fails on the first edit.
19
20
  self.MonacoEnvironment = { getWorker: () => new Worker(__MONACO_WORKER_BUNDLE_URL__) };
20
21
 
22
+ // Standalone Monaco has no TextMate scopes to map semantic types to colours.
23
+ monaco.editor.defineTheme('__PROJECT_ID__', {
24
+ base: 'vs',
25
+ inherit: true,
26
+ rules: [
27
+ { token: 'class', foreground: '247a93' },
28
+ { token: 'keyword', foreground: '0000ff' },
29
+ { token: 'comment', foreground: '008000' }
30
+ ],
31
+ colors: {}
32
+ });
33
+
21
34
  /** Each language as Monaco registers it, from the ids and extensions the server generates. */
22
35
  const LANGUAGES = ALL_LANGUAGES.map(language => ({
23
36
  id: language.languageId,
@@ -45,6 +58,7 @@ export async function startEditor(heads: WorkerHeads, shownFile: ShownFile): Pro
45
58
  const modelOf = (uri: string) => client.openDocument(uri, shownFile.files.get(uri) as string);
46
59
  const editor = client.createEditor(document.getElementById('editor') as HTMLElement, modelOf(shownFile.current()), {
47
60
  automaticLayout: true,
61
+ theme: '__PROJECT_ID__',
48
62
  minimap: { enabled: false },
49
63
  scrollBeyondLastLine: false,
50
64
  wordWrap: 'on'
@@ -0,0 +1,10 @@
1
+ import { AbstractHydraniumSemanticTokenProvider, type SemanticTokenKind } from '@hydranium/core/lsp';
2
+ import { SemanticTokenTypes } from 'vscode-languageserver';
3
+ import { __NODE_RULE__ } from './ast.js';
4
+
5
+ /** Declarations and references share the token kind of their target type. */
6
+ export class __GRAMMAR__SemanticTokenProvider extends AbstractHydraniumSemanticTokenProvider {
7
+ protected override getTokenKind(type: string): SemanticTokenKind | undefined {
8
+ return type === __NODE_RULE__.$type ? SemanticTokenTypes.class : undefined;
9
+ }
10
+ }
@@ -0,0 +1,34 @@
1
+ import { parseHelper } from '@hydranium/core/testing';
2
+ import { SemanticTokensDecoder } from '@hydranium/langium/lsp';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { create__NAME__Services } from '../src/language-server/__PROJECT_ID__-module.js';
5
+
6
+ describe('__GRAMMAR__ semantic tokens', () => {
7
+ it.each([false, true])('colours declarations and references with lexical highlighting %s', async lexical => {
8
+ const { __GRAMMAR__ } = create__NAME__Services({}, { highlightKeywords: lexical, highlightComments: lexical });
9
+ const document = await parseHelper(__GRAMMAR__)('// comment\nnode first -> second\nnode second', {
10
+ documentUri: 'file:///tokens.__EXTENSION__',
11
+ validation: true
12
+ });
13
+ expect(document.diagnostics).toEqual([]);
14
+ const provider = __GRAMMAR__.lsp.SemanticTokenProvider!;
15
+ const tokens = await provider.semanticHighlight(document, { textDocument: { uri: document.uri.toString() } });
16
+ const decoded = SemanticTokensDecoder.decode(tokens, provider.tokenTypes, document).map(token => ({
17
+ text: token.text,
18
+ type: token.tokenType
19
+ }));
20
+ expect(decoded).toEqual([
21
+ ...(lexical
22
+ ? [
23
+ { text: '// comment', type: 'comment' },
24
+ { text: 'node', type: 'keyword' }
25
+ ]
26
+ : []),
27
+ { text: 'first', type: 'class' },
28
+ ...(lexical ? [{ text: '->', type: 'keyword' }] : []),
29
+ { text: 'second', type: 'class' },
30
+ ...(lexical ? [{ text: 'node', type: 'keyword' }] : []),
31
+ { text: 'second', type: 'class' }
32
+ ]);
33
+ });
34
+ });