@hydranium/cli 1.0.0-next.10
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.
- package/LICENSE +21 -0
- package/README.md +240 -0
- package/heap-analysis/analyze-heap.mjs +798 -0
- package/heap-analysis/heap/core/aggregate.mjs +152 -0
- package/heap-analysis/heap/core/bucket-label.mjs +23 -0
- package/heap-analysis/heap/core/cutpoints.mjs +51 -0
- package/heap-analysis/heap/core/diff.mjs +56 -0
- package/heap-analysis/heap/core/format.mjs +47 -0
- package/heap-analysis/heap/core/holders.mjs +80 -0
- package/heap-analysis/heap/core/load.mjs +46 -0
- package/heap-analysis/heap/core/report.mjs +68 -0
- package/heap-analysis/heap/core/strings.mjs +80 -0
- package/heap-analysis/heap/langium/concepts.mjs +230 -0
- package/heap-analysis/heap/langium/glsp.classifier.mjs +80 -0
- package/heap-analysis/heap/langium/langium.classifier.mjs +203 -0
- package/heap-analysis/heap/renderer/renderer.mjs +122 -0
- package/lib/cli-version.d.ts +21 -0
- package/lib/cli-version.d.ts.map +1 -0
- package/lib/cli-version.js +30 -0
- package/lib/cli-version.js.map +1 -0
- package/lib/cli.d.ts +11 -0
- package/lib/cli.d.ts.map +1 -0
- package/lib/cli.js +135 -0
- package/lib/cli.js.map +1 -0
- package/lib/commands/analyze-heap-args.d.ts +46 -0
- package/lib/commands/analyze-heap-args.d.ts.map +1 -0
- package/lib/commands/analyze-heap-args.js +174 -0
- package/lib/commands/analyze-heap-args.js.map +1 -0
- package/lib/commands/analyze-heap.d.ts +37 -0
- package/lib/commands/analyze-heap.d.ts.map +1 -0
- package/lib/commands/analyze-heap.js +63 -0
- package/lib/commands/analyze-heap.js.map +1 -0
- package/lib/commands/ast-ground-truth-args.d.ts +23 -0
- package/lib/commands/ast-ground-truth-args.d.ts.map +1 -0
- package/lib/commands/ast-ground-truth-args.js +47 -0
- package/lib/commands/ast-ground-truth-args.js.map +1 -0
- package/lib/commands/ast-ground-truth-driver.d.ts +10 -0
- package/lib/commands/ast-ground-truth-driver.d.ts.map +1 -0
- package/lib/commands/ast-ground-truth-driver.js +72 -0
- package/lib/commands/ast-ground-truth-driver.js.map +1 -0
- package/lib/commands/ast-ground-truth.d.ts +31 -0
- package/lib/commands/ast-ground-truth.d.ts.map +1 -0
- package/lib/commands/ast-ground-truth.js +36 -0
- package/lib/commands/ast-ground-truth.js.map +1 -0
- package/lib/commands/generate-transfer-model-args.d.ts +38 -0
- package/lib/commands/generate-transfer-model-args.d.ts.map +1 -0
- package/lib/commands/generate-transfer-model-args.js +183 -0
- package/lib/commands/generate-transfer-model-args.js.map +1 -0
- package/lib/commands/generate-transfer-model-config.d.ts +32 -0
- package/lib/commands/generate-transfer-model-config.d.ts.map +1 -0
- package/lib/commands/generate-transfer-model-config.js +122 -0
- package/lib/commands/generate-transfer-model-config.js.map +1 -0
- package/lib/commands/generate-transfer-model.d.ts +110 -0
- package/lib/commands/generate-transfer-model.d.ts.map +1 -0
- package/lib/commands/generate-transfer-model.js +587 -0
- package/lib/commands/generate-transfer-model.js.map +1 -0
- package/lib/commands/harness-args.d.ts +198 -0
- package/lib/commands/harness-args.d.ts.map +1 -0
- package/lib/commands/harness-args.js +339 -0
- package/lib/commands/harness-args.js.map +1 -0
- package/lib/commands/headless-harness.d.ts +97 -0
- package/lib/commands/headless-harness.d.ts.map +1 -0
- package/lib/commands/headless-harness.js +121 -0
- package/lib/commands/headless-harness.js.map +1 -0
- package/lib/commands/init-args.d.ts +81 -0
- package/lib/commands/init-args.d.ts.map +1 -0
- package/lib/commands/init-args.js +250 -0
- package/lib/commands/init-args.js.map +1 -0
- package/lib/commands/init-prompt.d.ts +66 -0
- package/lib/commands/init-prompt.d.ts.map +1 -0
- package/lib/commands/init-prompt.js +71 -0
- package/lib/commands/init-prompt.js.map +1 -0
- package/lib/commands/init-templates.d.ts +42 -0
- package/lib/commands/init-templates.d.ts.map +1 -0
- package/lib/commands/init-templates.js +1902 -0
- package/lib/commands/init-templates.js.map +1 -0
- package/lib/commands/init-wizard.d.ts +65 -0
- package/lib/commands/init-wizard.d.ts.map +1 -0
- package/lib/commands/init-wizard.js +302 -0
- package/lib/commands/init-wizard.js.map +1 -0
- package/lib/commands/init-workspace.d.ts +168 -0
- package/lib/commands/init-workspace.d.ts.map +1 -0
- package/lib/commands/init-workspace.js +372 -0
- package/lib/commands/init-workspace.js.map +1 -0
- package/lib/commands/init.d.ts +273 -0
- package/lib/commands/init.d.ts.map +1 -0
- package/lib/commands/init.js +303 -0
- package/lib/commands/init.js.map +1 -0
- package/lib/commands/lint-grammar-args.d.ts +23 -0
- package/lib/commands/lint-grammar-args.d.ts.map +1 -0
- package/lib/commands/lint-grammar-args.js +63 -0
- package/lib/commands/lint-grammar-args.js.map +1 -0
- package/lib/commands/lint-grammar-driver.d.ts +10 -0
- package/lib/commands/lint-grammar-driver.d.ts.map +1 -0
- package/lib/commands/lint-grammar-driver.js +72 -0
- package/lib/commands/lint-grammar-driver.js.map +1 -0
- package/lib/commands/lint-grammar-report.d.ts +24 -0
- package/lib/commands/lint-grammar-report.d.ts.map +1 -0
- package/lib/commands/lint-grammar-report.js +65 -0
- package/lib/commands/lint-grammar-report.js.map +1 -0
- package/lib/commands/lint-grammar.d.ts +33 -0
- package/lib/commands/lint-grammar.d.ts.map +1 -0
- package/lib/commands/lint-grammar.js +42 -0
- package/lib/commands/lint-grammar.js.map +1 -0
- package/lib/commands/measure-memory-args.d.ts +23 -0
- package/lib/commands/measure-memory-args.d.ts.map +1 -0
- package/lib/commands/measure-memory-args.js +106 -0
- package/lib/commands/measure-memory-args.js.map +1 -0
- package/lib/commands/measure-memory-driver.d.ts +10 -0
- package/lib/commands/measure-memory-driver.d.ts.map +1 -0
- package/lib/commands/measure-memory-driver.js +110 -0
- package/lib/commands/measure-memory-driver.js.map +1 -0
- package/lib/commands/measure-memory.d.ts +68 -0
- package/lib/commands/measure-memory.d.ts.map +1 -0
- package/lib/commands/measure-memory.js +100 -0
- package/lib/commands/measure-memory.js.map +1 -0
- package/lib/commands/model-docs-args.d.ts +23 -0
- package/lib/commands/model-docs-args.d.ts.map +1 -0
- package/lib/commands/model-docs-args.js +53 -0
- package/lib/commands/model-docs-args.js.map +1 -0
- package/lib/commands/model-docs-driver.d.ts +10 -0
- package/lib/commands/model-docs-driver.d.ts.map +1 -0
- package/lib/commands/model-docs-driver.js +61 -0
- package/lib/commands/model-docs-driver.js.map +1 -0
- package/lib/commands/model-docs-report.d.ts +18 -0
- package/lib/commands/model-docs-report.d.ts.map +1 -0
- package/lib/commands/model-docs-report.js +123 -0
- package/lib/commands/model-docs-report.js.map +1 -0
- package/lib/commands/model-docs.d.ts +30 -0
- package/lib/commands/model-docs.d.ts.map +1 -0
- package/lib/commands/model-docs.js +37 -0
- package/lib/commands/model-docs.js.map +1 -0
- package/lib/commands/projects-args.d.ts +25 -0
- package/lib/commands/projects-args.d.ts.map +1 -0
- package/lib/commands/projects-args.js +49 -0
- package/lib/commands/projects-args.js.map +1 -0
- package/lib/commands/projects.d.ts +45 -0
- package/lib/commands/projects.d.ts.map +1 -0
- package/lib/commands/projects.js +39 -0
- package/lib/commands/projects.js.map +1 -0
- package/lib/commands/query-args.d.ts +25 -0
- package/lib/commands/query-args.d.ts.map +1 -0
- package/lib/commands/query-args.js +46 -0
- package/lib/commands/query-args.js.map +1 -0
- package/lib/commands/query.d.ts +32 -0
- package/lib/commands/query.d.ts.map +1 -0
- package/lib/commands/query.js +29 -0
- package/lib/commands/query.js.map +1 -0
- package/lib/commands/reflect-args.d.ts +23 -0
- package/lib/commands/reflect-args.d.ts.map +1 -0
- package/lib/commands/reflect-args.js +59 -0
- package/lib/commands/reflect-args.js.map +1 -0
- package/lib/commands/reflect-driver.d.ts +10 -0
- package/lib/commands/reflect-driver.d.ts.map +1 -0
- package/lib/commands/reflect-driver.js +66 -0
- package/lib/commands/reflect-driver.js.map +1 -0
- package/lib/commands/reflect-report.d.ts +20 -0
- package/lib/commands/reflect-report.d.ts.map +1 -0
- package/lib/commands/reflect-report.js +127 -0
- package/lib/commands/reflect-report.js.map +1 -0
- package/lib/commands/reflect.d.ts +30 -0
- package/lib/commands/reflect.d.ts.map +1 -0
- package/lib/commands/reflect.js +38 -0
- package/lib/commands/reflect.js.map +1 -0
- package/lib/commands/save-args.d.ts +26 -0
- package/lib/commands/save-args.d.ts.map +1 -0
- package/lib/commands/save-args.js +59 -0
- package/lib/commands/save-args.js.map +1 -0
- package/lib/commands/save.d.ts +38 -0
- package/lib/commands/save.d.ts.map +1 -0
- package/lib/commands/save.js +48 -0
- package/lib/commands/save.js.map +1 -0
- package/lib/commands/validate-args.d.ts +23 -0
- package/lib/commands/validate-args.d.ts.map +1 -0
- package/lib/commands/validate-args.js +60 -0
- package/lib/commands/validate-args.js.map +1 -0
- package/lib/commands/validate-driver.d.ts +10 -0
- package/lib/commands/validate-driver.d.ts.map +1 -0
- package/lib/commands/validate-driver.js +72 -0
- package/lib/commands/validate-driver.js.map +1 -0
- package/lib/commands/validate-report.d.ts +30 -0
- package/lib/commands/validate-report.d.ts.map +1 -0
- package/lib/commands/validate-report.js +87 -0
- package/lib/commands/validate-report.js.map +1 -0
- package/lib/commands/validate.d.ts +34 -0
- package/lib/commands/validate.d.ts.map +1 -0
- package/lib/commands/validate.js +41 -0
- package/lib/commands/validate.js.map +1 -0
- package/lib/commands/watch-args.d.ts +26 -0
- package/lib/commands/watch-args.d.ts.map +1 -0
- package/lib/commands/watch-args.js +59 -0
- package/lib/commands/watch-args.js.map +1 -0
- package/lib/commands/watch.d.ts +75 -0
- package/lib/commands/watch.d.ts.map +1 -0
- package/lib/commands/watch.js +112 -0
- package/lib/commands/watch.js.map +1 -0
- package/lib/index.d.ts +16 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +16 -0
- package/lib/index.js.map +1 -0
- package/lib/log-level.d.ts +39 -0
- package/lib/log-level.d.ts.map +1 -0
- package/lib/log-level.js +47 -0
- package/lib/log-level.js.map +1 -0
- package/lib/package-name.d.ts +33 -0
- package/lib/package-name.d.ts.map +1 -0
- package/lib/package-name.js +37 -0
- package/lib/package-name.js.map +1 -0
- package/lib/spawn-data-server.d.ts +102 -0
- package/lib/spawn-data-server.d.ts.map +1 -0
- package/lib/spawn-data-server.js +154 -0
- package/lib/spawn-data-server.js.map +1 -0
- package/lib/testing/echo-server.d.ts +11 -0
- package/lib/testing/echo-server.d.ts.map +1 -0
- package/lib/testing/echo-server.js +79 -0
- package/lib/testing/echo-server.js.map +1 -0
- package/package.json +105 -0
- package/src/cli-version.ts +32 -0
- package/src/cli.ts +139 -0
- package/src/commands/analyze-heap-args.ts +184 -0
- package/src/commands/analyze-heap.ts +76 -0
- package/src/commands/ast-ground-truth-args.ts +63 -0
- package/src/commands/ast-ground-truth-driver.ts +81 -0
- package/src/commands/ast-ground-truth.ts +49 -0
- package/src/commands/generate-transfer-model-args.ts +203 -0
- package/src/commands/generate-transfer-model-config.ts +128 -0
- package/src/commands/generate-transfer-model.ts +716 -0
- package/src/commands/harness-args.ts +429 -0
- package/src/commands/headless-harness.ts +165 -0
- package/src/commands/init-args.ts +273 -0
- package/src/commands/init-prompt.ts +149 -0
- package/src/commands/init-templates.ts +2037 -0
- package/src/commands/init-wizard.ts +378 -0
- package/src/commands/init-workspace.ts +442 -0
- package/src/commands/init.ts +554 -0
- package/src/commands/lint-grammar-args.ts +79 -0
- package/src/commands/lint-grammar-driver.ts +83 -0
- package/src/commands/lint-grammar-report.ts +74 -0
- package/src/commands/lint-grammar.ts +57 -0
- package/src/commands/measure-memory-args.ts +123 -0
- package/src/commands/measure-memory-driver.ts +128 -0
- package/src/commands/measure-memory.ts +143 -0
- package/src/commands/model-docs-args.ts +69 -0
- package/src/commands/model-docs-driver.ts +68 -0
- package/src/commands/model-docs-report.ts +133 -0
- package/src/commands/model-docs.ts +48 -0
- package/src/commands/projects-args.ts +62 -0
- package/src/commands/projects.ts +76 -0
- package/src/commands/query-args.ts +61 -0
- package/src/commands/query.ts +58 -0
- package/src/commands/reflect-args.ts +75 -0
- package/src/commands/reflect-driver.ts +76 -0
- package/src/commands/reflect-report.ts +138 -0
- package/src/commands/reflect.ts +51 -0
- package/src/commands/save-args.ts +74 -0
- package/src/commands/save.ts +84 -0
- package/src/commands/validate-args.ts +76 -0
- package/src/commands/validate-driver.ts +84 -0
- package/src/commands/validate-report.ts +95 -0
- package/src/commands/validate.ts +58 -0
- package/src/commands/watch-args.ts +74 -0
- package/src/commands/watch.ts +185 -0
- package/src/index.ts +16 -0
- package/src/log-level.ts +49 -0
- package/src/package-name.ts +39 -0
- package/src/spawn-data-server.ts +247 -0
- package/src/testing/echo-server.ts +93 -0
|
@@ -0,0 +1,2037 @@
|
|
|
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
|
+
/*
|
|
11
|
+
* Template source for the `init` scaffolding command. Every entry is a file the
|
|
12
|
+
* scaffold emits, rendered from an `InitComposition` — one project tier plus N
|
|
13
|
+
* grammar tiers.
|
|
14
|
+
*
|
|
15
|
+
* PROJECT tokens, substituted once:
|
|
16
|
+
*
|
|
17
|
+
* - `__NAME__` — PascalCase project name, the Langium `projectName`. Drives the
|
|
18
|
+
* SHARED generated symbols (`<name>AstReflection`,
|
|
19
|
+
* `<name>GeneratedSharedModule`), one set per project.
|
|
20
|
+
* - `__PROJECT_ID__` — kebab project id, always from `__NAME__`. For
|
|
21
|
+
* project-level names a second grammar must not change: the package name and
|
|
22
|
+
* `bin` key, the DI module filename, and the data-server port command.
|
|
23
|
+
* - `__CONFIG_ROOT_CONST__` — SCREAMING_SNAKE project id, naming the
|
|
24
|
+
* `lsp.configurationRoot` constant a multi-grammar project binds.
|
|
25
|
+
*
|
|
26
|
+
* GRAMMAR tokens, substituted once per grammar:
|
|
27
|
+
*
|
|
28
|
+
* - `__GRAMMAR__` — PascalCase grammar name, the `grammar X` declaration.
|
|
29
|
+
* Drives the PER-LANGUAGE generated symbols (`<grammar>GeneratedModule`,
|
|
30
|
+
* `<grammar>LanguageMetaData`).
|
|
31
|
+
* - `__GRAMMAR_ID__` — kebab grammar id: the `.langium` filename.
|
|
32
|
+
* - `__LANGUAGE_ID__` — kebab routing key: the `langium-config` entry id and
|
|
33
|
+
* its TextMate output.
|
|
34
|
+
* - `__EXTENSION__` — the grammar's FIRST file extension, without the dot.
|
|
35
|
+
* - `__ENTRY_RULE__` / `__NODE_RULE__` — `<Grammar>Model` / `<Grammar>Node`.
|
|
36
|
+
*
|
|
37
|
+
* **The project / grammar / language split is the point, not ceremony.** All
|
|
38
|
+
* three ids hold the same value in the default single-grammar scaffold, so a
|
|
39
|
+
* template that reaches for the wrong one still renders correctly — and then
|
|
40
|
+
* breaks the day a second grammar arrives, renaming the package or colliding on
|
|
41
|
+
* a file extension. Each token above names the tier it belongs to so that day
|
|
42
|
+
* is a no-op.
|
|
43
|
+
*
|
|
44
|
+
* **Rule names are per-grammar and that is load-bearing.** One `langium-cli`
|
|
45
|
+
* run over N grammars emits ONE combined `ast.ts` sharing one reflection, so
|
|
46
|
+
* two grammars declaring `entry Model:` would put the same interface name in it
|
|
47
|
+
* twice. Hence `__ENTRY_RULE__` rather than a fixed `Model`. `__NODE_RULE__` is
|
|
48
|
+
* `<Grammar>Node` and deliberately not `<Grammar>Element`: the transfer-model
|
|
49
|
+
* generator emits `interface <projectName>Element` as the base type every rule
|
|
50
|
+
* extends, which a rule of that name would collide with.
|
|
51
|
+
*
|
|
52
|
+
* Kept as in-source strings (not on-disk assets) so no build-time copy step
|
|
53
|
+
* is needed and the substitution is unit-testable. The `.ts` / `.langium` / JSON
|
|
54
|
+
* templates are derived from the in-repo reference example so the generated project
|
|
55
|
+
* compiles against the real `@hydranium/*` API once `langium generate` has run.
|
|
56
|
+
*
|
|
57
|
+
* Two conventions the templates follow deliberately:
|
|
58
|
+
*
|
|
59
|
+
* - **File-purpose prose is a `//` run, never a leading `/** … *\/` block.** A
|
|
60
|
+
* license-header tool typically REPLACES the leading block comment, so prose in
|
|
61
|
+
* that position is silently deleted the first time the adopter runs theirs. A
|
|
62
|
+
* line-comment run survives, because such tools prepend rather than replace when
|
|
63
|
+
* the file does not open with `/*`.
|
|
64
|
+
* - **Wrapped at the target repo's column budget, not at a fixed one.** No single
|
|
65
|
+
* width is stable for an unknown repo — prettier's own default is 80, and 100,
|
|
66
|
+
* 120 and 140 all wrap differently — so "passes a formatter check as-is" is only
|
|
67
|
+
* ever true relative to a config. A workspace scaffold reads the surrounding
|
|
68
|
+
* repo's `printWidth`; a standalone one falls back to {@link DEFAULT_COLUMNS}.
|
|
69
|
+
* Every wrap goes through {@link importList} / {@link arrayLiteral} rather than
|
|
70
|
+
* being written out by hand, because a hand-wrapped line ignores the budget and
|
|
71
|
+
* is what the detection cannot fix. A very long `--name` can still push a line
|
|
72
|
+
* over; the adopter's own formatter settles it.
|
|
73
|
+
*/
|
|
74
|
+
|
|
75
|
+
import { readCliVersion } from '../cli-version.js';
|
|
76
|
+
import type { InitComposition, InitFile, InitGrammarNames, InitHead } from './init.js';
|
|
77
|
+
import type { JsonValue } from './init-workspace.js';
|
|
78
|
+
|
|
79
|
+
/** One template file: its path (tokens allowed) and its content (tokens allowed). */
|
|
80
|
+
export interface InitTemplate {
|
|
81
|
+
path: string;
|
|
82
|
+
content: string;
|
|
83
|
+
}
|
|
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
|
+
/**
|
|
96
|
+
* The version `init` pins every `@hydranium/*` dependency at: the scaffolding
|
|
97
|
+
* CLI's own.
|
|
98
|
+
*
|
|
99
|
+
* Read rather than written down. The changesets config declares the scope
|
|
100
|
+
* `fixed`, so the CLI's version IS the framework's, and a literal here would be
|
|
101
|
+
* a second source of truth that a scaffold can pin from before anyone gets
|
|
102
|
+
* round to updating it.
|
|
103
|
+
*/
|
|
104
|
+
export function readFrameworkVersion(): string {
|
|
105
|
+
return readCliVersion();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The range a scaffolded project pins a framework package at.
|
|
110
|
+
*
|
|
111
|
+
* Caret rather than exact: the scope's fixed versioning means a caret set can
|
|
112
|
+
* only resolve to one version line, so the two-physical-copies hazard that
|
|
113
|
+
* argues for exact pins cannot arise, while exact pins would deny the adopter a
|
|
114
|
+
* patch release.
|
|
115
|
+
*/
|
|
116
|
+
function frameworkPin(composition: InitComposition): string {
|
|
117
|
+
return `^${composition.frameworkVersion}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** SCREAMING_SNAKE a kebab id. */
|
|
121
|
+
function screamingSnake(projectId: string): string {
|
|
122
|
+
return projectId.replace(/-/g, '_').toUpperCase();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Substitute the project-tier tokens. */
|
|
126
|
+
function project(text: string, composition: InitComposition): string {
|
|
127
|
+
return text
|
|
128
|
+
.replace(/__NAME__/g, composition.name)
|
|
129
|
+
.replace(/__PROJECT_ID__/g, composition.projectId)
|
|
130
|
+
.replace(/__CONFIG_ROOT_CONST__/g, screamingSnake(composition.projectId));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Substitute the grammar-tier tokens for ONE grammar.
|
|
135
|
+
*
|
|
136
|
+
* `__GRAMMAR__` must not be folded into `__NAME__`, nor `__GRAMMAR_ID__` into
|
|
137
|
+
* `__PROJECT_ID__`: each pair holds the same value in the single-grammar
|
|
138
|
+
* scaffold and diverges the moment a second grammar arrives, which is precisely
|
|
139
|
+
* the case the separate tokens exist to keep correct.
|
|
140
|
+
*/
|
|
141
|
+
function grammarTier(text: string, grammar: InitGrammarNames): string {
|
|
142
|
+
return text
|
|
143
|
+
.replace(/__GRAMMAR_ID__/g, grammar.grammarId)
|
|
144
|
+
.replace(/__GRAMMAR__/g, grammar.grammar)
|
|
145
|
+
.replace(/__LANGUAGE_ID__/g, grammar.languageId)
|
|
146
|
+
.replace(/__EXTENSION__/g, grammar.extensions[0])
|
|
147
|
+
.replace(/__ENTRY_RULE__/g, grammar.entryRule)
|
|
148
|
+
.replace(/__NODE_RULE__/g, grammar.nodeRule);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Render both tiers for a single-grammar file. */
|
|
152
|
+
function render(text: string, composition: InitComposition, grammar: InitGrammarNames): string {
|
|
153
|
+
return grammarTier(project(text, composition), grammar);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** A comma-separated list of single-quoted TypeScript/JS string literals. */
|
|
157
|
+
function quotedList(values: readonly string[]): string {
|
|
158
|
+
return values.map(value => `'${value}'`).join(', ');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The column budget the emitted sources wrap at when nothing better is known.
|
|
163
|
+
*
|
|
164
|
+
* **Not "prettier's default"** — that is 80. 120 is a choice, and stating it as
|
|
165
|
+
* a default was wrong in a way that mattered: no single width is stable for an
|
|
166
|
+
* unknown repo, so emitted code only passes a formatter check relative to a
|
|
167
|
+
* config. A workspace scaffold detects the surrounding repo's `printWidth` and
|
|
168
|
+
* uses it (see {@link columnsFor}); a standalone one has no config to read and
|
|
169
|
+
* falls back here.
|
|
170
|
+
*/
|
|
171
|
+
const DEFAULT_COLUMNS = 120;
|
|
172
|
+
|
|
173
|
+
/** The width THIS composition wraps at: the detected workspace one, else {@link DEFAULT_COLUMNS}. */
|
|
174
|
+
function columnsFor(composition: InitComposition): number {
|
|
175
|
+
return composition.packaging.workspace?.printWidth ?? DEFAULT_COLUMNS;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* 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
|
|
181
|
+
* generated-module import grows by one symbol per grammar.
|
|
182
|
+
*/
|
|
183
|
+
function importList(symbols: readonly string[], from: string, columns: number, typeOnly = false): string {
|
|
184
|
+
const keyword = typeOnly ? 'import type' : 'import';
|
|
185
|
+
const single = `${keyword} { ${symbols.join(', ')} } from '${from}';`;
|
|
186
|
+
return single.length <= columns ? single : `${keyword} {\n${symbols.map(symbol => ` ${symbol}`).join(',\n')}\n} from '${from}';`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** An array literal on one line while it fits at `indent`, else one element per line. */
|
|
190
|
+
function arrayLiteral(elements: readonly string[], indent: string, columns: number): string {
|
|
191
|
+
const single = `[${elements.join(', ')}]`;
|
|
192
|
+
return `${indent}${single}`.length <= columns
|
|
193
|
+
? single
|
|
194
|
+
: `[\n${elements.map(element => `${indent} ${element}`).join(',\n')}\n${indent}]`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* The FRAMEWORK packages each head adds, carrying no version: every
|
|
199
|
+
* `@hydranium/*` pin is derived at scaffold time from {@link frameworkPin}, so
|
|
200
|
+
* none of them can name a version its siblings were not published at.
|
|
201
|
+
*/
|
|
202
|
+
const HEAD_FRAMEWORK_DEPENDENCIES: Record<InitHead, readonly string[]> = {
|
|
203
|
+
lsp: ['@hydranium/core', '@hydranium/langium', '@hydranium/protocol'],
|
|
204
|
+
data: ['@hydranium/data-server'],
|
|
205
|
+
glsp: ['@hydranium/glsp-server']
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* The third-party runtime dependencies each head adds, read off a working
|
|
210
|
+
* project rather than guessed.
|
|
211
|
+
*
|
|
212
|
+
* **This is why the head axis exists.** Keyed per head rather than emitted as
|
|
213
|
+
* one fixed list, because otherwise an adopter who adds a head hand-maintains
|
|
214
|
+
* its dependencies too, and the two drift apart until a lint rule catches the
|
|
215
|
+
* undeclared packages.
|
|
216
|
+
*
|
|
217
|
+
* **These literals are the only hand-maintained versions the scaffold emits, and
|
|
218
|
+
* a bump of the framework's own pinned chain does not touch them.** A scaffolded
|
|
219
|
+
* project pinning a different `langium` than the framework was built against
|
|
220
|
+
* resolves a second physical copy, which is the identity failure the repo's
|
|
221
|
+
* `overrides` block exists to prevent — so a repo gate holds each entry here
|
|
222
|
+
* against the manifest that declares it.
|
|
223
|
+
*
|
|
224
|
+
* `langium` is required by `lsp` even though hand-written code goes through the
|
|
225
|
+
* `@hydranium/langium` chokepoint, because `langium-cli` emits direct imports
|
|
226
|
+
* into the generated files and regenerates them on every build.
|
|
227
|
+
*/
|
|
228
|
+
const HEAD_THIRD_PARTY_DEPENDENCIES: Record<InitHead, Readonly<Record<string, string>>> = {
|
|
229
|
+
lsp: {
|
|
230
|
+
langium: '4.3.1',
|
|
231
|
+
'vscode-languageserver': '~10.0.1'
|
|
232
|
+
},
|
|
233
|
+
data: {},
|
|
234
|
+
glsp: {
|
|
235
|
+
'@eclipse-glsp/graph': '2.7.0',
|
|
236
|
+
'@eclipse-glsp/server': '2.7.0',
|
|
237
|
+
// `^6.1.3` and NOT `^6.0.0`: `@eclipse-glsp/server@2.7.0` requires it, and
|
|
238
|
+
// `^6.0.0` resolved only because the hoisted copy happened to satisfy it.
|
|
239
|
+
inversify: '^6.1.3',
|
|
240
|
+
'reflect-metadata': '~0.2.2'
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
/** The dependency block for a head set, merged and sorted as npm writes it. */
|
|
245
|
+
function dependencyBlock(composition: InitComposition, indent: string): string {
|
|
246
|
+
const merged: Record<string, string> = {};
|
|
247
|
+
for (const head of composition.heads) {
|
|
248
|
+
Object.assign(merged, HEAD_THIRD_PARTY_DEPENDENCIES[head]);
|
|
249
|
+
for (const framework of HEAD_FRAMEWORK_DEPENDENCIES[head]) {
|
|
250
|
+
merged[framework] = frameworkPin(composition);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return Object.keys(merged)
|
|
254
|
+
.sort()
|
|
255
|
+
.map(name => `${indent}"${name}": "${merged[name]}"`)
|
|
256
|
+
.join(',\n');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// JSON at two-space indent, not the three the `.ts` templates use: that is the
|
|
260
|
+
// npm convention, and `npm install` rewrites `package.json` at two spaces
|
|
261
|
+
// regardless — so three would not survive the adopter's first install.
|
|
262
|
+
//
|
|
263
|
+
// The KEY ORDER is `prettier-plugin-packagejson`'s canonical one, `private`
|
|
264
|
+
// before `license` included. Any other order is rewritten by the scaffolded
|
|
265
|
+
// project's own first `format` run — a diff on a file the adopter never touched
|
|
266
|
+
// — and it makes a byte-compare against a formatted copy of this emission
|
|
267
|
+
// unsatisfiable, since both tools own the file.
|
|
268
|
+
//
|
|
269
|
+
// `private` and `license` are ONE decision, not two independent keys: a manifest
|
|
270
|
+
// declaring UNLICENSED grants no rights, so leaving it publishable to a public
|
|
271
|
+
// registry asserts the opposite of what it grants. The scaffold cannot choose a
|
|
272
|
+
// licence for a stranger's project, so it withholds publication instead, and
|
|
273
|
+
// `--public` is the opt-out once the project has chosen one.
|
|
274
|
+
//
|
|
275
|
+
// `files` is what keeps the first `npm publish` from succeeding with an unusable
|
|
276
|
+
// tarball. With no `files` and no `.npmignore`, npm falls back to `.gitignore`,
|
|
277
|
+
// which this scaffold also emits with `lib/` in it: npm force-includes `main`
|
|
278
|
+
// and OMITS the `bin` target beside it, so the package installs and the binary
|
|
279
|
+
// is missing. `syntaxes` is listed because the TextMate grammar is generated
|
|
280
|
+
// from this package's own grammar and can reach a consumer from nowhere else; a
|
|
281
|
+
// listed path that does not exist yet is inert.
|
|
282
|
+
//
|
|
283
|
+
// `repository` is deliberately NOT emitted while `author` is emitted empty. An
|
|
284
|
+
// empty `author` is the form `npm init` itself writes and asserts nothing, but
|
|
285
|
+
// `repository` is CONSUMED — the registry page, `npm repo` and publish
|
|
286
|
+
// provenance all follow it — so an empty or invented value sends a reader
|
|
287
|
+
// somewhere wrong rather than nowhere. A scaffold has no way to learn the real
|
|
288
|
+
// one: a standalone target has no surrounding repository at all.
|
|
289
|
+
const PACKAGE_JSON = `{
|
|
290
|
+
"name": "__PACKAGE_NAME__",
|
|
291
|
+
"version": "0.0.0",
|
|
292
|
+
__PRIVATE__ "description": "__NAME__ language server, built with the Hydranium framework.",
|
|
293
|
+
"keywords": [
|
|
294
|
+
"hydranium",
|
|
295
|
+
"langium",
|
|
296
|
+
"language-server",
|
|
297
|
+
"__PROJECT_ID__"
|
|
298
|
+
],
|
|
299
|
+
"license": "UNLICENSED",
|
|
300
|
+
"author": "",
|
|
301
|
+
"type": "module",
|
|
302
|
+
"main": "lib/index.js",
|
|
303
|
+
"types": "lib/index.d.ts",
|
|
304
|
+
"bin": {
|
|
305
|
+
__BIN__ },
|
|
306
|
+
"files": [
|
|
307
|
+
"lib",
|
|
308
|
+
"src",
|
|
309
|
+
"syntaxes"
|
|
310
|
+
],
|
|
311
|
+
"scripts": {
|
|
312
|
+
"build": "npm run generate && tsc",
|
|
313
|
+
"clean": "rimraf lib syntaxes src/language-server/generated src/language-server/generated-transfer tsconfig.tsbuildinfo",
|
|
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\\"",
|
|
316
|
+
"langium:generate": "langium generate",
|
|
317
|
+
"langium:watch": "langium generate --watch",
|
|
318
|
+
__LINT__ "start": "node lib/main.js --stdio",
|
|
319
|
+
"test": "npm run typecheck:test && vitest run",
|
|
320
|
+
"typecheck:test": "tsc --noEmit -p tsconfig.test.json",
|
|
321
|
+
"watch": "tsc --watch"
|
|
322
|
+
},
|
|
323
|
+
"dependencies": {
|
|
324
|
+
__DEPENDENCIES__
|
|
325
|
+
},
|
|
326
|
+
"devDependencies": {
|
|
327
|
+
"@hydranium/cli": "__FRAMEWORK_PIN__",
|
|
328
|
+
"@types/node": "^22.0.0",
|
|
329
|
+
"langium-cli": "4.3.0",
|
|
330
|
+
"rimraf": "^5.0.0",
|
|
331
|
+
"typescript": "^5.8.0",
|
|
332
|
+
"vitest": "^4.0.0"
|
|
333
|
+
},
|
|
334
|
+
"engines": {
|
|
335
|
+
"node": ">=22.13"
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
`;
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* How the scaffold names `npm run` when talking about itself.
|
|
342
|
+
*
|
|
343
|
+
* A workspace member's scripts are routinely invoked from the repo root, where
|
|
344
|
+
* a bare `npm run` reaches the ROOT manifest and fails — so the regen hint a
|
|
345
|
+
* generated file prints has to name the package, or it sends every reader who
|
|
346
|
+
* follows it to the wrong place.
|
|
347
|
+
*/
|
|
348
|
+
function npmRun(composition: InitComposition): string {
|
|
349
|
+
const workspace = composition.packaging.workspace;
|
|
350
|
+
return workspace === undefined ? 'npm run' : `npm --prefix ${workspace.targetPath} run`;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* The `bin` targets, one per executable entry the head set emits.
|
|
355
|
+
*
|
|
356
|
+
* The data entry earns a key of its own rather than an argument on the first
|
|
357
|
+
* one: the two entries put DIFFERENT protocols on stdio, so a process can host
|
|
358
|
+
* only one of them, and `--server` takes a command line rather than a package
|
|
359
|
+
* name. Without the second key a scaffolded project has no command line that
|
|
360
|
+
* reaches its own data head at all.
|
|
361
|
+
*
|
|
362
|
+
* Alphabetical, because `prettier-plugin-packagejson` sorts `bin` and any other
|
|
363
|
+
* order is rewritten by the scaffolded project's first `format` run.
|
|
364
|
+
*/
|
|
365
|
+
function binBlock(composition: InitComposition): string {
|
|
366
|
+
const entries: Array<[string, string]> = [[composition.projectId, 'lib/main.js']];
|
|
367
|
+
if (composition.heads.includes('data')) {
|
|
368
|
+
entries.push([`${composition.projectId}-data-server`, 'lib/data-server-main.js']);
|
|
369
|
+
}
|
|
370
|
+
return entries.map(([name, target]) => ` "${name}": "${target}"`).join(',\n') + '\n';
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** `package.json` with the name, the private flag and the dependency block all derived. */
|
|
374
|
+
function packageJson(composition: InitComposition): string {
|
|
375
|
+
const { scope } = composition.packaging;
|
|
376
|
+
const packageName = scope === undefined ? composition.projectId : `${scope}/${composition.projectId}`;
|
|
377
|
+
// Emitted only where a root eslint config says the repo lints. The absence is
|
|
378
|
+
// the dangerous direction: a task runner runs a script only where one is
|
|
379
|
+
// declared, so a package with no `lint` is SKIPPED rather than reported, and
|
|
380
|
+
// 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';
|
|
382
|
+
return project(PACKAGE_JSON, composition)
|
|
383
|
+
.replace('__PACKAGE_NAME__', packageName)
|
|
384
|
+
.replace('__BIN__', binBlock(composition))
|
|
385
|
+
.replace('__LINT__', lint)
|
|
386
|
+
.replace('__PRIVATE__', composition.packaging.private ? ' "private": true,\n' : '')
|
|
387
|
+
.replace('__NPM_RUN__', npmRun(composition))
|
|
388
|
+
.replace('__FRAMEWORK_PIN__', frameworkPin(composition))
|
|
389
|
+
.replace('__DEPENDENCIES__', dependencyBlock(composition, ' '));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* `projectName` is the PROJECT name, not any grammar's: one `langium-cli` run
|
|
394
|
+
* over N grammars emits ONE `<projectName>AstReflection` covering all of them.
|
|
395
|
+
* A second grammar is a new entry in `languages` here, never a second config.
|
|
396
|
+
* `textMate.out` gives a VS Code extension its syntax highlighting; drop the
|
|
397
|
+
* block if you are not shipping one.
|
|
398
|
+
*/
|
|
399
|
+
function langiumConfig(composition: InitComposition): string {
|
|
400
|
+
const languages = composition.grammars
|
|
401
|
+
.map(grammar =>
|
|
402
|
+
[
|
|
403
|
+
' {',
|
|
404
|
+
` "id": "${grammar.languageId}",`,
|
|
405
|
+
` "grammar": "src/grammar/${grammar.grammarId}.langium",`,
|
|
406
|
+
` "fileExtensions": [${grammar.extensions.map(extension => `".${extension}"`).join(', ')}],`,
|
|
407
|
+
' "textMate": {',
|
|
408
|
+
` "out": "syntaxes/${grammar.languageId}.tmLanguage.json"`,
|
|
409
|
+
' }',
|
|
410
|
+
' }'
|
|
411
|
+
].join('\n')
|
|
412
|
+
)
|
|
413
|
+
.join(',\n');
|
|
414
|
+
return `{
|
|
415
|
+
"projectName": "${composition.name}",
|
|
416
|
+
"languages": [
|
|
417
|
+
${languages}
|
|
418
|
+
],
|
|
419
|
+
"out": "src/language-server/generated"
|
|
420
|
+
}
|
|
421
|
+
`;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* The compiler options the scaffold needs, as ordered data rather than as a
|
|
426
|
+
* string literal.
|
|
427
|
+
*
|
|
428
|
+
* Structured because a workspace member emits a SUBSET of them — whatever its
|
|
429
|
+
* base config does not already supply — and a second hand-maintained literal
|
|
430
|
+
* for that case would drift from this one silently, which is the failure mode
|
|
431
|
+
* a tsconfig is worst at reporting.
|
|
432
|
+
*/
|
|
433
|
+
const TSCONFIG_COMPILER_OPTIONS: ReadonlyArray<readonly [string, JsonValue]> = [
|
|
434
|
+
['target', 'ES2022'],
|
|
435
|
+
['lib', ['ES2022']],
|
|
436
|
+
['module', 'NodeNext'],
|
|
437
|
+
['moduleResolution', 'NodeNext'],
|
|
438
|
+
['rootDir', 'src'],
|
|
439
|
+
['outDir', 'lib'],
|
|
440
|
+
['strict', true],
|
|
441
|
+
['esModuleInterop', true],
|
|
442
|
+
['skipLibCheck', true],
|
|
443
|
+
['declaration', true],
|
|
444
|
+
['experimentalDecorators', true],
|
|
445
|
+
['emitDecoratorMetadata', true],
|
|
446
|
+
['forceConsistentCasingInFileNames', true],
|
|
447
|
+
['types', ['node']]
|
|
448
|
+
];
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* The options that describe where THIS package's own files live. Never pruned,
|
|
452
|
+
* and emitted first in the workspace case: a base config that supplied them
|
|
453
|
+
* would be pointing every member at one directory, so a match there means the
|
|
454
|
+
* base is wrong rather than that the member is redundant.
|
|
455
|
+
*/
|
|
456
|
+
const PACKAGE_LOCAL_OPTIONS: readonly string[] = ['rootDir', 'outDir'];
|
|
457
|
+
|
|
458
|
+
/** Compare two JSON option values. Sound here because compiler options are scalars and flat arrays. */
|
|
459
|
+
function jsonEquals(left: JsonValue | undefined, right: JsonValue): boolean {
|
|
460
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* `tsconfig.json`, extending the workspace's base config when there is one.
|
|
465
|
+
*
|
|
466
|
+
* Options are dropped only when the base supplies the SAME value, never merely
|
|
467
|
+
* the same key: inheriting `"module": "CommonJS"` where the scaffold needs
|
|
468
|
+
* `NodeNext` would break every emitted import, so a differing base value is a
|
|
469
|
+
* reason to keep the override rather than to trust the parent.
|
|
470
|
+
*/
|
|
471
|
+
function tsconfigJson(composition: InitComposition): string {
|
|
472
|
+
const workspace = composition.packaging.workspace;
|
|
473
|
+
const base = workspace?.baseTsconfig === undefined ? undefined : (workspace.baseCompilerOptions ?? {});
|
|
474
|
+
const options =
|
|
475
|
+
base === undefined
|
|
476
|
+
? TSCONFIG_COMPILER_OPTIONS
|
|
477
|
+
: [
|
|
478
|
+
...TSCONFIG_COMPILER_OPTIONS.filter(([key]) => PACKAGE_LOCAL_OPTIONS.includes(key)),
|
|
479
|
+
...TSCONFIG_COMPILER_OPTIONS.filter(([key, value]) => !PACKAGE_LOCAL_OPTIONS.includes(key) && !jsonEquals(base[key], value))
|
|
480
|
+
];
|
|
481
|
+
const extendsLine = workspace?.baseTsconfig === undefined ? '' : ` "extends": "${workspace.baseTsconfig}",\n`;
|
|
482
|
+
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`;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// `isolatedModules` is what makes this check agree with the transform that
|
|
487
|
+
// actually runs the tests. `npm test` is `typecheck:test && vitest run`, and
|
|
488
|
+
// vitest compiles through esbuild — a per-file transform with no type
|
|
489
|
+
// information, which cannot tell a re-exported TYPE from a re-exported value.
|
|
490
|
+
// Without the flag `tsc` accepts `export { SomeType } from './x'` that esbuild
|
|
491
|
+
// then emits as a real import of a symbol that does not exist at runtime, so
|
|
492
|
+
// the typecheck passes and the suite fails with a confusing missing-export.
|
|
493
|
+
const TSCONFIG_TEST = `{
|
|
494
|
+
"extends": "./tsconfig.json",
|
|
495
|
+
"compilerOptions": {
|
|
496
|
+
"rootDir": ".",
|
|
497
|
+
"noEmit": true,
|
|
498
|
+
"isolatedModules": true,
|
|
499
|
+
"types": ["node"]
|
|
500
|
+
},
|
|
501
|
+
"include": ["src", "test"]
|
|
502
|
+
}
|
|
503
|
+
`;
|
|
504
|
+
|
|
505
|
+
const VITEST_CONFIG = `import { defineConfig } from 'vitest/config';
|
|
506
|
+
|
|
507
|
+
// Vitest transforms TypeScript itself and resolves \`.js\` specifiers to their
|
|
508
|
+
// \`.ts\` source, so tests import from \`../src/\` with the same specifiers the
|
|
509
|
+
// compiled output uses. \`include\` is scoped to \`test/\` so \`lib/\` is never
|
|
510
|
+
// scanned; \`npm test\` runs \`typecheck:test\` first, because the esbuild
|
|
511
|
+
// transform strips types without checking them.
|
|
512
|
+
|
|
513
|
+
export default defineConfig({
|
|
514
|
+
test: {
|
|
515
|
+
environment: 'node',
|
|
516
|
+
include: ['test/**/*.{test,spec}.ts']
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
`;
|
|
520
|
+
|
|
521
|
+
const GITIGNORE = `node_modules/
|
|
522
|
+
lib/
|
|
523
|
+
*.tsbuildinfo
|
|
524
|
+
|
|
525
|
+
# Langium-generated TextMate grammar (rewritten by every \`langium generate\`).
|
|
526
|
+
syntaxes/
|
|
527
|
+
`;
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* The workspace-member `.gitignore` — one entry, and the one entry a root
|
|
531
|
+
* cannot be assumed to have.
|
|
532
|
+
*
|
|
533
|
+
* A member inherits the root's rules, and `node_modules/`, `lib/` and
|
|
534
|
+
* `*.tsbuildinfo` are in every monorepo root already, so repeating them here
|
|
535
|
+
* would be three lines to keep in step for no coverage. `syntaxes/` is not like
|
|
536
|
+
* them: it is a Langium artefact, so a root that has never held a Langium
|
|
537
|
+
* package has no rule for it, and the first `langium generate` then offers
|
|
538
|
+
* generated output up for commit with nothing to warn the adopter. Dropping the
|
|
539
|
+
* whole file rather than this subset is the mistake this template exists to
|
|
540
|
+
* undo.
|
|
541
|
+
*/
|
|
542
|
+
const GITIGNORE_WORKSPACE_MEMBER = `# The workspace root already covers \`node_modules/\`, \`lib/\` and \`*.tsbuildinfo\`.
|
|
543
|
+
# It has no reason to know about this one: the TextMate grammar is a Langium
|
|
544
|
+
# artefact, rewritten by every \`langium generate\`, so a root that has never held
|
|
545
|
+
# a Langium package ignores nothing here and the generated file is offered for
|
|
546
|
+
# commit.
|
|
547
|
+
syntaxes/
|
|
548
|
+
`;
|
|
549
|
+
|
|
550
|
+
/** The terminals every grammar needs, shared verbatim by the fragment and the lone-grammar case. */
|
|
551
|
+
const TERMINALS = `hidden terminal WS: /\\s+/;
|
|
552
|
+
terminal ID: /[_a-zA-Z][\\w_]*/;
|
|
553
|
+
hidden terminal SL_COMMENT: /\\/\\/[^\\n\\r]*/;
|
|
554
|
+
hidden terminal ML_COMMENT: /\\/\\*[\\s\\S]*?\\*\\//;
|
|
555
|
+
`;
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* The shared lexical fragment, emitted only for a multi-grammar project.
|
|
559
|
+
*
|
|
560
|
+
* The alternative — every grammar declaring its own `WS` / `ID` — lets the token
|
|
561
|
+
* sets drift apart.
|
|
562
|
+
*/
|
|
563
|
+
const COMMON_GRAMMAR = `// Shared lexical basis for this project's grammars. Imported, never registered:
|
|
564
|
+
// it has no entry rule and no \`langium-config.json\` entry, so it is a fragment
|
|
565
|
+
// rather than a language.
|
|
566
|
+
//
|
|
567
|
+
// Keeping the tokens here rather than in one of the languages means a grammar
|
|
568
|
+
// imports \`common\` for its TOKENS and another language only when it needs that
|
|
569
|
+
// language's TYPES — separate reasons that are worth keeping separate.
|
|
570
|
+
|
|
571
|
+
${TERMINALS}`;
|
|
572
|
+
|
|
573
|
+
/** One starter grammar. `shared` decides whether it imports the fragment or declares its own terminals. */
|
|
574
|
+
function grammarFile(shared: boolean): string {
|
|
575
|
+
const lexis = shared ? "import './common'\n" : `\n${TERMINALS}`;
|
|
576
|
+
return `grammar __GRAMMAR__
|
|
577
|
+
${shared ? '\n' + lexis : ''}
|
|
578
|
+
// A minimal starter grammar — replace with your own language. \`__NODE_RULE__\` is
|
|
579
|
+
// a named, cross-referenceable node, which is enough for \`hydranium-cli reflect\`,
|
|
580
|
+
// \`lint-grammar\`, and \`validate\` to work out of the box.
|
|
581
|
+
//
|
|
582
|
+
// This name is the GRAMMAR's, not the project's: langium-cli emits
|
|
583
|
+
// \`__GRAMMAR__GeneratedModule\` from it, while \`projectName\` in
|
|
584
|
+
// langium-config.json emits the shared \`__NAME__GeneratedSharedModule\` and
|
|
585
|
+
// \`__NAME__AstReflection\`. They match by default and diverge as soon as you add
|
|
586
|
+
// a second grammar — that grammar gets its own name here, and the project name
|
|
587
|
+
// stays the umbrella.
|
|
588
|
+
//
|
|
589
|
+
// The rule names carry the grammar's prefix (\`__ENTRY_RULE__\`, \`__NODE_RULE__\`)
|
|
590
|
+
// because one langium-cli run over N grammars emits ONE combined \`ast.ts\`: two
|
|
591
|
+
// grammars both declaring \`entry Model:\` would put that interface in it twice.
|
|
592
|
+
|
|
593
|
+
entry __ENTRY_RULE__:
|
|
594
|
+
(nodes+=__NODE_RULE__)*;
|
|
595
|
+
|
|
596
|
+
__NODE_RULE__:
|
|
597
|
+
'node' name=ID ('->' target=[__NODE_RULE__:ID])?;
|
|
598
|
+
${shared ? '' : lexis}`;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* DI bootstrap. The single-grammar form composes one language; the multi-grammar
|
|
603
|
+
* form adds each further grammar through `additionalLanguages` (one shared tier,
|
|
604
|
+
* so `AstReflection`, `IndexManager` and `DocumentBuilder` are common and
|
|
605
|
+
* cross-grammar references resolve through one global index) and binds
|
|
606
|
+
* `lsp.configurationRoot` explicitly, which the framework warns about otherwise
|
|
607
|
+
* because its default is registration order rather than a decision.
|
|
608
|
+
*/
|
|
609
|
+
function moduleFile(composition: InitComposition): string {
|
|
610
|
+
const columns = columnsFor(composition);
|
|
611
|
+
const [primary, ...additional] = composition.grammars;
|
|
612
|
+
const multi = additional.length > 0;
|
|
613
|
+
const generatedImports = [
|
|
614
|
+
...composition.grammars.map(grammar => `${grammar.grammar}GeneratedModule`),
|
|
615
|
+
`${composition.name}GeneratedSharedModule`
|
|
616
|
+
];
|
|
617
|
+
|
|
618
|
+
const configurationRoot = multi
|
|
619
|
+
? `
|
|
620
|
+
/** LSP settings section every language reads its configuration from. */
|
|
621
|
+
export const __CONFIG_ROOT_CONST___CONFIGURATION_ROOT = '__PROJECT_ID__';
|
|
622
|
+
`
|
|
623
|
+
: '';
|
|
624
|
+
|
|
625
|
+
const sharedModule = multi
|
|
626
|
+
? `const __NAME__SharedModule: Module<
|
|
627
|
+
__NAME__SharedServices,
|
|
628
|
+
PartialLangiumSharedServices & { lsp: { configurationRoot: string } }
|
|
629
|
+
> = {
|
|
630
|
+
lsp: {
|
|
631
|
+
// Several languages are registered, so the framework default ("first
|
|
632
|
+
// registered id") would be registration order rather than a choice.
|
|
633
|
+
configurationRoot: () => __CONFIG_ROOT_CONST___CONFIGURATION_ROOT
|
|
634
|
+
}
|
|
635
|
+
};`
|
|
636
|
+
: `const __NAME__SharedModule: Module<__NAME__SharedServices, PartialLangiumSharedServices> = {};`;
|
|
637
|
+
|
|
638
|
+
const serializerImports = composition.grammars
|
|
639
|
+
.map(grammar => `import { ${grammar.grammar}Serializer } from './${grammar.grammarId}-serializer.js';`)
|
|
640
|
+
.join('\n');
|
|
641
|
+
|
|
642
|
+
// Each language gets its own adopter module because `Serializer` is a
|
|
643
|
+
// per-language slot. `additionalLanguages` defaults its `adopter` to the
|
|
644
|
+
// primary's, which would give every further grammar the FIRST grammar's
|
|
645
|
+
// serializer — and that one throws on a `$type` it has no emitter for.
|
|
646
|
+
const languageModules = composition.grammars
|
|
647
|
+
.map(
|
|
648
|
+
grammar => `const ${grammar.grammar}LanguageModule: Module<__NAME__Services, PartialLangiumServices & DeepPartial<ServerAddedServices>> = {
|
|
649
|
+
serializer: {
|
|
650
|
+
Serializer: services => new ${grammar.grammar}Serializer(services)
|
|
651
|
+
}
|
|
652
|
+
};`
|
|
653
|
+
)
|
|
654
|
+
.join('\n\n');
|
|
655
|
+
|
|
656
|
+
const additionalLanguages = multi
|
|
657
|
+
? `,\n additionalLanguages: ${arrayLiteral(
|
|
658
|
+
additional.map(grammar => `{ generated: ${grammar.grammar}GeneratedModule, adopter: () => ${grammar.grammar}LanguageModule }`),
|
|
659
|
+
' ',
|
|
660
|
+
columns
|
|
661
|
+
)}`
|
|
662
|
+
: '';
|
|
663
|
+
|
|
664
|
+
const destructure = multi ? '{ shared, languages }' : '{ shared, language }';
|
|
665
|
+
const returnType = composition.grammars.map(grammar => ` ${grammar.grammar}: __NAME__Services;`).join('\n');
|
|
666
|
+
const returnValue = multi
|
|
667
|
+
? composition.grammars.map((grammar, index) => `${grammar.grammar}: languages[${index}]`).join(', ')
|
|
668
|
+
: `${primary.grammar}: language`;
|
|
669
|
+
|
|
670
|
+
const text = `// DI bootstrap for __NAME__. Composes the framework defaults with adopter
|
|
671
|
+
// overrides via Langium's \`inject()\` (through the framework's
|
|
672
|
+
// \`createIntegrationServices\`). Each adopter module binds ONE slot — its
|
|
673
|
+
// language's \`Serializer\`, which the framework cannot default because a
|
|
674
|
+
// concrete syntax is grammar knowledge. Everything else (scope, naming, project
|
|
675
|
+
// management, build pipeline) boots on the framework defaults; scope
|
|
676
|
+
// computation, validation checks and AST extensions go in these same modules.
|
|
677
|
+
//
|
|
678
|
+
// Note which generated symbol comes from which name: the SHARED module is
|
|
679
|
+
// \`__NAME__GeneratedSharedModule\` (from \`projectName\`) and there is one of it,
|
|
680
|
+
// while the per-language \`<Grammar>GeneratedModule\` (from each \`grammar\`
|
|
681
|
+
// declaration) has one per grammar.${
|
|
682
|
+
multi
|
|
683
|
+
? `
|
|
684
|
+
//
|
|
685
|
+
// Every grammar here comes from ONE \`langium-cli\` run — \`AstReflection\` is a
|
|
686
|
+
// single shared slot, so two independently generated language packages would
|
|
687
|
+
// leave only the last one bound.`
|
|
688
|
+
: `
|
|
689
|
+
//
|
|
690
|
+
// A second grammar goes in the \`additionalLanguages\` option of
|
|
691
|
+
// \`createIntegrationServices\`, generated from this same \`langium-config.json\`.`
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
${importList(
|
|
695
|
+
['createLspServerLanguageModule', 'createLspServerSharedModule', 'type LspServerAddedServices', 'type LspServerAddedSharedServices'],
|
|
696
|
+
'@hydranium/core/lsp',
|
|
697
|
+
columns
|
|
698
|
+
)}
|
|
699
|
+
import { createIntegrationServices, type ServerAddedServices, type ServerModuleContext, type ServerSharedServices } from '@hydranium/core';
|
|
700
|
+
import { type DeepPartial, EmptyFileSystem, type Module } from '@hydranium/langium';
|
|
701
|
+
import { type LangiumServices, type PartialLangiumServices, type PartialLangiumSharedServices } from '@hydranium/langium/lsp';
|
|
702
|
+
${importList(generatedImports, './generated/module.js', columns)}
|
|
703
|
+
${serializerImports}
|
|
704
|
+
|
|
705
|
+
export type __NAME__SharedServices = ServerSharedServices & LspServerAddedSharedServices;
|
|
706
|
+
export type __NAME__Services = LangiumServices &
|
|
707
|
+
ServerAddedServices &
|
|
708
|
+
LspServerAddedServices & {
|
|
709
|
+
shared: __NAME__SharedServices;
|
|
710
|
+
};
|
|
711
|
+
${configurationRoot}
|
|
712
|
+
${sharedModule}
|
|
713
|
+
|
|
714
|
+
${languageModules}
|
|
715
|
+
|
|
716
|
+
/** Compose the Langium DI tree for __NAME__ — returns the shared + language services. */
|
|
717
|
+
export function create__NAME__Services(context: Partial<ServerModuleContext> = EmptyFileSystem): {
|
|
718
|
+
shared: __NAME__SharedServices;
|
|
719
|
+
${returnType}
|
|
720
|
+
} {
|
|
721
|
+
const fullContext: ServerModuleContext = { ...EmptyFileSystem, ...context };
|
|
722
|
+
const ${destructure} = createIntegrationServices<ServerModuleContext, __NAME__SharedServices, __NAME__Services>({
|
|
723
|
+
context: fullContext,
|
|
724
|
+
sharedModules: {
|
|
725
|
+
generated: __NAME__GeneratedSharedModule,
|
|
726
|
+
adopter: __NAME__SharedModule,
|
|
727
|
+
extra: [createLspServerSharedModule(fullContext)]
|
|
728
|
+
},
|
|
729
|
+
languageModules: {
|
|
730
|
+
generated: ${primary.grammar}GeneratedModule,
|
|
731
|
+
adopter: () => ${primary.grammar}LanguageModule,
|
|
732
|
+
extra: [createLspServerLanguageModule(fullContext)]
|
|
733
|
+
}${additionalLanguages}
|
|
734
|
+
});
|
|
735
|
+
return { shared, ${returnValue} };
|
|
736
|
+
}
|
|
737
|
+
`;
|
|
738
|
+
return project(text, composition);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const SERVICES = `// Zero-arg service factory for the headless \`hydranium-cli\` tooling: the
|
|
742
|
+
// \`reflect\` / \`lint-grammar\` / \`validate\` subcommands import this via
|
|
743
|
+
// \`--services ./lib/services.js\`. The head wires its own filesystem here.
|
|
744
|
+
//
|
|
745
|
+
// The contract is language-count-agnostic — a second grammar needs no edit here.
|
|
746
|
+
|
|
747
|
+
import { NodeFileSystem } from '@hydranium/core/node';
|
|
748
|
+
import { create__NAME__Services } from './language-server/__PROJECT_ID__-module.js';
|
|
749
|
+
|
|
750
|
+
export function createServices(): ReturnType<typeof create__NAME__Services> {
|
|
751
|
+
return create__NAME__Services({ ...NodeFileSystem });
|
|
752
|
+
}
|
|
753
|
+
`;
|
|
754
|
+
|
|
755
|
+
const AST = `// The language's AST entry point: a re-export of what \`langium generate\`
|
|
756
|
+
// emits, plus anywhere you augment those types.
|
|
757
|
+
//
|
|
758
|
+
// Import the AST from HERE rather than from \`./generated/ast.js\`, so any
|
|
759
|
+
// augmentation below travels with every import. One \`langium-cli\` run over N
|
|
760
|
+
// grammars emits ONE combined AST module sharing one reflection, so a further
|
|
761
|
+
// grammar needs no change here.
|
|
762
|
+
//
|
|
763
|
+
// \`generate:transfer-model\` reads this file as its \`--augmentation-file\`: the
|
|
764
|
+
// generated wire types are derived from the AST *as augmented*, not from the
|
|
765
|
+
// raw generated module. Augment a type here and the transfer model follows.
|
|
766
|
+
//
|
|
767
|
+
// A \`@derived\` property is computed at build time rather than parsed, so it is
|
|
768
|
+
// declared here and populated by an AST-extension contribution:
|
|
769
|
+
//
|
|
770
|
+
// declare module './generated/ast.js' {
|
|
771
|
+
// interface __ENTRY_RULE__ {
|
|
772
|
+
// /** @derived Populated by an \`ast.extensions.computedProperties\` contribution. */
|
|
773
|
+
// readonly _summary?: string;
|
|
774
|
+
// }
|
|
775
|
+
// }
|
|
776
|
+
|
|
777
|
+
export * from './generated/ast.js';
|
|
778
|
+
`;
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* The starter serializer — the concrete-syntax emitter for ONE grammar.
|
|
782
|
+
*
|
|
783
|
+
* Emitted per grammar, not per project, because `services.serializer.Serializer`
|
|
784
|
+
* is a per-language slot and a serializer is grammar-shaped by definition. That
|
|
785
|
+
* is also why the framework refuses to default it: its `UnboundSerializer`
|
|
786
|
+
* throws, naming the binding to add, rather than guessing a syntax.
|
|
787
|
+
*/
|
|
788
|
+
const SERIALIZER = `// Concrete-syntax emitter for __GRAMMAR__ — the parser's inverse, turning a
|
|
789
|
+
// model back into text this grammar accepts. Derived from the starter grammar
|
|
790
|
+
// exactly as \`generated/ast.ts\` is, so replacing the grammar replaces this too.
|
|
791
|
+
//
|
|
792
|
+
// Without it every structured write fails: the framework's default binding at
|
|
793
|
+
// \`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.
|
|
797
|
+
//
|
|
798
|
+
// \`AbstractSerializer\`'s generic property walk lays out FORMAT-structured
|
|
799
|
+
// output — its YAML and JSON subclasses are what it exists for — and cannot
|
|
800
|
+
// produce a keyword-delimited syntax like \`node a -> b\`. So \`serializeNode\` is
|
|
801
|
+
// a per-\`$type\` emitter here and the two array hooks are unreachable.
|
|
802
|
+
|
|
803
|
+
import { AbstractSerializer } from '@hydranium/core';
|
|
804
|
+
import type { AstNode } from '@hydranium/langium';
|
|
805
|
+
import { type __ENTRY_RULE__, type __NODE_RULE__, is__ENTRY_RULE__, is__NODE_RULE__ } from './ast.js';
|
|
806
|
+
|
|
807
|
+
export class __GRAMMAR__Serializer extends AbstractSerializer<__ENTRY_RULE__> {
|
|
808
|
+
protected override serializeNode(node: AstNode | Record<string, unknown>): string {
|
|
809
|
+
if (is__ENTRY_RULE__(node)) {
|
|
810
|
+
return node.nodes.map(child => this.emitNode(child)).join('\\n');
|
|
811
|
+
}
|
|
812
|
+
if (is__NODE_RULE__(node)) {
|
|
813
|
+
return this.emitNode(node);
|
|
814
|
+
}
|
|
815
|
+
// Defensive: a rule added to the grammar with no emitter added here.
|
|
816
|
+
throw new Error(\`__GRAMMAR__Serializer: no emitter for $type \${(node as AstNode).$type}\`);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/** Unreachable — this grammar's one list is emitted by its \`__ENTRY_RULE__\` parent. */
|
|
820
|
+
protected override serializeArray(): string {
|
|
821
|
+
throw new Error('__GRAMMAR__Serializer: arrays are emitted by the per-$type parent, not the generic dispatch.');
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/** Unreachable — same reasoning as {@link serializeArray}. */
|
|
825
|
+
protected override serializeReferenceArray(): string {
|
|
826
|
+
throw new Error('__GRAMMAR__Serializer: reference arrays are emitted by the per-$type parent, not the generic dispatch.');
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* \`serializeReferenceText\` rather than \`node.target?.$refText\`: it is the one
|
|
831
|
+
* read that spans BOTH input shapes. A transfer model reaching
|
|
832
|
+
* \`serializeTransfer\` carries \`target\` as a plain string rather than a
|
|
833
|
+
* \`Reference\`, and a serializer that reaches for \`$refText\` directly emits
|
|
834
|
+
* the AST correctly and drops every reference on the transfer path.
|
|
835
|
+
*/
|
|
836
|
+
private emitNode(node: __NODE_RULE__): string {
|
|
837
|
+
const target = this.serializeReferenceText(node.target);
|
|
838
|
+
return target === undefined ? \`node \${node.name}\` : \`node \${node.name} -> \${target}\`;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
`;
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* The exported constant names for the head set, in emission order. Both are
|
|
845
|
+
* keyed by the PROJECT rather than by a language: one server of each kind per
|
|
846
|
+
* process serves every registered grammar, so a language-derived name would tie
|
|
847
|
+
* a project-level endpoint to whichever grammar was scaffolded first.
|
|
848
|
+
*/
|
|
849
|
+
function portCommandNames(composition: InitComposition): string[] {
|
|
850
|
+
const upper = screamingSnake(composition.projectId);
|
|
851
|
+
return [
|
|
852
|
+
...(composition.heads.includes('data') ? [`${upper}_DATA_SERVER_PORT_COMMAND`] : []),
|
|
853
|
+
...(composition.heads.includes('glsp') ? [`${upper}_GLSP_PORT_COMMAND`] : [])
|
|
854
|
+
];
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* The socket heads' discovery commands, as an importable module.
|
|
859
|
+
*
|
|
860
|
+
* Deliberately NOT declared in `main.ts`. A host shell has to name the same
|
|
861
|
+
* string to reach the head, and `main.ts` opens a connection at module scope,
|
|
862
|
+
* so importing a constant from it would start a server. A host that retypes the
|
|
863
|
+
* literal instead gets no error when it drifts: the framework's port poll
|
|
864
|
+
* defaults to `findPortAttempts = -1`, so a wrong command retries forever
|
|
865
|
+
* rather than failing.
|
|
866
|
+
*/
|
|
867
|
+
function headPortsFile(composition: InitComposition): string {
|
|
868
|
+
const upper = screamingSnake(composition.projectId);
|
|
869
|
+
const data = composition.heads.includes('data');
|
|
870
|
+
const glsp = composition.heads.includes('glsp');
|
|
871
|
+
const blocks = [
|
|
872
|
+
...(data
|
|
873
|
+
? [
|
|
874
|
+
`/** LSP request the host queries to discover the data-server socket port. */\nexport const ${upper}_DATA_SERVER_PORT_COMMAND = '${composition.projectId}/data-server/port';`
|
|
875
|
+
]
|
|
876
|
+
: []),
|
|
877
|
+
...(glsp
|
|
878
|
+
? [
|
|
879
|
+
`/** LSP request the host queries to discover the GLSP socket port. */\nexport const ${upper}_GLSP_PORT_COMMAND = '${composition.projectId}/glsp/port';`
|
|
880
|
+
]
|
|
881
|
+
: [])
|
|
882
|
+
];
|
|
883
|
+
return `// The LSP requests a host queries to discover this server's socket head${blocks.length > 1 ? 's' : ''}.
|
|
884
|
+
//
|
|
885
|
+
// Both are keyed by the PROJECT, not the language: one server of each kind per
|
|
886
|
+
// process serves every registered grammar, so a language-derived name would tie
|
|
887
|
+
// a project-level endpoint to whichever grammar was scaffolded first.
|
|
888
|
+
//
|
|
889
|
+
// They live here rather than in \`main.ts\` because a host shell has to name the
|
|
890
|
+
// same string to reach the head, and \`main.ts\` is an executable entry — nothing
|
|
891
|
+
// can import from it. A shell that retypes the literal gets no error when it
|
|
892
|
+
// drifts: the framework's port poll retries indefinitely by default. Import
|
|
893
|
+
// these instead, and assert any host-side copy against them.
|
|
894
|
+
|
|
895
|
+
${blocks.join('\n\n')}
|
|
896
|
+
`;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
const INDEX = `// Public surface of the __NAME__ language server: the DI factory, the generated
|
|
900
|
+
// AST, and the headless \`createServices\` entry.
|
|
901
|
+
//
|
|
902
|
+
// The entry points under \`src/\` are deliberately NOT re-exported. Each opens a
|
|
903
|
+
// transport at module scope, so importing one starts a server as a side effect
|
|
904
|
+
// — which is why they are \`bin\` targets and this file is \`main\`.
|
|
905
|
+
|
|
906
|
+
export * from './language-server/__PROJECT_ID__-module.js';
|
|
907
|
+
export * from './language-server/ast.js';
|
|
908
|
+
export { createServices } from './services.js';
|
|
909
|
+
`;
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* The public surface, plus the head-port commands when there is a socket head.
|
|
913
|
+
*
|
|
914
|
+
* The commands belong here rather than only in `head-ports.ts` so a host shell
|
|
915
|
+
* reaches them from the package root, the same way it reaches `createServices`.
|
|
916
|
+
*/
|
|
917
|
+
function indexFile(composition: InitComposition): string {
|
|
918
|
+
const base = project(INDEX, composition);
|
|
919
|
+
return portCommandNames(composition).length ? `${base}export * from './head-ports.js';\n` : base;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* The `#!` line every emitted `bin` target carries.
|
|
924
|
+
*
|
|
925
|
+
* Required in the SOURCE, not added at publish time: npm's install-time `fixBin`
|
|
926
|
+
* sets the exec bit on a linked `bin` target but writes no interpreter line, so
|
|
927
|
+
* an entry whose first line is the SPDX header is handed to `/bin/sh`, which
|
|
928
|
+
* tries to execute the licence comment. `tsc` carries a leading shebang through
|
|
929
|
+
* to `lib/` unchanged, so emitting it here is what makes the linked binary
|
|
930
|
+
* runnable — and it must stay the FIRST line, ahead of the header a licence
|
|
931
|
+
* sweep adds below it.
|
|
932
|
+
*/
|
|
933
|
+
const SHEBANG = '#!/usr/bin/env node\n';
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* The launcher, assembled from the head set.
|
|
937
|
+
*
|
|
938
|
+
* Every head runs in the same process over ONE `createXxxServices` call, which
|
|
939
|
+
* is what "three heads over one workspace" means: they share the model store,
|
|
940
|
+
* the index and the build pipeline rather than each parsing their own copy.
|
|
941
|
+
*
|
|
942
|
+
* The data head's root type is the union of every grammar's transfer root, which
|
|
943
|
+
* is what one data server serving N grammars means.
|
|
944
|
+
*/
|
|
945
|
+
function mainFile(composition: InitComposition): string {
|
|
946
|
+
const columns = columnsFor(composition);
|
|
947
|
+
const roots = [...composition.grammars.map(grammar => grammar.entryRule)].sort();
|
|
948
|
+
const upper = screamingSnake(composition.projectId);
|
|
949
|
+
const data = composition.heads.includes('data');
|
|
950
|
+
const glsp = composition.heads.includes('glsp');
|
|
951
|
+
const diagrams = composition.grammars.filter(grammar => grammar.diagram);
|
|
952
|
+
const plural = roots.length > 1 ? 's' : '';
|
|
953
|
+
|
|
954
|
+
const headNames = composition.heads.map(head =>
|
|
955
|
+
head === 'lsp' ? 'the Langium LSP head' : head === 'data' ? 'a socket data-server (model server) head' : 'a GLSP head'
|
|
956
|
+
);
|
|
957
|
+
// Comma-joined but for the last, which takes `and`: a bare `, ` join reads as
|
|
958
|
+
// a truncated list, and the emitted comment is the first thing an adopter
|
|
959
|
+
// reads in the file they run.
|
|
960
|
+
const headSummary =
|
|
961
|
+
headNames.length === 1 ? headNames[0] : `${headNames.slice(0, -1).join(', ')} and ${headNames[headNames.length - 1]}`;
|
|
962
|
+
|
|
963
|
+
// `reflect-metadata` must be imported before anything that reads a
|
|
964
|
+
// decorator's emitted metadata, which inversify does at module scope.
|
|
965
|
+
const reflectImport = glsp ? "import 'reflect-metadata';\n" : '';
|
|
966
|
+
const coreNodeSymbols = ['NodeFileSystem', ...(data ? ['publishPortOnLspConnection', 'startSocketServer'] : [])];
|
|
967
|
+
const glspImports = glsp
|
|
968
|
+
? `import { ServerModule } from '@eclipse-glsp/server/node.js';
|
|
969
|
+
import { GlspClientLogger, HydraniumGlspAppModule } from '@hydranium/glsp-server';
|
|
970
|
+
import { startGlspServer } from '@hydranium/glsp-server/node';
|
|
971
|
+
`
|
|
972
|
+
: '';
|
|
973
|
+
const diagramImports = diagrams
|
|
974
|
+
.map(grammar => `import { ${grammar.grammar}DiagramModule } from './glsp/${grammar.grammarId}/diagram-module.js';\n`)
|
|
975
|
+
.join('');
|
|
976
|
+
const transferImports = data
|
|
977
|
+
? `// The TRANSFER root${plural}, not the AST one${plural}. The data head serialises to the
|
|
978
|
+
// persisted shape, where \`Reference<T>\` is a plain \`string\`; the AST's is a
|
|
979
|
+
// Langium reference object with \`.ref\` / \`.$refText\`. Both satisfy
|
|
980
|
+
// \`TransferElement\` structurally, so naming the AST type here compiles fine and
|
|
981
|
+
// silently tells every typed client that a reference is a resolvable object
|
|
982
|
+
// rather than a name.
|
|
983
|
+
${importList(roots, './language-server/generated-transfer/transfer-model.js', columns, true)}
|
|
984
|
+
`
|
|
985
|
+
: '';
|
|
986
|
+
|
|
987
|
+
// The commands live in `head-ports.ts`, not here: a host shell has to name
|
|
988
|
+
// the same string to reach the head, and this file is an executable entry, so
|
|
989
|
+
// nothing can import from it.
|
|
990
|
+
const portCommands = portCommandNames(composition).length
|
|
991
|
+
? importList(portCommandNames(composition), './head-ports.js', columns) + '\n'
|
|
992
|
+
: '';
|
|
993
|
+
|
|
994
|
+
const dataBlock = data
|
|
995
|
+
? `
|
|
996
|
+
// Data-server head alongside LSP: binds an ephemeral port, published over the LSP
|
|
997
|
+
// connection for the host to discover. Each accepted client gets its own DataServer.
|
|
998
|
+
//
|
|
999
|
+
// Neither the bind nor the publish may be swallowed: either failure leaves the LSP
|
|
1000
|
+
// head serving text edits while every data client waits on a port command that was
|
|
1001
|
+
// never registered, and the launcher reports a bind failure only if given a logger.
|
|
1002
|
+
const dataServer = startSocketServer({ port: 0, logTag: 'ModelServer', logger: shared.Logger }, dataConnection => {
|
|
1003
|
+
new DataServer<${roots.join(' | ')}>(dataConnection, shared);
|
|
1004
|
+
return { dispose: () => undefined };
|
|
1005
|
+
});
|
|
1006
|
+
dataServer.started
|
|
1007
|
+
.then(() => {
|
|
1008
|
+
const { port } = dataServer;
|
|
1009
|
+
if (port === undefined) {
|
|
1010
|
+
// \`started\` resolves only once the address is resolved, so this is
|
|
1011
|
+
// unreachable; a non-null assertion in its place would publish
|
|
1012
|
+
// \`undefined\`, which the host cannot tell from an unreachable port.
|
|
1013
|
+
throw new Error('the data head started without a resolved port');
|
|
1014
|
+
}
|
|
1015
|
+
publishPortOnLspConnection(shared.lsp.Connection, ${upper}_DATA_SERVER_PORT_COMMAND, port);
|
|
1016
|
+
})
|
|
1017
|
+
.catch((error: unknown) => {
|
|
1018
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1019
|
+
shared.Logger.error(\`[ModelServer] Could not publish the data-server port: \${reason}\`);
|
|
1020
|
+
});
|
|
1021
|
+
`
|
|
1022
|
+
: '';
|
|
1023
|
+
|
|
1024
|
+
const glspBlock = glsp
|
|
1025
|
+
? `
|
|
1026
|
+
// GLSP head on the same shared workspace. \`HydraniumGlspAppModule\` is used
|
|
1027
|
+
// unsubclassed: the app container is one per process and cannot know which
|
|
1028
|
+
// grammar a request concerns, so nothing per-language belongs there — each
|
|
1029
|
+
// diagram module declares its own language instead.
|
|
1030
|
+
//
|
|
1031
|
+
// GLSP framework logs route through the LSP connection rather than stdout, which
|
|
1032
|
+
// IS the LSP transport in stdio mode; writing there corrupts the protocol stream.
|
|
1033
|
+
const glspServer = startGlspServer({
|
|
1034
|
+
// The GLSP log threshold lives on the logger, not beside it: the framework
|
|
1035
|
+
// replaces GLSP's own \`Logger\` binding, so a launcher-level \`logLevel\` would
|
|
1036
|
+
// be discarded. The logger tracks the framework's process-wide threshold, so
|
|
1037
|
+
// \`HYDRANIUM_LOG_LEVEL\` and the LSP log-level setting govern GLSP output too;
|
|
1038
|
+
// pass \`logLevel\` only to make GLSP quieter than the rest of the server.
|
|
1039
|
+
createLogger: caller => new GlspClientLogger(shared, { component: caller }),
|
|
1040
|
+
serverModule: new ServerModule()${diagrams.map(grammar => `.configureDiagramModule(new ${grammar.grammar}DiagramModule())`).join('')},
|
|
1041
|
+
appModules: [new HydraniumGlspAppModule({ shared })],
|
|
1042
|
+
lspConnection: shared.lsp.Connection,
|
|
1043
|
+
portCommand: ${upper}_GLSP_PORT_COMMAND
|
|
1044
|
+
});
|
|
1045
|
+
void glspServer;
|
|
1046
|
+
`
|
|
1047
|
+
: '';
|
|
1048
|
+
|
|
1049
|
+
const text = `${SHEBANG}// Standalone entry point: starts ${headSummary} in the same
|
|
1050
|
+
// process, so every head shares one model store.
|
|
1051
|
+
// Invocation: \`node lib/main.js --stdio\`, or the package's \`__PROJECT_ID__\` bin
|
|
1052
|
+
// script.${
|
|
1053
|
+
data
|
|
1054
|
+
? `
|
|
1055
|
+
//
|
|
1056
|
+
// NOT the entry \`hydranium-cli query\` / \`save\` / \`projects\` / \`watch\` speak to:
|
|
1057
|
+
// stdio here carries LSP, and the data head is a socket whose port is published
|
|
1058
|
+
// over the LSP connection. Those subcommands spawn \`data-server-main.js\`.`
|
|
1059
|
+
: ''
|
|
1060
|
+
}
|
|
1061
|
+
//
|
|
1062
|
+
// Everything here runs at module scope, so this file is an executable rather
|
|
1063
|
+
// than a library entry — import \`./index.js\` instead to compose the language.
|
|
1064
|
+
|
|
1065
|
+
${reflectImport}${glspImports}${importList(coreNodeSymbols, '@hydranium/core/node', columns)}
|
|
1066
|
+
${data ? "import { DataServer } from '@hydranium/data-server';\n" : ''}import { startLanguageServer } from '@hydranium/langium/lsp';
|
|
1067
|
+
import { ProposedFeatures, createConnection } from 'vscode-languageserver/node';
|
|
1068
|
+
${diagramImports}${transferImports}import { create__NAME__Services } from './language-server/__PROJECT_ID__-module.js';
|
|
1069
|
+
${portCommands === '' ? '' : '\n' + portCommands}
|
|
1070
|
+
const connection = createConnection(ProposedFeatures.all);
|
|
1071
|
+
const { shared } = create__NAME__Services({ connection, ...NodeFileSystem });
|
|
1072
|
+
startLanguageServer(shared);
|
|
1073
|
+
${dataBlock}${glspBlock}`;
|
|
1074
|
+
return project(text, composition);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
/**
|
|
1078
|
+
* The second executable entry: the data head alone, on stdio.
|
|
1079
|
+
*
|
|
1080
|
+
* Emitted with the `data` head because it is the ONLY command line that reaches
|
|
1081
|
+
* that head from outside the editor. `main.ts` gives stdio to LSP and publishes
|
|
1082
|
+
* the data head's socket port over the LSP connection, so a client that is not
|
|
1083
|
+
* an LSP client cannot discover it — which is every one of `hydranium-cli`'s
|
|
1084
|
+
* `query` / `save` / `projects` / `watch`, all of which drive the data protocol
|
|
1085
|
+
* over a spawned child's stdin/stdout. Without this file a scaffolded project
|
|
1086
|
+
* has no `--server` value, and the closest guess — the LSP entry — fails as an
|
|
1087
|
+
* unrecognised method rather than as a missing head.
|
|
1088
|
+
*
|
|
1089
|
+
* A separate process rather than a flag on `main.ts`: only one protocol can own
|
|
1090
|
+
* stdio, so the two entries are mutually exclusive by construction.
|
|
1091
|
+
*/
|
|
1092
|
+
function dataServerMainFile(composition: InitComposition): string {
|
|
1093
|
+
const columns = columnsFor(composition);
|
|
1094
|
+
const roots = [...composition.grammars.map(grammar => grammar.entryRule)].sort();
|
|
1095
|
+
const plural = roots.length > 1 ? 's' : '';
|
|
1096
|
+
|
|
1097
|
+
const text = `${SHEBANG}// Standalone entry point: a data-server head on **stdio**, with no LSP head in
|
|
1098
|
+
// the process. This is the transport \`hydranium-cli\` speaks — \`query\`, \`save\`,
|
|
1099
|
+
// \`projects\` and \`watch\` all spawn a server command and drive JSON-RPC over its
|
|
1100
|
+
// stdin/stdout — so it is what makes those subcommands usable against this
|
|
1101
|
+
// language.
|
|
1102
|
+
//
|
|
1103
|
+
// Invocation: \`node lib/data-server-main.js [<workspace-path>]\`, or the
|
|
1104
|
+
// package's \`__PROJECT_ID__-data-server\` bin script. The workspace path defaults to
|
|
1105
|
+
// the process cwd, which is what the CLI's \`--cwd\` sets on the child.
|
|
1106
|
+
//
|
|
1107
|
+
// Contrast with \`main.ts\`, the editor entry: there the LSP head owns stdio and
|
|
1108
|
+
// the data head is a socket published over the LSP connection. Here there is no
|
|
1109
|
+
// LSP connection at all, so the workspace initialization an
|
|
1110
|
+
// \`initialize\`/\`initialized\` pair would otherwise drive has to happen here —
|
|
1111
|
+
// which is why this entry uses \`startStdioServer\` rather than wiring a
|
|
1112
|
+
// connection directly.
|
|
1113
|
+
//
|
|
1114
|
+
// Everything here runs at module scope, so this file is an executable rather
|
|
1115
|
+
// than a library entry — import \`./index.js\` instead to compose the language.
|
|
1116
|
+
|
|
1117
|
+
${importList(['NodeFileSystem', 'startStdioServer'], '@hydranium/core/node', columns)}
|
|
1118
|
+
import { DataServer } from '@hydranium/data-server';
|
|
1119
|
+
// 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)}
|
|
1121
|
+
import { create__NAME__Services } from './language-server/__PROJECT_ID__-module.js';
|
|
1122
|
+
|
|
1123
|
+
const { shared } = create__NAME__Services({ ...NodeFileSystem });
|
|
1124
|
+
|
|
1125
|
+
// \`startStdioServer\` owns the transport AND the workspace bring-up, including
|
|
1126
|
+
// the ordering between them: a head with no LSP connection never receives
|
|
1127
|
+
// \`initialize\`/\`initialized\`, and initialization has to complete before the
|
|
1128
|
+
// reader is attached or a request arriving during startup races an unpopulated
|
|
1129
|
+
// project registry. The launcher exists so no adopter has to re-derive that
|
|
1130
|
+
// ordering by hand.
|
|
1131
|
+
const server = startStdioServer(
|
|
1132
|
+
{
|
|
1133
|
+
shared,
|
|
1134
|
+
// Defaults to the process cwd, which is what the CLI's \`--cwd\` sets on the
|
|
1135
|
+
// spawned child; an explicit path argument overrides it.
|
|
1136
|
+
workspace: process.argv[2] ?? process.cwd(),
|
|
1137
|
+
logger: shared.Logger,
|
|
1138
|
+
logTag: 'ModelServer'
|
|
1139
|
+
},
|
|
1140
|
+
connection => {
|
|
1141
|
+
new DataServer<${roots.join(' | ')}>(connection, shared);
|
|
1142
|
+
// The DataServer self-cleans via \`connection.onClose\`, so there is nothing
|
|
1143
|
+
// extra to tear down here.
|
|
1144
|
+
return { dispose: () => undefined };
|
|
1145
|
+
}
|
|
1146
|
+
);
|
|
1147
|
+
|
|
1148
|
+
// Surfaces a failed bring-up as a non-zero exit instead of a silent, listening
|
|
1149
|
+
// head that would answer against an empty workspace.
|
|
1150
|
+
server.started.catch(() => process.exit(1));
|
|
1151
|
+
`;
|
|
1152
|
+
return project(text, composition);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
/** The scaffold's first test: the DI tree composes and every language is registered. */
|
|
1156
|
+
function servicesTest(composition: InitComposition): string {
|
|
1157
|
+
const ids = composition.grammars.map(grammar => grammar.languageId);
|
|
1158
|
+
const extensions = composition.grammars.flatMap(grammar => grammar.extensions.map(extension => `.${extension}`));
|
|
1159
|
+
const title = ids.length === 1 ? 'exactly one language' : `all ${ids.length} languages`;
|
|
1160
|
+
const text = `// The scaffold's first test: the DI tree composes and every language is
|
|
1161
|
+
// registered. Deliberately grammar-agnostic, so it keeps passing once you
|
|
1162
|
+
// replace the starter grammar with your own.
|
|
1163
|
+
//
|
|
1164
|
+
// \`createServices()\` not throwing is itself an assertion — the framework's
|
|
1165
|
+
// \`assertCoreSlotsBound\` runs during bootstrap and fails loudly when a module
|
|
1166
|
+
// is missing from the composition.
|
|
1167
|
+
|
|
1168
|
+
import { describe, expect, it } from 'vitest';
|
|
1169
|
+
import { createServices } from '../src/services.js';
|
|
1170
|
+
|
|
1171
|
+
describe('__NAME__ services', () => {
|
|
1172
|
+
it('composes the DI tree and registers ${title}', () => {
|
|
1173
|
+
const { shared } = createServices();
|
|
1174
|
+
|
|
1175
|
+
const registered = shared.ServiceRegistry.all.map(language => language.LanguageMetaData);
|
|
1176
|
+
expect(registered.map(metadata => metadata.languageId)).toEqual([${quotedList(ids)}]);
|
|
1177
|
+
expect(registered.flatMap(metadata => [...metadata.fileExtensions])).toEqual([${quotedList(extensions)}]);
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1180
|
+
it('binds a reflection covering the generated AST', () => {
|
|
1181
|
+
const { shared } = createServices();
|
|
1182
|
+
|
|
1183
|
+
expect(shared.AstReflection.getAllTypes().length).toBeGreaterThan(0);
|
|
1184
|
+
});
|
|
1185
|
+
});
|
|
1186
|
+
`;
|
|
1187
|
+
return project(text, composition);
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
/**
|
|
1191
|
+
* One `describe` per grammar, round-tripping that grammar's serializer.
|
|
1192
|
+
*
|
|
1193
|
+
* Both directions, because they fail independently: an emitter reaching for
|
|
1194
|
+
* `$refText` renders the AST correctly and silently drops every reference on the
|
|
1195
|
+
* transfer path, which is the shape the data head hands it.
|
|
1196
|
+
*/
|
|
1197
|
+
const SERIALIZATION_SUITE = `describe('__GRAMMAR__ serialization', () => {
|
|
1198
|
+
const source = 'node first -> second\\nnode second';
|
|
1199
|
+
|
|
1200
|
+
it('round-trips parsed source back to the same text', async () => {
|
|
1201
|
+
const { __GRAMMAR__ } = createServices();
|
|
1202
|
+
|
|
1203
|
+
const document = await parseHelper<__ENTRY_RULE__>(__GRAMMAR__)(source, { documentUri: 'file:///round-trip.__EXTENSION__' });
|
|
1204
|
+
|
|
1205
|
+
expect(document.parseResult.parserErrors).toHaveLength(0);
|
|
1206
|
+
expect(await __GRAMMAR__.serializer.Serializer.serializeAst(document.parseResult.value)).toBe(source);
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
it('emits the same text from a transfer model, whose references are plain strings', async () => {
|
|
1210
|
+
const { __GRAMMAR__ } = createServices();
|
|
1211
|
+
const model: Transfer__ENTRY_RULE__ = {
|
|
1212
|
+
$type: '__ENTRY_RULE__',
|
|
1213
|
+
nodes: [
|
|
1214
|
+
{ $type: '__NODE_RULE__', name: 'first', target: 'second' },
|
|
1215
|
+
{ $type: '__NODE_RULE__', name: 'second' }
|
|
1216
|
+
]
|
|
1217
|
+
};
|
|
1218
|
+
|
|
1219
|
+
expect(await __GRAMMAR__.serializer.Serializer.serializeTransfer(model)).toBe(source);
|
|
1220
|
+
});
|
|
1221
|
+
});`;
|
|
1222
|
+
|
|
1223
|
+
/**
|
|
1224
|
+
* The serializer round-trip test, one suite per grammar.
|
|
1225
|
+
*
|
|
1226
|
+
* Grammar-DERIVED, unlike `services.test.ts` next to it: it names the starter
|
|
1227
|
+
* rules and the syntax they spell, so replacing the grammar replaces this file
|
|
1228
|
+
* along with the serializer it covers. That is the same bargain
|
|
1229
|
+
* `gmodel-factory.ts` makes, and the reason it is worth making here is that
|
|
1230
|
+
* nothing else executes the serializer — the golden pins its bytes and `tsc`
|
|
1231
|
+
* pins its types, and neither can see a wrong emission.
|
|
1232
|
+
*/
|
|
1233
|
+
function serializationTest(composition: InitComposition): string {
|
|
1234
|
+
const columns = columnsFor(composition);
|
|
1235
|
+
const astTypes = composition.grammars.map(grammar => grammar.entryRule);
|
|
1236
|
+
const transferTypes = composition.grammars.map(grammar => `${grammar.entryRule} as Transfer${grammar.entryRule}`);
|
|
1237
|
+
const text = `// Round-trips each grammar through its serializer: parse the source, serialize
|
|
1238
|
+
// 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.
|
|
1241
|
+
//
|
|
1242
|
+
// The transfer case is the one that catches the mistake worth catching.
|
|
1243
|
+
// \`ModelService.modelToText\` short-circuits only a RAW STRING, so a typed model
|
|
1244
|
+
// from the data head reaches \`serializeTransfer\` — where a cross-reference is a
|
|
1245
|
+
// plain string, not a \`Reference\`.
|
|
1246
|
+
|
|
1247
|
+
import { parseHelper } from '@hydranium/core/testing';
|
|
1248
|
+
import { describe, expect, it } from 'vitest';
|
|
1249
|
+
${importList(astTypes, '../src/language-server/ast.js', columns, true)}
|
|
1250
|
+
${importList(transferTypes, '../src/language-server/generated-transfer/transfer-model.js', columns, true)}
|
|
1251
|
+
import { createServices } from '../src/services.js';
|
|
1252
|
+
|
|
1253
|
+
${composition.grammars.map(grammar => render(SERIALIZATION_SUITE, composition, grammar)).join('\n\n')}
|
|
1254
|
+
`;
|
|
1255
|
+
return text;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
/**
|
|
1259
|
+
* The three tiers `generator-langium` scaffolds, in the order a new adopter
|
|
1260
|
+
* breaks them: does my rule parse, does my cross-reference resolve, does a
|
|
1261
|
+
* broken one get reported.
|
|
1262
|
+
*
|
|
1263
|
+
* Each is grammar-derived and each keeps working in SHAPE once the starter
|
|
1264
|
+
* grammar is replaced, which is the same bargain `services.test.ts` makes from
|
|
1265
|
+
* the other direction — that one stays grammar-agnostic and asserts nothing
|
|
1266
|
+
* about the language.
|
|
1267
|
+
*/
|
|
1268
|
+
const PARSING_SUITE = `describe('__GRAMMAR__ parsing', () => {
|
|
1269
|
+
it('parses the starter rules and populates the AST', async () => {
|
|
1270
|
+
const { __GRAMMAR__ } = createServices();
|
|
1271
|
+
|
|
1272
|
+
const document = await parseHelper<__ENTRY_RULE__>(__GRAMMAR__)('node first -> second\\nnode second', {
|
|
1273
|
+
documentUri: 'file:///parsing.__EXTENSION__'
|
|
1274
|
+
});
|
|
1275
|
+
|
|
1276
|
+
expect(document.parseResult.lexerErrors).toHaveLength(0);
|
|
1277
|
+
expect(document.parseResult.parserErrors).toHaveLength(0);
|
|
1278
|
+
expect(document.parseResult.value.nodes.map(node => node.name)).toEqual(['first', 'second']);
|
|
1279
|
+
});
|
|
1280
|
+
|
|
1281
|
+
it('reports a parser error for text the grammar does not accept', async () => {
|
|
1282
|
+
const { __GRAMMAR__ } = createServices();
|
|
1283
|
+
|
|
1284
|
+
// The name is mandatory, so this is a parse failure rather than a
|
|
1285
|
+
// validation one — nothing downstream of the parser runs on it.
|
|
1286
|
+
const document = await parseHelper<__ENTRY_RULE__>(__GRAMMAR__)('node -> second', {
|
|
1287
|
+
documentUri: 'file:///invalid.__EXTENSION__'
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1290
|
+
expect(document.parseResult.parserErrors.length).toBeGreaterThan(0);
|
|
1291
|
+
});
|
|
1292
|
+
});`;
|
|
1293
|
+
|
|
1294
|
+
/**
|
|
1295
|
+
* Both directions of linking, and the CROSS-document one is the half that
|
|
1296
|
+
* matters: a same-document reference resolves through the local scope and would
|
|
1297
|
+
* still pass with the global index empty.
|
|
1298
|
+
*
|
|
1299
|
+
* `DocumentBuilder.build` rather than `parseHelper`'s `validation` option
|
|
1300
|
+
* because linking is what is under test, and a build over BOTH documents is
|
|
1301
|
+
* what puts the first one's exports in the index the second one reads.
|
|
1302
|
+
*/
|
|
1303
|
+
const LINKING_SUITE = `describe('__GRAMMAR__ linking', () => {
|
|
1304
|
+
it('resolves a reference within one document', async () => {
|
|
1305
|
+
const { shared, __GRAMMAR__ } = createServices();
|
|
1306
|
+
|
|
1307
|
+
const document = await parseHelper<__ENTRY_RULE__>(__GRAMMAR__)('node first -> second\\nnode second', {
|
|
1308
|
+
documentUri: 'file:///within.__EXTENSION__'
|
|
1309
|
+
});
|
|
1310
|
+
await shared.workspace.DocumentBuilder.build([document]);
|
|
1311
|
+
|
|
1312
|
+
expect(document.parseResult.value.nodes[0].target?.ref?.name).toBe('second');
|
|
1313
|
+
});
|
|
1314
|
+
|
|
1315
|
+
it('resolves a reference across documents, through the shared index', async () => {
|
|
1316
|
+
const { shared, __GRAMMAR__ } = createServices();
|
|
1317
|
+
const parse = parseHelper<__ENTRY_RULE__>(__GRAMMAR__);
|
|
1318
|
+
|
|
1319
|
+
const declaring = await parse('node second', { documentUri: 'file:///declaring.__EXTENSION__' });
|
|
1320
|
+
const referencing = await parse('node first -> second', { documentUri: 'file:///referencing.__EXTENSION__' });
|
|
1321
|
+
await shared.workspace.DocumentBuilder.build([declaring, referencing]);
|
|
1322
|
+
|
|
1323
|
+
// The URI, not just the name: the referencing document declares no
|
|
1324
|
+
// \`second\` of its own, but asserting WHERE the target came from is what
|
|
1325
|
+
// keeps this about the global index rather than about local scope.
|
|
1326
|
+
const target = referencing.parseResult.value.nodes[0].target?.ref;
|
|
1327
|
+
expect(target?.name).toBe('second');
|
|
1328
|
+
expect(target ? AstUtils.getDocument(target).uri.toString() : undefined).toBe('file:///declaring.__EXTENSION__');
|
|
1329
|
+
});
|
|
1330
|
+
});`;
|
|
1331
|
+
|
|
1332
|
+
/**
|
|
1333
|
+
* The validation tier, asserting the FRAMEWORK's diagnostics rather than an
|
|
1334
|
+
* adopter rule.
|
|
1335
|
+
*
|
|
1336
|
+
* The scaffold binds no `validation.checks`, so a test over an adopter check
|
|
1337
|
+
* would have to invent one and would then assert the test's own fixture rather
|
|
1338
|
+
* than the language. What a scaffolded project really guarantees on day one is
|
|
1339
|
+
* that a dangling reference is reported, which is a linker diagnostic — and
|
|
1340
|
+
* that is the tier a new adopter breaks third.
|
|
1341
|
+
*/
|
|
1342
|
+
const VALIDATING_SUITE = `describe('__GRAMMAR__ validation', () => {
|
|
1343
|
+
it('reports nothing for a well-formed document', async () => {
|
|
1344
|
+
const { shared, __GRAMMAR__ } = createServices();
|
|
1345
|
+
|
|
1346
|
+
const document = await parseHelper<__ENTRY_RULE__>(__GRAMMAR__)('node first -> second\\nnode second', {
|
|
1347
|
+
documentUri: 'file:///valid.__EXTENSION__'
|
|
1348
|
+
});
|
|
1349
|
+
await shared.workspace.DocumentBuilder.build([document], { validation: true });
|
|
1350
|
+
|
|
1351
|
+
expect(document.diagnostics ?? []).toHaveLength(0);
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1354
|
+
it('reports an error for a reference that resolves to nothing', async () => {
|
|
1355
|
+
const { shared, __GRAMMAR__ } = createServices();
|
|
1356
|
+
|
|
1357
|
+
const document = await parseHelper<__ENTRY_RULE__>(__GRAMMAR__)('node first -> absent', {
|
|
1358
|
+
documentUri: 'file:///dangling.__EXTENSION__'
|
|
1359
|
+
});
|
|
1360
|
+
await shared.workspace.DocumentBuilder.build([document], { validation: true });
|
|
1361
|
+
|
|
1362
|
+
const diagnostics = document.diagnostics ?? [];
|
|
1363
|
+
expect(diagnostics).toHaveLength(1);
|
|
1364
|
+
expect(diagnostics[0].severity).toBe(DiagnosticSeverity.Error);
|
|
1365
|
+
expect(diagnostics[0].message).toContain('absent');
|
|
1366
|
+
});
|
|
1367
|
+
});`;
|
|
1368
|
+
|
|
1369
|
+
/** Assemble one grammar-derived test file: a fixed preamble plus one suite per grammar. */
|
|
1370
|
+
function grammarTest(composition: InitComposition, header: string, imports: readonly string[], suite: string): string {
|
|
1371
|
+
const columns = columnsFor(composition);
|
|
1372
|
+
const astTypes = composition.grammars.map(grammar => grammar.entryRule);
|
|
1373
|
+
return `${header}
|
|
1374
|
+
|
|
1375
|
+
import { parseHelper } from '@hydranium/core/testing';
|
|
1376
|
+
import { describe, expect, it } from 'vitest';
|
|
1377
|
+
${imports.join('\n')}${imports.length ? '\n' : ''}${importList(astTypes, '../src/language-server/ast.js', columns, true)}
|
|
1378
|
+
import { createServices } from '../src/services.js';
|
|
1379
|
+
|
|
1380
|
+
${composition.grammars.map(grammar => render(suite, composition, grammar)).join('\n\n')}
|
|
1381
|
+
`;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
const PARSING_HEADER = `// Tier one of the three a new language breaks in order: does my rule parse.
|
|
1385
|
+
//
|
|
1386
|
+
// Grammar-derived, so replacing the starter grammar replaces this file — but
|
|
1387
|
+
// the SHAPE survives, which is the point of scaffolding it: the questions stay
|
|
1388
|
+
// the same for every language, only the source text changes.`;
|
|
1389
|
+
|
|
1390
|
+
const LINKING_HEADER = `// Tier two: does my cross-reference resolve.
|
|
1391
|
+
//
|
|
1392
|
+
// Both cases matter and they fail for different reasons. A same-document
|
|
1393
|
+
// reference resolves through local scope alone; a cross-document one needs the
|
|
1394
|
+
// declaring document's exports in the shared global index, which is what a
|
|
1395
|
+
// multi-file workspace depends on and what a single-document test cannot see.`;
|
|
1396
|
+
|
|
1397
|
+
const VALIDATING_HEADER = `// Tier three: does a broken model get reported.
|
|
1398
|
+
//
|
|
1399
|
+
// These are the FRAMEWORK's own linker diagnostics, not adopter checks — the
|
|
1400
|
+
// scaffold binds no \`validation.checks\`, and a test over an invented one would
|
|
1401
|
+
// assert its own fixture rather than the language. Bind a check in your
|
|
1402
|
+
// language module and assert it here alongside these.`;
|
|
1403
|
+
|
|
1404
|
+
/** The README, assembled from lines to avoid escaping the Markdown code fences. */
|
|
1405
|
+
function readme(composition: InitComposition): string {
|
|
1406
|
+
const multi = composition.grammars.length > 1;
|
|
1407
|
+
const data = composition.heads.includes('data');
|
|
1408
|
+
const primaryExtension = composition.grammars[0].extensions[0];
|
|
1409
|
+
const grammarBullets = composition.grammars.map(
|
|
1410
|
+
grammar => `- \`src/grammar/${grammar.grammarId}.langium\` — the \`${grammar.grammar}\` grammar (.${grammar.extensions.join(', .')})`
|
|
1411
|
+
);
|
|
1412
|
+
const lines = [
|
|
1413
|
+
'# __NAME__',
|
|
1414
|
+
'',
|
|
1415
|
+
'A Hydranium language project scaffolded by `hydranium-cli init`.',
|
|
1416
|
+
'',
|
|
1417
|
+
'## Getting started',
|
|
1418
|
+
'',
|
|
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
|
+
'```bash',
|
|
1434
|
+
'npm install',
|
|
1435
|
+
'npm run langium:generate # generate the AST from the grammar',
|
|
1436
|
+
'npm run build # generate + compile to lib/',
|
|
1437
|
+
'npm test # typecheck the tests, then run them',
|
|
1438
|
+
'```',
|
|
1439
|
+
'',
|
|
1440
|
+
'## Try the framework CLI against it',
|
|
1441
|
+
'',
|
|
1442
|
+
// `npx`, not a bare invocation: `@hydranium/cli` is a devDependency of the
|
|
1443
|
+
// emitted project, so the binary is on PATH inside an npm script and
|
|
1444
|
+
// nowhere else.
|
|
1445
|
+
'```bash',
|
|
1446
|
+
'# Grammar / AST reflection',
|
|
1447
|
+
'npx hydranium-cli reflect --services ./lib/services.js',
|
|
1448
|
+
'',
|
|
1449
|
+
'# Grammar-convention lint (CI gate)',
|
|
1450
|
+
'npx hydranium-cli lint-grammar --services ./lib/services.js',
|
|
1451
|
+
'',
|
|
1452
|
+
'# Validate a workspace of model files (non-zero exit on errors)',
|
|
1453
|
+
'npx hydranium-cli validate --services ./lib/services.js <workspace-dir>',
|
|
1454
|
+
...(data
|
|
1455
|
+
? [
|
|
1456
|
+
'',
|
|
1457
|
+
'# Data-head operations. `--server` is a command line the CLI spawns and then',
|
|
1458
|
+
"# drives the data protocol over the child's stdin/stdout, so it has to name",
|
|
1459
|
+
'# `data-server-main.js` — `main.js` gives stdio to LSP and answers these',
|
|
1460
|
+
'# methods with "Unhandled method".',
|
|
1461
|
+
'#',
|
|
1462
|
+
'# The workspace goes to the ENTRY, not to `--cwd`: `--cwd` re-roots the child,',
|
|
1463
|
+
'# so a relative entry path is refused by name (an absolute one is fine).',
|
|
1464
|
+
'npx hydranium-cli projects --server "node ./lib/data-server-main.js <workspace-dir>"',
|
|
1465
|
+
`npx hydranium-cli query --server "node ./lib/data-server-main.js <workspace-dir>" --uri <file:// URI of a .${primaryExtension} file>`
|
|
1466
|
+
]
|
|
1467
|
+
: []),
|
|
1468
|
+
'```',
|
|
1469
|
+
'',
|
|
1470
|
+
'## Layout',
|
|
1471
|
+
'',
|
|
1472
|
+
...grammarBullets,
|
|
1473
|
+
...(multi ? ['- `src/grammar/common.langium` — shared terminals, imported by each grammar (not a language)'] : []),
|
|
1474
|
+
'- `src/language-server/__PROJECT_ID__-module.ts` — `create__NAME__Services` DI wiring',
|
|
1475
|
+
...composition.grammars.map(
|
|
1476
|
+
grammar =>
|
|
1477
|
+
`- \`src/language-server/${grammar.grammarId}-serializer.ts\` — emits \`${grammar.grammar}\` back to text (the framework defaults this to a throw)`
|
|
1478
|
+
),
|
|
1479
|
+
'- `src/services.ts` — zero-arg `createServices()` for the headless CLI',
|
|
1480
|
+
'- `src/index.ts` — the package entry (`main`): DI factory + generated AST',
|
|
1481
|
+
`- \`src/main.ts\` — starts ${composition.heads.join(' + ')}, the \`__PROJECT_ID__\` bin entry`,
|
|
1482
|
+
...(data
|
|
1483
|
+
? [
|
|
1484
|
+
'- `src/data-server-main.ts` — the data head alone on stdio, the',
|
|
1485
|
+
' `__PROJECT_ID__-data-server` bin entry and the `--server` value the CLI needs'
|
|
1486
|
+
]
|
|
1487
|
+
: []),
|
|
1488
|
+
...composition.grammars
|
|
1489
|
+
.filter(grammar => grammar.diagram)
|
|
1490
|
+
.map(
|
|
1491
|
+
grammar =>
|
|
1492
|
+
`- \`src/glsp/${grammar.grammarId}/\` — the \`${grammar.grammar}\` diagram: type ids, state, storage, submission handler, GModel factory, configuration, create-node handler, DI module`
|
|
1493
|
+
),
|
|
1494
|
+
'- `test/services.test.ts` — the DI tree composes; grows as your language does',
|
|
1495
|
+
'- `test/parsing.test.ts` / `linking.test.ts` / `validating.test.ts` — the three tiers a new language breaks first',
|
|
1496
|
+
'- `test/serialization.test.ts` — each grammar round-trips through its serializer',
|
|
1497
|
+
'- `syntaxes/` — generated TextMate grammar for a VS Code extension (gitignored)',
|
|
1498
|
+
'',
|
|
1499
|
+
...(data
|
|
1500
|
+
? ['The two entry points are `bin` scripts rather than `main` on purpose:']
|
|
1501
|
+
: ['`main.ts` is a `bin` script rather than `main` on purpose:']),
|
|
1502
|
+
'each opens a transport at module scope, so importing one would start a',
|
|
1503
|
+
'server as a side effect. Compose the language through `src/index.ts` instead.',
|
|
1504
|
+
'',
|
|
1505
|
+
'## Three names, and when they diverge',
|
|
1506
|
+
'',
|
|
1507
|
+
...(multi
|
|
1508
|
+
? [
|
|
1509
|
+
'This project holds several grammars, so the tiers have already diverged:',
|
|
1510
|
+
'the project name is the umbrella and each grammar names its own language.'
|
|
1511
|
+
]
|
|
1512
|
+
: [
|
|
1513
|
+
'The scaffold sets all three to the same value, which is right for one',
|
|
1514
|
+
'grammar and stops being right the moment you add a second:'
|
|
1515
|
+
]),
|
|
1516
|
+
'',
|
|
1517
|
+
'| Name | Set by | Generates |',
|
|
1518
|
+
'| --- | --- | --- |',
|
|
1519
|
+
'| project | `--name` / `projectName` | `__NAME__AstReflection`, `__NAME__GeneratedSharedModule` — one set per project |',
|
|
1520
|
+
'| grammar | `--grammar` / `grammar X` | `<Grammar>GeneratedModule`, `<Grammar>LanguageMetaData` — one set per grammar |',
|
|
1521
|
+
'| language id | derived, or `--language-id` | file routing, the `langium-config` entry id |',
|
|
1522
|
+
'',
|
|
1523
|
+
'A further grammar is another `--grammar` (or another entry in',
|
|
1524
|
+
'`langium-config.json` — never a second config file, because `AstReflection`',
|
|
1525
|
+
'is one shared slot) with its own grammar name, while the project name stays',
|
|
1526
|
+
'the umbrella.',
|
|
1527
|
+
'',
|
|
1528
|
+
'Each grammar declares its own entry rule (`<Grammar>Model`) rather than a',
|
|
1529
|
+
'shared `Model`, because one `langium-cli` run over N grammars emits one',
|
|
1530
|
+
'combined `ast.ts` and two `Model` interfaces would collide in it.',
|
|
1531
|
+
'',
|
|
1532
|
+
...(multi
|
|
1533
|
+
? [
|
|
1534
|
+
'The scaffolded grammars are independent — each imports `common.langium`',
|
|
1535
|
+
'for its terminals and nothing else. Which grammar may reference which is',
|
|
1536
|
+
'a modelling decision, so add an `import` between them when you know the',
|
|
1537
|
+
'direction, and remember that references point one way.',
|
|
1538
|
+
''
|
|
1539
|
+
]
|
|
1540
|
+
: []),
|
|
1541
|
+
'The `langium` / `langium-cli` versions are pinned exactly rather than',
|
|
1542
|
+
'ranged: the framework treats `langium` and its `vscode-*` chain as one',
|
|
1543
|
+
'atomic set and depends on a single physical copy, so a floating range can',
|
|
1544
|
+
'silently resolve a second one.',
|
|
1545
|
+
'',
|
|
1546
|
+
'## If your repo gates license headers',
|
|
1547
|
+
'',
|
|
1548
|
+
'The emitted `.ts` files carry no copyright header — the scaffold cannot know',
|
|
1549
|
+
'your license. Run your own header tool over `src/` and `test/` after',
|
|
1550
|
+
'scaffolding. The file-purpose comments are `//` runs rather than `/** */`',
|
|
1551
|
+
'blocks precisely so that a tool which REPLACES the leading block comment',
|
|
1552
|
+
'does not silently delete them.',
|
|
1553
|
+
'',
|
|
1554
|
+
`The ${data ? 'two `bin` entries' : '`bin` entry'} start with a \`#!\` line, and it has to STAY the first`,
|
|
1555
|
+
'line: a header tool that prepends unconditionally leaves the shell reading',
|
|
1556
|
+
'the license comment as a script, which is what a linked binary then runs.',
|
|
1557
|
+
''
|
|
1558
|
+
];
|
|
1559
|
+
return project(lines.join('\n'), composition);
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
/**
|
|
1563
|
+
* The GLSP files for ONE grammar's diagram.
|
|
1564
|
+
*
|
|
1565
|
+
* Per grammar rather than per project because a diagram type has exactly one
|
|
1566
|
+
* grammar — `AbstractHydraniumGlspDiagramModule.declareLanguage()` returns that
|
|
1567
|
+
* grammar's `LanguageMetaData`, and it is bound on the SESSION container so a
|
|
1568
|
+
* grammar with no diagram can sit on the same server.
|
|
1569
|
+
*
|
|
1570
|
+
* The index and the computed-bounds handler are framework classes used
|
|
1571
|
+
* unmodified; storage and the submission handler are thin typed subclasses,
|
|
1572
|
+
* which is where an adopter's own load/save and log formatting land.
|
|
1573
|
+
*
|
|
1574
|
+
* The set is EDITABLE rather than read-only: one create-node handler, registered
|
|
1575
|
+
* on the module. GLSP's own `DiagramModule` binds no handler that mutates a
|
|
1576
|
+
* source model, so without it the head renders and answers while telling the
|
|
1577
|
+
* client it may change nothing — which reads as a broken diagram rather than as a
|
|
1578
|
+
* deliberate viewer.
|
|
1579
|
+
*/
|
|
1580
|
+
function glspFiles(composition: InitComposition, grammar: InitGrammarNames): InitFile[] {
|
|
1581
|
+
const columns = columnsFor(composition);
|
|
1582
|
+
const upper = screamingSnake(grammar.grammarId);
|
|
1583
|
+
const dir = `src/glsp/${grammar.grammarId}`;
|
|
1584
|
+
const render = (text: string): string => grammarTier(project(text, composition), grammar);
|
|
1585
|
+
|
|
1586
|
+
const types = `// GLSP diagram-type and element-type ids for the __GRAMMAR__ diagram.
|
|
1587
|
+
//
|
|
1588
|
+
// **Authoritative half of a client/server contract.** Every id here has to be
|
|
1589
|
+
// registered on the client too: sprotty's registries are exact-key maps with no
|
|
1590
|
+
// prefix fallback, so an id the client does not know yields an element with none
|
|
1591
|
+
// of a node's features, rendered by \`MissingView\` with only a
|
|
1592
|
+
// \`no registered view for type '…'\` console warning. Nothing fails server-side.
|
|
1593
|
+
//
|
|
1594
|
+
// The ids stay namespaced under the GLSP defaults (\`node:\` / \`edge:\`) so a
|
|
1595
|
+
// reader can tell a node id from an edge id at a glance.
|
|
1596
|
+
|
|
1597
|
+
import { DefaultTypes } from '@eclipse-glsp/server';
|
|
1598
|
+
|
|
1599
|
+
/**
|
|
1600
|
+
* The GLSP diagram type — the string GLSP routes every per-diagram-type request
|
|
1601
|
+
* by. Mirrored by the client; a mismatch silently DROPS the request rather than
|
|
1602
|
+
* reporting an unknown diagram type. Same value as the language id, so the two
|
|
1603
|
+
* cannot drift apart as a diagram gains element types.
|
|
1604
|
+
*/
|
|
1605
|
+
export const ${upper}_DIAGRAM_TYPE = '__LANGUAGE_ID__';
|
|
1606
|
+
|
|
1607
|
+
/** A \`__NODE_RULE__\` — the starter grammar's one named, referenceable node. */
|
|
1608
|
+
export const ${upper}_NODE_TYPE = \`\${DefaultTypes.NODE}:__GRAMMAR_ID__-node\`;
|
|
1609
|
+
|
|
1610
|
+
/** A resolved \`target\` reference, drawn as a connection between two nodes. */
|
|
1611
|
+
export const ${upper}_EDGE_TYPE = \`\${DefaultTypes.EDGE}:__GRAMMAR_ID__-target\`;
|
|
1612
|
+
`;
|
|
1613
|
+
|
|
1614
|
+
const state = `// GLSP state for the __GRAMMAR__ diagram.
|
|
1615
|
+
//
|
|
1616
|
+
// \`FullTextHydraniumGlspState\` is the simplest of the framework source-model
|
|
1617
|
+
// strategies (the others project a structured transfer model, over one document
|
|
1618
|
+
// or several): the source model is the whole document text, serialised through
|
|
1619
|
+
// the per-URI \`Serializer\` and round-tripped by re-parsing. Both seams resolve
|
|
1620
|
+
// through shared services, so narrowing the root type is all an adopter adds.
|
|
1621
|
+
//
|
|
1622
|
+
// It cannot field-merge — every concurrent edit on a whole-document model is a
|
|
1623
|
+
// same-document collision, so undo / redo degrade to drop-on-divergence. Move to
|
|
1624
|
+
// \`ReconcilingTransferHydraniumGlspState\` when you need field-level undo.
|
|
1625
|
+
|
|
1626
|
+
import { FullTextHydraniumGlspState } from '@hydranium/glsp-server';
|
|
1627
|
+
import { injectable } from 'inversify';
|
|
1628
|
+
import type { __ENTRY_RULE__ } from '../../language-server/ast.js';
|
|
1629
|
+
|
|
1630
|
+
@injectable()
|
|
1631
|
+
export class __GRAMMAR__GlspState extends FullTextHydraniumGlspState<__ENTRY_RULE__> {}
|
|
1632
|
+
`;
|
|
1633
|
+
|
|
1634
|
+
const storage = `// Source-model storage for the __GRAMMAR__ diagram, inheriting both framework
|
|
1635
|
+
// defaults: \`loadSourceModel\` (open + settle + \`setSourceRoot\`) and
|
|
1636
|
+
// \`saveSourceModel\` (through \`ModelService.save\` → the per-URI \`Serializer\` →
|
|
1637
|
+
// the multi-client text store → \`WritableFileSystemProvider\`).
|
|
1638
|
+
//
|
|
1639
|
+
// The subclass exists so a bespoke load or save has a stable place to land.
|
|
1640
|
+
|
|
1641
|
+
import { type FullTextSourceModel, HydraniumGlspStorage } from '@hydranium/glsp-server';
|
|
1642
|
+
import { injectable } from 'inversify';
|
|
1643
|
+
import type { __ENTRY_RULE__ } from '../../language-server/ast.js';
|
|
1644
|
+
|
|
1645
|
+
@injectable()
|
|
1646
|
+
export class __GRAMMAR__GlspStorage extends HydraniumGlspStorage<__ENTRY_RULE__, FullTextSourceModel> {}
|
|
1647
|
+
`;
|
|
1648
|
+
|
|
1649
|
+
const submission = `// Submission handler for the __GRAMMAR__ diagram.
|
|
1650
|
+
//
|
|
1651
|
+
// Inherits the framework's \`readyEvent = IntegrityService.SettledState\`, which
|
|
1652
|
+
// is load-bearing rather than incidental: the GModel factory resolves
|
|
1653
|
+
// \`target.ref\`, so it needs a fully-linked AST. Without the gate those reads
|
|
1654
|
+
// fire mid-build and warn about resolution before scopes are computed.
|
|
1655
|
+
//
|
|
1656
|
+
// Only \`formatSourceRoot\` is overridden, so the submit log names the model and
|
|
1657
|
+
// its node count instead of a bare \`$type\`.
|
|
1658
|
+
|
|
1659
|
+
import { ModelState } from '@eclipse-glsp/server';
|
|
1660
|
+
import { type FullTextSourceModel, HydraniumGlspSubmissionHandler } from '@hydranium/glsp-server';
|
|
1661
|
+
import { inject, injectable } from 'inversify';
|
|
1662
|
+
import type { __ENTRY_RULE__ } from '../../language-server/ast.js';
|
|
1663
|
+
import type { __GRAMMAR__GlspState } from './state.js';
|
|
1664
|
+
|
|
1665
|
+
@injectable()
|
|
1666
|
+
export class __GRAMMAR__SubmissionHandler extends HydraniumGlspSubmissionHandler<__ENTRY_RULE__, FullTextSourceModel> {
|
|
1667
|
+
@inject(ModelState) declare protected modelState: __GRAMMAR__GlspState;
|
|
1668
|
+
|
|
1669
|
+
protected override formatSourceRoot(root: __ENTRY_RULE__ | undefined): string {
|
|
1670
|
+
return root ? \`__ENTRY_RULE__ nodes=\${root.nodes.length}\` : 'none';
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
`;
|
|
1674
|
+
|
|
1675
|
+
const factory = `// AST → GModel for the __GRAMMAR__ diagram: one node per \`__NODE_RULE__\`, one
|
|
1676
|
+
// edge per resolved \`target\` reference.
|
|
1677
|
+
//
|
|
1678
|
+
// Ids come from the index rather than being composed here, so the id strategy
|
|
1679
|
+
// stays in one place. Nodes are emitted before edges, because a GLSP edge
|
|
1680
|
+
// pointing at an id that does not exist fails client-side with a far less
|
|
1681
|
+
// obvious error than a missing edge. An unresolved reference is SKIPPED rather
|
|
1682
|
+
// than treated as an error: a dangling reference is ordinary editing state, and
|
|
1683
|
+
// the LSP head already reports it as a diagnostic.
|
|
1684
|
+
|
|
1685
|
+
import { DefaultTypes, GEdge, GGraph, GLabel, type GModelFactory, GNode, ModelState } from '@eclipse-glsp/server';
|
|
1686
|
+
import { inject, injectable } from 'inversify';
|
|
1687
|
+
import type { __ENTRY_RULE__ } from '../../language-server/ast.js';
|
|
1688
|
+
import { ${upper}_EDGE_TYPE, ${upper}_NODE_TYPE } from './types.js';
|
|
1689
|
+
import type { __GRAMMAR__GlspState } from './state.js';
|
|
1690
|
+
|
|
1691
|
+
@injectable()
|
|
1692
|
+
export class __GRAMMAR__GModelFactory implements GModelFactory {
|
|
1693
|
+
@inject(ModelState) protected readonly modelState!: __GRAMMAR__GlspState;
|
|
1694
|
+
|
|
1695
|
+
createModel(): void {
|
|
1696
|
+
const root = this.modelState.sourceRoot;
|
|
1697
|
+
const graph = GGraph.builder().id(this.modelState.sourceUri).build();
|
|
1698
|
+
if (root) {
|
|
1699
|
+
this.buildGraph(root, graph);
|
|
1700
|
+
}
|
|
1701
|
+
this.modelState.updateRoot(graph);
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
protected buildGraph(root: __ENTRY_RULE__, graph: GGraph): void {
|
|
1705
|
+
for (const node of root.nodes) {
|
|
1706
|
+
const id = this.modelState.index.createId(node);
|
|
1707
|
+
graph.children.push(
|
|
1708
|
+
GNode.builder()
|
|
1709
|
+
.id(id)
|
|
1710
|
+
.type(${upper}_NODE_TYPE)
|
|
1711
|
+
.add(GLabel.builder().id(\`\${id}_name\`).text(node.name).type(DefaultTypes.LABEL).build())
|
|
1712
|
+
.build()
|
|
1713
|
+
);
|
|
1714
|
+
}
|
|
1715
|
+
for (const node of root.nodes) {
|
|
1716
|
+
const target = node.target?.ref;
|
|
1717
|
+
if (!target) {
|
|
1718
|
+
continue;
|
|
1719
|
+
}
|
|
1720
|
+
const sourceId = this.modelState.index.createId(node);
|
|
1721
|
+
graph.children.push(
|
|
1722
|
+
GEdge.builder()
|
|
1723
|
+
.id(\`\${sourceId}_target\`)
|
|
1724
|
+
.type(${upper}_EDGE_TYPE)
|
|
1725
|
+
.sourceId(sourceId)
|
|
1726
|
+
.targetId(this.modelState.index.createId(target))
|
|
1727
|
+
.build()
|
|
1728
|
+
);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
`;
|
|
1733
|
+
|
|
1734
|
+
const configuration = `// Diagram configuration for the __GRAMMAR__ diagram.
|
|
1735
|
+
//
|
|
1736
|
+
// **Every hint is \`false\`, and that is not the same as read-only.** The starter
|
|
1737
|
+
// __GRAMMAR__CreateNodeOperationHandler makes this diagram editable through the
|
|
1738
|
+
// tool palette, which GLSP assembles from the create handlers' trigger actions —
|
|
1739
|
+
// there is no \`creatable\` hint. What the hints govern is delete / reparent /
|
|
1740
|
+
// reposition / resize, and nothing backs those, so declaring one would offer a
|
|
1741
|
+
// gesture whose operation the server rejects: a worse failure than the tool being
|
|
1742
|
+
// absent. Turn a hint on in the same change that adds its handler.
|
|
1743
|
+
//
|
|
1744
|
+
// \`needsClientLayout\` is \`true\` and \`layoutKind\` is \`NONE\`: the starter grammar
|
|
1745
|
+
// persists no bounds, so the client measures and places everything. That is also
|
|
1746
|
+
// why \`ChangeBoundsOperation\` is a poor second handler to add — with nowhere in
|
|
1747
|
+
// the grammar to write a position, a move never reaches the text and is lost on
|
|
1748
|
+
// the next reload.
|
|
1749
|
+
|
|
1750
|
+
import { type GModelElementConstructor } from '@eclipse-glsp/graph';
|
|
1751
|
+
${importList(
|
|
1752
|
+
['type DiagramConfiguration', 'type EdgeTypeHint', 'ServerLayoutKind', 'type ShapeTypeHint', 'getDefaultMapping'],
|
|
1753
|
+
'@eclipse-glsp/server',
|
|
1754
|
+
columns
|
|
1755
|
+
)}
|
|
1756
|
+
import { injectable } from 'inversify';
|
|
1757
|
+
import { ${upper}_EDGE_TYPE, ${upper}_NODE_TYPE } from './types.js';
|
|
1758
|
+
|
|
1759
|
+
@injectable()
|
|
1760
|
+
export class __GRAMMAR__DiagramConfiguration implements DiagramConfiguration {
|
|
1761
|
+
readonly layoutKind: ServerLayoutKind = ServerLayoutKind.NONE;
|
|
1762
|
+
readonly needsClientLayout: boolean = true;
|
|
1763
|
+
readonly animatedUpdate: boolean = false;
|
|
1764
|
+
|
|
1765
|
+
readonly typeMapping: Map<string, GModelElementConstructor> = getDefaultMapping();
|
|
1766
|
+
|
|
1767
|
+
readonly shapeTypeHints: ShapeTypeHint[] = [
|
|
1768
|
+
{
|
|
1769
|
+
elementTypeId: ${upper}_NODE_TYPE,
|
|
1770
|
+
deletable: false,
|
|
1771
|
+
reparentable: false,
|
|
1772
|
+
repositionable: false,
|
|
1773
|
+
resizable: false
|
|
1774
|
+
}
|
|
1775
|
+
];
|
|
1776
|
+
|
|
1777
|
+
readonly edgeTypeHints: EdgeTypeHint[] = [
|
|
1778
|
+
{
|
|
1779
|
+
elementTypeId: ${upper}_EDGE_TYPE,
|
|
1780
|
+
deletable: false,
|
|
1781
|
+
repositionable: false,
|
|
1782
|
+
routable: false,
|
|
1783
|
+
sourceElementTypeIds: [${upper}_NODE_TYPE],
|
|
1784
|
+
targetElementTypeIds: [${upper}_NODE_TYPE]
|
|
1785
|
+
}
|
|
1786
|
+
];
|
|
1787
|
+
}
|
|
1788
|
+
`;
|
|
1789
|
+
|
|
1790
|
+
const createHandler = `// The starter operation handler for the __GRAMMAR__ diagram: the tool-palette
|
|
1791
|
+
// entry that creates a \`__NODE_RULE__\`.
|
|
1792
|
+
//
|
|
1793
|
+
// **Deleting this file and its \`configureOperationHandlers\` registration in
|
|
1794
|
+
// \`diagram-module.ts\` gives a read-only viewer**, and nothing else has to change
|
|
1795
|
+
// — creation is offered through the palette rather than through a type hint, so
|
|
1796
|
+
// every hint in \`diagram-configuration.ts\` is already \`false\`. The scaffold emits
|
|
1797
|
+
// the editable direction because that asymmetry runs one way: editable →
|
|
1798
|
+
// read-only is a deletion the compiler checks, while read-only → editable is
|
|
1799
|
+
// authoring against a seam you have not used yet.
|
|
1800
|
+
//
|
|
1801
|
+
// **It composes TEXT rather than mutating the AST.** The source model of
|
|
1802
|
+
// \`FullTextHydraniumGlspState\` is the document text, and reading it back through
|
|
1803
|
+
// \`state.sourceModel\` serialises the AST through the per-URI \`Serializer\`, which
|
|
1804
|
+
// this scaffold does not bind — so that getter throws until you do. Appending a
|
|
1805
|
+
// declaration to the text the parser last read needs no serializer, which is what
|
|
1806
|
+
// makes a scaffolded diagram editable on day one. Bind a \`Serializer\` at
|
|
1807
|
+
// \`services.serializer.Serializer\` and this becomes a
|
|
1808
|
+
// \`HydraniumGlspRecordingCommand\` over \`state.sourceModel\` instead — the same
|
|
1809
|
+
// binding the diagram's own save action needs.
|
|
1810
|
+
//
|
|
1811
|
+
// **The drop location is discarded.** \`needsClientLayout\` is \`true\` and the
|
|
1812
|
+
// starter grammar persists no bounds, so there is nowhere to put a coordinate: the
|
|
1813
|
+
// node is appended at the end of the document and the client places it.
|
|
1814
|
+
//
|
|
1815
|
+
// Like \`types.ts\`, \`gmodel-factory.ts\` and \`diagram-configuration.ts\`, this file
|
|
1816
|
+
// knows the starter grammar's concrete syntax — the \`node\` keyword below is that
|
|
1817
|
+
// grammar's. Replacing the grammar means replacing these four together.
|
|
1818
|
+
|
|
1819
|
+
import { type Command, type CreateNodeOperation, JsonCreateNodeOperationHandler, type MaybePromise } from '@eclipse-glsp/server';
|
|
1820
|
+
import { findNextUnique } from '@hydranium/protocol';
|
|
1821
|
+
import { injectable } from 'inversify';
|
|
1822
|
+
import { ${upper}_NODE_TYPE } from './types.js';
|
|
1823
|
+
import type { __GRAMMAR__GlspState } from './state.js';
|
|
1824
|
+
|
|
1825
|
+
/** Proposed name for a new node, uniquified against the ones the document already has. */
|
|
1826
|
+
const NODE_NAME_STEM = 'Node';
|
|
1827
|
+
|
|
1828
|
+
@injectable()
|
|
1829
|
+
export class __GRAMMAR__CreateNodeOperationHandler extends JsonCreateNodeOperationHandler {
|
|
1830
|
+
declare protected modelState: __GRAMMAR__GlspState;
|
|
1831
|
+
|
|
1832
|
+
/** The palette's word for the thing it creates, so a noun rather than an action. */
|
|
1833
|
+
override readonly label = '__NODE_RULE__';
|
|
1834
|
+
elementTypeIds = [${upper}_NODE_TYPE];
|
|
1835
|
+
|
|
1836
|
+
override createCommand(operation: CreateNodeOperation): MaybePromise<Command | undefined> {
|
|
1837
|
+
if (!this.elementTypeIds.includes(operation.elementTypeId)) {
|
|
1838
|
+
return undefined;
|
|
1839
|
+
}
|
|
1840
|
+
const state = this.modelState;
|
|
1841
|
+
const before = this.documentText();
|
|
1842
|
+
const after = this.withNode(
|
|
1843
|
+
before,
|
|
1844
|
+
findNextUnique(
|
|
1845
|
+
NODE_NAME_STEM,
|
|
1846
|
+
state.sourceRoot.nodes.map(node => node.name)
|
|
1847
|
+
)
|
|
1848
|
+
);
|
|
1849
|
+
// Whole-document undo, which is all a full-text source model can offer: it
|
|
1850
|
+
// has exactly one field, so there is nothing to merge a concurrent edit into.
|
|
1851
|
+
return {
|
|
1852
|
+
execute: () => state.updateSourceModel({ text: after }),
|
|
1853
|
+
undo: () => state.updateSourceModel({ text: before }),
|
|
1854
|
+
redo: () => state.updateSourceModel({ text: after })
|
|
1855
|
+
};
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
/** The text the captured source root was parsed from — the baseline an edit appends to. */
|
|
1859
|
+
protected documentText(): string {
|
|
1860
|
+
return this.modelState.sourceRoot.$document?.textDocument.getText() ?? '';
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
/** \`text\` with one more node declaration, under exactly one trailing newline. */
|
|
1864
|
+
protected withNode(text: string, name: string): string {
|
|
1865
|
+
const body = text.trimEnd();
|
|
1866
|
+
const declaration = \`node \${name}\`;
|
|
1867
|
+
return body.length === 0 ? \`\${declaration}\\n\` : \`\${body}\\n\${declaration}\\n\`;
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
`;
|
|
1871
|
+
|
|
1872
|
+
const module = `// GLSP diagram module for __GRAMMAR__ — the DI wiring of one diagram type.
|
|
1873
|
+
//
|
|
1874
|
+
// \`declareLanguage\` is the multi-grammar seam: it binds the grammar this diagram
|
|
1875
|
+
// edits on the SESSION container, so \`modelState.diagramLanguage\` and the
|
|
1876
|
+
// per-language lookups resolve to __LANGUAGE_ID__. That is what stops one
|
|
1877
|
+
// diagram type from fighting over a process-wide binding when a project holds
|
|
1878
|
+
// several grammars — a grammar with no diagram is registered on the same server.
|
|
1879
|
+
//
|
|
1880
|
+
// \`configureActionHandlers\` REBINDS rather than adds: GLSP's own
|
|
1881
|
+
// \`DiagramModule\` already registers \`ComputedBoundsActionHandler\`, and two
|
|
1882
|
+
// handlers for one action would both run.
|
|
1883
|
+
//
|
|
1884
|
+
// \`configureOperationHandlers\` ADDS: GLSP's default pair
|
|
1885
|
+
// (\`CompoundOperationHandler\` and \`LayoutOperationHandler\`) mutates no source
|
|
1886
|
+
// model, so the starter create handler is what makes this diagram editable at all.
|
|
1887
|
+
|
|
1888
|
+
${importList(
|
|
1889
|
+
[
|
|
1890
|
+
'ActionHandlerConstructor',
|
|
1891
|
+
'BindingTarget',
|
|
1892
|
+
'DiagramConfiguration',
|
|
1893
|
+
'GModelFactory',
|
|
1894
|
+
'GModelIndex',
|
|
1895
|
+
'InstanceMultiBinding',
|
|
1896
|
+
'ModelState',
|
|
1897
|
+
'ModelSubmissionHandler',
|
|
1898
|
+
'OperationHandlerConstructor',
|
|
1899
|
+
'SourceModelStorage'
|
|
1900
|
+
],
|
|
1901
|
+
'@eclipse-glsp/server',
|
|
1902
|
+
columns,
|
|
1903
|
+
true
|
|
1904
|
+
)}
|
|
1905
|
+
import { ComputedBoundsActionHandler } from '@eclipse-glsp/server';
|
|
1906
|
+
import type { LanguageMetaData } from '@hydranium/langium';
|
|
1907
|
+
${importList(
|
|
1908
|
+
['HydraniumGlspComputedBoundsActionHandler', 'AbstractHydraniumGlspDiagramModule', 'HydraniumGlspIndex'],
|
|
1909
|
+
'@hydranium/glsp-server',
|
|
1910
|
+
columns
|
|
1911
|
+
)}
|
|
1912
|
+
import { __GRAMMAR__LanguageMetaData } from '../../language-server/generated/module.js';
|
|
1913
|
+
import { __GRAMMAR__CreateNodeOperationHandler } from './create-node-operation-handler.js';
|
|
1914
|
+
import { __GRAMMAR__DiagramConfiguration } from './diagram-configuration.js';
|
|
1915
|
+
import { __GRAMMAR__GModelFactory } from './gmodel-factory.js';
|
|
1916
|
+
import { __GRAMMAR__GlspState } from './state.js';
|
|
1917
|
+
import { __GRAMMAR__GlspStorage } from './storage.js';
|
|
1918
|
+
import { __GRAMMAR__SubmissionHandler } from './submission-handler.js';
|
|
1919
|
+
import { ${upper}_DIAGRAM_TYPE } from './types.js';
|
|
1920
|
+
|
|
1921
|
+
export class __GRAMMAR__DiagramModule extends AbstractHydraniumGlspDiagramModule {
|
|
1922
|
+
readonly diagramType = ${upper}_DIAGRAM_TYPE;
|
|
1923
|
+
|
|
1924
|
+
protected override declareLanguage(): LanguageMetaData {
|
|
1925
|
+
return __GRAMMAR__LanguageMetaData;
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
protected override bindModelState(): BindingTarget<ModelState> {
|
|
1929
|
+
return { service: __GRAMMAR__GlspState };
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
protected override bindSourceModelStorage(): BindingTarget<SourceModelStorage> {
|
|
1933
|
+
return { service: __GRAMMAR__GlspStorage };
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
protected override bindModelSubmissionHandler(): BindingTarget<ModelSubmissionHandler> {
|
|
1937
|
+
return { service: __GRAMMAR__SubmissionHandler };
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
protected override bindDiagramConfiguration(): BindingTarget<DiagramConfiguration> {
|
|
1941
|
+
return { service: __GRAMMAR__DiagramConfiguration };
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
protected override bindGModelFactory(): BindingTarget<GModelFactory> {
|
|
1945
|
+
return { service: __GRAMMAR__GModelFactory };
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
/** The framework index unmodified — it keys elements by name, with a positional fallback. */
|
|
1949
|
+
protected override bindGModelIndex(): BindingTarget<GModelIndex> {
|
|
1950
|
+
return { service: HydraniumGlspIndex };
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
protected override configureActionHandlers(binding: InstanceMultiBinding<ActionHandlerConstructor>): void {
|
|
1954
|
+
super.configureActionHandlers(binding);
|
|
1955
|
+
binding.rebind(ComputedBoundsActionHandler, HydraniumGlspComputedBoundsActionHandler);
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
protected override configureOperationHandlers(binding: InstanceMultiBinding<OperationHandlerConstructor>): void {
|
|
1959
|
+
super.configureOperationHandlers(binding);
|
|
1960
|
+
binding.add(__GRAMMAR__CreateNodeOperationHandler);
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
`;
|
|
1964
|
+
|
|
1965
|
+
return [
|
|
1966
|
+
{ path: `${dir}/types.ts`, content: render(types) },
|
|
1967
|
+
{ path: `${dir}/state.ts`, content: render(state) },
|
|
1968
|
+
{ path: `${dir}/storage.ts`, content: render(storage) },
|
|
1969
|
+
{ path: `${dir}/submission-handler.ts`, content: render(submission) },
|
|
1970
|
+
{ path: `${dir}/gmodel-factory.ts`, content: render(factory) },
|
|
1971
|
+
{ path: `${dir}/diagram-configuration.ts`, content: render(configuration) },
|
|
1972
|
+
{ path: `${dir}/create-node-operation-handler.ts`, content: render(createHandler) },
|
|
1973
|
+
{ path: `${dir}/diagram-module.ts`, content: render(module) }
|
|
1974
|
+
];
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
/**
|
|
1978
|
+
* Every file the scaffold emits, fully rendered.
|
|
1979
|
+
*
|
|
1980
|
+
* Grammar-count-dependent emissions live here rather than in a template string:
|
|
1981
|
+
* one `.langium` file per grammar, and the shared `common.langium` fragment only
|
|
1982
|
+
* when there is more than one grammar to share it.
|
|
1983
|
+
*/
|
|
1984
|
+
export function buildInitTemplates(composition: InitComposition): InitFile[] {
|
|
1985
|
+
const multi = composition.grammars.length > 1;
|
|
1986
|
+
const grammarFiles: InitFile[] = composition.grammars.map(grammar => ({
|
|
1987
|
+
path: `src/grammar/${grammar.grammarId}.langium`,
|
|
1988
|
+
content: render(grammarFile(multi), composition, grammar)
|
|
1989
|
+
}));
|
|
1990
|
+
if (multi) {
|
|
1991
|
+
grammarFiles.unshift({ path: 'src/grammar/common.langium', content: COMMON_GRAMMAR });
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
const diagramFiles = composition.grammars.filter(grammar => grammar.diagram).flatMap(grammar => glspFiles(composition, grammar));
|
|
1995
|
+
|
|
1996
|
+
return [
|
|
1997
|
+
{ path: 'package.json', content: packageJson(composition) },
|
|
1998
|
+
{ path: 'langium-config.json', content: langiumConfig(composition) },
|
|
1999
|
+
{ path: 'tsconfig.json', content: tsconfigJson(composition) },
|
|
2000
|
+
{ path: 'tsconfig.test.json', content: TSCONFIG_TEST },
|
|
2001
|
+
{ path: 'vitest.config.ts', content: VITEST_CONFIG },
|
|
2002
|
+
{
|
|
2003
|
+
path: '.gitignore',
|
|
2004
|
+
content: composition.packaging.workspace === undefined ? GITIGNORE : GITIGNORE_WORKSPACE_MEMBER
|
|
2005
|
+
},
|
|
2006
|
+
{ path: 'README.md', content: readme(composition) },
|
|
2007
|
+
...grammarFiles,
|
|
2008
|
+
{ path: `src/language-server/${composition.projectId}-module.ts`, content: moduleFile(composition) },
|
|
2009
|
+
{ path: 'src/language-server/ast.ts', content: render(AST, composition, composition.grammars[0]) },
|
|
2010
|
+
...composition.grammars.map(grammar => ({
|
|
2011
|
+
path: `src/language-server/${grammar.grammarId}-serializer.ts`,
|
|
2012
|
+
content: render(SERIALIZER, composition, grammar)
|
|
2013
|
+
})),
|
|
2014
|
+
{ path: 'src/index.ts', content: indexFile(composition) },
|
|
2015
|
+
{ path: 'src/services.ts', content: project(SERVICES, composition) },
|
|
2016
|
+
{ path: 'src/main.ts', content: mainFile(composition) },
|
|
2017
|
+
...(composition.heads.includes('data') ? [{ path: 'src/data-server-main.ts', content: dataServerMainFile(composition) }] : []),
|
|
2018
|
+
...(portCommandNames(composition).length ? [{ path: 'src/head-ports.ts', content: headPortsFile(composition) }] : []),
|
|
2019
|
+
...diagramFiles,
|
|
2020
|
+
{ path: 'test/services.test.ts', content: servicesTest(composition) },
|
|
2021
|
+
{ path: 'test/parsing.test.ts', content: grammarTest(composition, PARSING_HEADER, [], PARSING_SUITE) },
|
|
2022
|
+
{
|
|
2023
|
+
path: 'test/linking.test.ts',
|
|
2024
|
+
content: grammarTest(composition, LINKING_HEADER, ["import { AstUtils } from '@hydranium/langium';"], LINKING_SUITE)
|
|
2025
|
+
},
|
|
2026
|
+
{
|
|
2027
|
+
path: 'test/validating.test.ts',
|
|
2028
|
+
content: grammarTest(
|
|
2029
|
+
composition,
|
|
2030
|
+
VALIDATING_HEADER,
|
|
2031
|
+
["import { DiagnosticSeverity } from 'vscode-languageserver';"],
|
|
2032
|
+
VALIDATING_SUITE
|
|
2033
|
+
)
|
|
2034
|
+
},
|
|
2035
|
+
{ path: 'test/serialization.test.ts', content: serializationTest(composition) }
|
|
2036
|
+
];
|
|
2037
|
+
}
|