@hydranium/cli 1.0.0-next.304 → 1.0.0-next.305
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/lib/commands/init-templates.d.ts.map +1 -1
- package/lib/commands/init-templates.js +80 -484
- package/lib/commands/init-templates.js.map +1 -1
- package/package.json +12 -11
- package/src/commands/init-templates.ts +84 -499
- package/templates/server/gitignore +6 -0
- package/templates/server/src/glsp/__GRAMMAR_ID__/create-node-operation-handler.ts +61 -0
- package/templates/server/src/glsp/__GRAMMAR_ID__/gmodel-factory.ts +57 -0
- package/templates/server/src/glsp/__GRAMMAR_ID__/state.ts +18 -0
- package/templates/server/src/glsp/__GRAMMAR_ID__/storage.ts +16 -0
- package/templates/server/src/glsp/__GRAMMAR_ID__/submission-handler.ts +24 -0
- package/templates/server/src/glsp/__GRAMMAR_ID__/types.ts +26 -0
- package/templates/server/src/grammar/common.langium +12 -0
- package/templates/server/src/language-server/__GRAMMAR_ID__-serializer.ts +53 -0
- package/templates/server/src/language-server/ast.ts +23 -0
- package/templates/server/src/services.ts +12 -0
- package/templates/server/test/__GRAMMAR_ID__-diagram.test.ts +68 -0
- package/templates/server/test/tsconfig.json +4 -0
- package/templates/server/tsconfig.test.json +10 -0
- package/templates/server/vitest.config.ts +14 -0
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
* Drives the PER-LANGUAGE generated symbols (`<grammar>GeneratedModule`,
|
|
30
30
|
* `<grammar>LanguageMetaData`).
|
|
31
31
|
* - `__GRAMMAR_ID__` — kebab grammar id: the `.langium` filename.
|
|
32
|
+
* - `__GRAMMAR_ID_CONST__` — SCREAMING_SNAKE grammar id, prefixing the
|
|
33
|
+
* diagram's type-id constants.
|
|
32
34
|
* - `__LANGUAGE_ID__` — kebab routing key: the `langium-config` entry id and
|
|
33
35
|
* its TextMate output.
|
|
34
36
|
* - `__EXTENSION__` — the grammar's FIRST file extension, without the dot.
|
|
@@ -49,10 +51,12 @@
|
|
|
49
51
|
* generator emits `interface <projectName>Element` as the base type every rule
|
|
50
52
|
* extends, which a rule of that name would collide with.
|
|
51
53
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
54
|
+
* A file that is fixed text with tokens lives in the `templates/` tree the
|
|
55
|
+
* package ships beside `lib`, at the path it is emitted at, tokens included
|
|
56
|
+
* (the ignore file without its dot); one computed from the composition is
|
|
57
|
+
* built here. The tree is read synchronously, so `runInit` stays synchronous,
|
|
58
|
+
* and it is neither formatted with the repo's settings nor given a license
|
|
59
|
+
* header, since either would change what `init` emits.
|
|
56
60
|
*
|
|
57
61
|
* Two conventions the templates follow deliberately:
|
|
58
62
|
*
|
|
@@ -72,6 +76,7 @@
|
|
|
72
76
|
* over; the adopter's own formatter settles it.
|
|
73
77
|
*/
|
|
74
78
|
|
|
79
|
+
import { readFileSync } from 'node:fs';
|
|
75
80
|
import { readCliVersion } from '../cli-version.js';
|
|
76
81
|
import type { InitComposition, InitFile, InitGrammarNames, InitHead } from './init.js';
|
|
77
82
|
import type { JsonValue } from './init-workspace.js';
|
|
@@ -130,6 +135,7 @@ function project(text: string, composition: InitComposition): string {
|
|
|
130
135
|
*/
|
|
131
136
|
function grammarTier(text: string, grammar: InitGrammarNames): string {
|
|
132
137
|
return text
|
|
138
|
+
.replace(/__GRAMMAR_ID_CONST__/g, screamingSnake(grammar.grammarId))
|
|
133
139
|
.replace(/__GRAMMAR_ID__/g, grammar.grammarId)
|
|
134
140
|
.replace(/__GRAMMAR__/g, grammar.grammar)
|
|
135
141
|
.replace(/__LANGUAGE_ID__/g, grammar.languageId)
|
|
@@ -143,6 +149,24 @@ function render(text: string, composition: InitComposition, grammar: InitGrammar
|
|
|
143
149
|
return grammarTier(project(text, composition), grammar);
|
|
144
150
|
}
|
|
145
151
|
|
|
152
|
+
/** The template tree, the same two levels up from `src/commands` and `lib/commands`. */
|
|
153
|
+
const TEMPLATES = new URL('../../templates/', import.meta.url);
|
|
154
|
+
|
|
155
|
+
/** One template file's text. */
|
|
156
|
+
function template(path: string): string {
|
|
157
|
+
return readFileSync(new URL(path, TEMPLATES), 'utf-8');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** A `server/` template emitted once per project, rendered with the project tokens. */
|
|
161
|
+
function projectTemplate(path: string, composition: InitComposition): InitFile {
|
|
162
|
+
return { path, content: project(template(`server/${path}`), composition) };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** A `server/` template emitted once per grammar, its path rendered like its content. */
|
|
166
|
+
function grammarTemplate(path: string, composition: InitComposition, grammar: InitGrammarNames): InitFile {
|
|
167
|
+
return { path: render(path, composition, grammar), content: render(template(`server/${path}`), composition, grammar) };
|
|
168
|
+
}
|
|
169
|
+
|
|
146
170
|
/** A comma-separated list of single-quoted TypeScript/JS string literals. */
|
|
147
171
|
function quotedList(values: readonly string[]): string {
|
|
148
172
|
return values.map(value => `'${value}'`).join(', ');
|
|
@@ -540,90 +564,17 @@ function tsconfigJson(composition: InitComposition): string {
|
|
|
540
564
|
return `{\n${extendsLine} "compilerOptions": {\n${body}\n },\n "include": ["src", "src/**/*.json"]\n}\n`;
|
|
541
565
|
}
|
|
542
566
|
|
|
543
|
-
// `isolatedModules` is what makes this check agree with the transform that
|
|
544
|
-
// actually runs the tests. `npm test` is `typecheck:test && vitest run`, and
|
|
545
|
-
// vitest compiles each file on its own — a transform with no type
|
|
546
|
-
// information, which cannot tell a re-exported TYPE from a re-exported value.
|
|
547
|
-
// Without the flag `tsc` accepts `export { SomeType } from './x'` that the
|
|
548
|
-
// transform then emits as a real import of a symbol that does not exist at
|
|
549
|
-
// runtime, so the typecheck passes and the suite fails with a confusing
|
|
550
|
-
// missing-export.
|
|
551
|
-
const TSCONFIG_TEST = `{
|
|
552
|
-
"extends": "./tsconfig.json",
|
|
553
|
-
"compilerOptions": {
|
|
554
|
-
"rootDir": ".",
|
|
555
|
-
"noEmit": true,
|
|
556
|
-
"isolatedModules": true,
|
|
557
|
-
"types": ["node"]
|
|
558
|
-
},
|
|
559
|
-
"include": ["src", "test", "src/**/*.json"]
|
|
560
|
-
}
|
|
561
|
-
`;
|
|
562
|
-
|
|
563
|
-
// Found by name, unlike `tsconfig.test.json`. Under vite 8, vitest's transform
|
|
564
|
-
// applies a tsconfig only to the files its own `include` lists, so without this
|
|
565
|
-
// a test declaring a decorated class fails to load.
|
|
566
|
-
const TEST_DIR_TSCONFIG = `{
|
|
567
|
-
"extends": "../tsconfig.test.json",
|
|
568
|
-
"include": ["../src", ".", "../src/**/*.json"]
|
|
569
|
-
}
|
|
570
|
-
`;
|
|
571
|
-
|
|
572
|
-
const VITEST_CONFIG = `import { defineConfig } from 'vitest/config';
|
|
573
|
-
|
|
574
|
-
// Vitest transforms TypeScript itself and resolves \`.js\` specifiers to their
|
|
575
|
-
// \`.ts\` source, so tests import from \`../src/\` with the same specifiers the
|
|
576
|
-
// compiled output uses. \`include\` is scoped to \`test/\` so \`lib/\` is never
|
|
577
|
-
// scanned; \`npm test\` runs \`typecheck:test\` first, because vitest's
|
|
578
|
-
// transform strips types without checking them.
|
|
579
|
-
|
|
580
|
-
export default defineConfig({
|
|
581
|
-
test: {
|
|
582
|
-
environment: 'node',
|
|
583
|
-
include: ['test/**/*.{test,spec}.ts']
|
|
584
|
-
}
|
|
585
|
-
});
|
|
586
|
-
`;
|
|
587
|
-
|
|
588
|
-
/**
|
|
589
|
-
* The `.gitignore`, written for a workspace member too: its root may not ignore
|
|
590
|
-
* `lib/` (a `yo langium` root ignores `out/`), and a rule the root repeats
|
|
591
|
-
* costs nothing.
|
|
592
|
-
*/
|
|
593
|
-
const GITIGNORE = `node_modules/
|
|
594
|
-
lib/
|
|
595
|
-
*.tsbuildinfo
|
|
596
|
-
|
|
597
|
-
# Langium-generated TextMate grammar (rewritten by every \`langium generate\`).
|
|
598
|
-
syntaxes/
|
|
599
|
-
`;
|
|
600
|
-
|
|
601
|
-
/** The terminals every grammar needs, shared verbatim by the fragment and the lone-grammar case. */
|
|
602
|
-
const TERMINALS = `hidden terminal WS: /\\s+/;
|
|
603
|
-
terminal ID: /[_a-zA-Z][\\w_]*/;
|
|
604
|
-
hidden terminal SL_COMMENT: /\\/\\/[^\\n\\r]*/;
|
|
605
|
-
hidden terminal ML_COMMENT: /\\/\\*[\\s\\S]*?\\*\\//;
|
|
606
|
-
`;
|
|
607
|
-
|
|
608
567
|
/**
|
|
609
|
-
* The
|
|
610
|
-
*
|
|
611
|
-
* The alternative — every grammar declaring its own `WS` / `ID` — lets the token
|
|
612
|
-
* sets drift apart.
|
|
568
|
+
* The terminals every grammar needs: the shared fragment's text after its
|
|
569
|
+
* leading comment run, so a lone grammar declares the same set.
|
|
613
570
|
*/
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
//
|
|
618
|
-
// Keeping the tokens here rather than in one of the languages means a grammar
|
|
619
|
-
// imports \`common\` for its TOKENS and another language only when it needs that
|
|
620
|
-
// language's TYPES — separate reasons that are worth keeping separate.
|
|
621
|
-
|
|
622
|
-
${TERMINALS}`;
|
|
571
|
+
function terminals(): string {
|
|
572
|
+
return template('server/src/grammar/common.langium').replace(/^(\/\/.*\n)*\n/, '');
|
|
573
|
+
}
|
|
623
574
|
|
|
624
575
|
/** One starter grammar. `shared` decides whether it imports the fragment or declares its own terminals. */
|
|
625
576
|
function grammarFile(shared: boolean): string {
|
|
626
|
-
const lexis = shared ? "import './common'\n" : `\n${
|
|
577
|
+
const lexis = shared ? "import './common'\n" : `\n${terminals()}`;
|
|
627
578
|
return `grammar __GRAMMAR__
|
|
628
579
|
${shared ? '\n' + lexis : ''}
|
|
629
580
|
// A minimal starter grammar — replace with your own language. \`__NODE_RULE__\` is
|
|
@@ -812,108 +763,6 @@ ${returnType}
|
|
|
812
763
|
return project(text, composition);
|
|
813
764
|
}
|
|
814
765
|
|
|
815
|
-
const SERVICES = `// Zero-arg service factory for the headless \`hydranium-cli\` tooling: the
|
|
816
|
-
// \`reflect\` / \`lint-grammar\` / \`validate\` subcommands import this via
|
|
817
|
-
// \`--services ./lib/services.js\`. The head wires its own filesystem here.
|
|
818
|
-
//
|
|
819
|
-
// The contract is language-count-agnostic — a second grammar needs no edit here.
|
|
820
|
-
|
|
821
|
-
import { NodeFileSystem } from '@hydranium/core/node';
|
|
822
|
-
import { create__NAME__Services } from './language-server/__PROJECT_ID__-module.js';
|
|
823
|
-
|
|
824
|
-
export function createServices(): ReturnType<typeof create__NAME__Services> {
|
|
825
|
-
return create__NAME__Services({ ...NodeFileSystem });
|
|
826
|
-
}
|
|
827
|
-
`;
|
|
828
|
-
|
|
829
|
-
const AST = `// The language's AST entry point: a re-export of what \`langium generate\`
|
|
830
|
-
// emits, plus anywhere you augment those types.
|
|
831
|
-
//
|
|
832
|
-
// Import the AST from HERE rather than from \`./generated/ast.js\`, so any
|
|
833
|
-
// augmentation below travels with every import. One \`langium-cli\` run over N
|
|
834
|
-
// grammars emits ONE combined AST module sharing one reflection, so a further
|
|
835
|
-
// grammar needs no change here.
|
|
836
|
-
//
|
|
837
|
-
// \`generate:transfer-model\` reads this file as its \`--augmentation-file\`: the
|
|
838
|
-
// generated wire types are derived from the AST *as augmented*, not from the
|
|
839
|
-
// raw generated module. Augment a type here and the transfer model follows.
|
|
840
|
-
//
|
|
841
|
-
// A \`@derived\` property is computed at build time rather than parsed, so it is
|
|
842
|
-
// declared here and populated by an AST-extension contribution:
|
|
843
|
-
//
|
|
844
|
-
// declare module './generated/ast.js' {
|
|
845
|
-
// interface __ENTRY_RULE__ {
|
|
846
|
-
// /** @derived Populated by an \`ast.extensions.computedProperties\` contribution. */
|
|
847
|
-
// readonly _summary?: string;
|
|
848
|
-
// }
|
|
849
|
-
// }
|
|
850
|
-
|
|
851
|
-
export * from './generated/ast.js';
|
|
852
|
-
`;
|
|
853
|
-
|
|
854
|
-
/**
|
|
855
|
-
* The starter serializer — the concrete-syntax emitter for ONE grammar.
|
|
856
|
-
*
|
|
857
|
-
* Emitted per grammar, not per project, because `services.serializer.Serializer`
|
|
858
|
-
* is a per-language slot and a serializer is grammar-shaped by definition. That
|
|
859
|
-
* is also why the framework refuses to default it: its `UnboundSerializer`
|
|
860
|
-
* throws, naming the binding to add, rather than guessing a syntax.
|
|
861
|
-
*/
|
|
862
|
-
const SERIALIZER = `// Concrete-syntax emitter for __GRAMMAR__ — the parser's inverse, turning a
|
|
863
|
-
// model back into text this grammar accepts. Derived from the starter grammar
|
|
864
|
-
// exactly as \`generated/ast.ts\` is, so replacing the grammar replaces this too.
|
|
865
|
-
//
|
|
866
|
-
// Without it every structured write fails: the framework's default binding at
|
|
867
|
-
// \`services.serializer.Serializer\` THROWS, because a concrete syntax is
|
|
868
|
-
// language knowledge no framework can derive. A client session's \`update\` /
|
|
869
|
-
// \`save\`, the data head's \`saveModelDocument\` and a GLSP \`SaveModelAction\`
|
|
870
|
-
// all reach it.
|
|
871
|
-
//
|
|
872
|
-
// \`AbstractSerializer\`'s generic property walk lays out FORMAT-structured
|
|
873
|
-
// output — its YAML and JSON subclasses are what it exists for — and cannot
|
|
874
|
-
// produce a keyword-delimited syntax like \`node a -> b\`. So \`serializeNode\` is
|
|
875
|
-
// a per-\`$type\` emitter here and the two array hooks are unreachable.
|
|
876
|
-
|
|
877
|
-
import { AbstractSerializer } from '@hydranium/core';
|
|
878
|
-
import type { AstNode } from '@hydranium/langium';
|
|
879
|
-
import { type __ENTRY_RULE__, type __NODE_RULE__, is__ENTRY_RULE__, is__NODE_RULE__ } from './ast.js';
|
|
880
|
-
|
|
881
|
-
export class __GRAMMAR__Serializer extends AbstractSerializer<__ENTRY_RULE__> {
|
|
882
|
-
protected override serializeNode(node: AstNode | Record<string, unknown>): string {
|
|
883
|
-
if (is__ENTRY_RULE__(node)) {
|
|
884
|
-
return node.nodes.map(child => this.emitNode(child)).join('\\n');
|
|
885
|
-
}
|
|
886
|
-
if (is__NODE_RULE__(node)) {
|
|
887
|
-
return this.emitNode(node);
|
|
888
|
-
}
|
|
889
|
-
// Defensive: a rule added to the grammar with no emitter added here.
|
|
890
|
-
throw new Error(\`__GRAMMAR__Serializer: no emitter for $type \${(node as AstNode).$type}\`);
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
/** Unreachable — this grammar's one list is emitted by its \`__ENTRY_RULE__\` parent. */
|
|
894
|
-
protected override serializeArray(): string {
|
|
895
|
-
throw new Error('__GRAMMAR__Serializer: arrays are emitted by the per-$type parent, not the generic dispatch.');
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
/** Unreachable — same reasoning as {@link serializeArray}. */
|
|
899
|
-
protected override serializeReferenceArray(): string {
|
|
900
|
-
throw new Error('__GRAMMAR__Serializer: reference arrays are emitted by the per-$type parent, not the generic dispatch.');
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
/**
|
|
904
|
-
* \`serializeReferenceText\` rather than \`node.target?.$refText\`: it is the one
|
|
905
|
-
* read that spans BOTH input shapes. A transfer model reaching
|
|
906
|
-
* \`serializeTransfer\` carries \`target\` as a plain string rather than a
|
|
907
|
-
* \`Reference\`, and a serializer that reaches for \`$refText\` directly emits
|
|
908
|
-
* the AST correctly and drops every reference on the transfer path.
|
|
909
|
-
*/
|
|
910
|
-
private emitNode(node: __NODE_RULE__): string {
|
|
911
|
-
const target = this.serializeReferenceText(node.target);
|
|
912
|
-
return target === undefined ? \`node \${node.name}\` : \`node \${node.name} -> \${target}\`;
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
`;
|
|
916
|
-
|
|
917
766
|
/**
|
|
918
767
|
* The exported constant names for the head set, in emission order. Both are
|
|
919
768
|
* keyed by the PROJECT rather than by a language: one server of each kind per
|
|
@@ -1664,160 +1513,11 @@ function readme(composition: InitComposition): string {
|
|
|
1664
1513
|
*/
|
|
1665
1514
|
function glspFiles(composition: InitComposition, grammar: InitGrammarNames): InitFile[] {
|
|
1666
1515
|
const columns = columnsFor(composition);
|
|
1667
|
-
const
|
|
1668
|
-
const
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
//
|
|
1673
|
-
// **Authoritative half of a client/server contract.** Every id here has to be
|
|
1674
|
-
// registered on the client too: sprotty's registries are exact-key maps with no
|
|
1675
|
-
// prefix fallback, so an id the client does not know yields an element with none
|
|
1676
|
-
// of a node's features, rendered by \`MissingView\` with only a
|
|
1677
|
-
// \`no registered view for type '…'\` console warning. Nothing fails server-side.
|
|
1678
|
-
//
|
|
1679
|
-
// The ids stay namespaced under the GLSP defaults (\`node:\` / \`edge:\`) so a
|
|
1680
|
-
// reader can tell a node id from an edge id at a glance.
|
|
1681
|
-
|
|
1682
|
-
import { DefaultTypes } from '@eclipse-glsp/server';
|
|
1683
|
-
|
|
1684
|
-
/**
|
|
1685
|
-
* The GLSP diagram type — the string GLSP routes every per-diagram-type request
|
|
1686
|
-
* by. Mirrored by the client; a mismatch silently DROPS the request rather than
|
|
1687
|
-
* reporting an unknown diagram type. Same value as the language id, so the two
|
|
1688
|
-
* cannot drift apart as a diagram gains element types.
|
|
1689
|
-
*/
|
|
1690
|
-
export const ${upper}_DIAGRAM_TYPE = '__LANGUAGE_ID__';
|
|
1691
|
-
|
|
1692
|
-
/** A \`__NODE_RULE__\` — the starter grammar's one named, referenceable node. */
|
|
1693
|
-
export const ${upper}_NODE_TYPE = \`\${DefaultTypes.NODE}:__GRAMMAR_ID__-node\`;
|
|
1694
|
-
|
|
1695
|
-
/** A resolved \`target\` reference, drawn as a connection between two nodes. */
|
|
1696
|
-
export const ${upper}_EDGE_TYPE = \`\${DefaultTypes.EDGE}:__GRAMMAR_ID__-target\`;
|
|
1697
|
-
`;
|
|
1698
|
-
|
|
1699
|
-
const state = `// GLSP state for the __GRAMMAR__ diagram.
|
|
1700
|
-
//
|
|
1701
|
-
// \`FullTextHydraniumGlspState\` is the simplest of the framework source-model
|
|
1702
|
-
// strategies (the others project a structured transfer model, over one document
|
|
1703
|
-
// or several): the source model is the whole document text, serialised through
|
|
1704
|
-
// the per-URI \`Serializer\` and round-tripped by re-parsing. Both seams resolve
|
|
1705
|
-
// through shared services, so narrowing the root type is all an adopter adds.
|
|
1706
|
-
//
|
|
1707
|
-
// It cannot field-merge — every concurrent edit on a whole-document model is a
|
|
1708
|
-
// same-document collision, so undo / redo degrade to drop-on-divergence. Move to
|
|
1709
|
-
// \`ReconcilingTransferHydraniumGlspState\` when you need field-level undo.
|
|
1710
|
-
|
|
1711
|
-
import { FullTextHydraniumGlspState } from '@hydranium/glsp-server';
|
|
1712
|
-
import { injectable } from 'inversify';
|
|
1713
|
-
import type { __ENTRY_RULE__ } from '../../language-server/ast.js';
|
|
1714
|
-
|
|
1715
|
-
@injectable()
|
|
1716
|
-
export class __GRAMMAR__GlspState extends FullTextHydraniumGlspState<__ENTRY_RULE__> {}
|
|
1717
|
-
`;
|
|
1718
|
-
|
|
1719
|
-
const storage = `// Source-model storage for the __GRAMMAR__ diagram, inheriting both framework
|
|
1720
|
-
// defaults: \`loadSourceModel\` (open + settle + \`setSourceRoot\`) and
|
|
1721
|
-
// \`saveSourceModel\` (flush the store's text for the primary and every tracked
|
|
1722
|
-
// secondary through the diagram's client session).
|
|
1723
|
-
//
|
|
1724
|
-
// No serializer runs on the save path — a save persists what the diagram's
|
|
1725
|
-
// operations already wrote to the store.
|
|
1726
|
-
//
|
|
1727
|
-
// The subclass exists so a bespoke load or save has a stable place to land.
|
|
1728
|
-
|
|
1729
|
-
import { type FullTextSourceModel, HydraniumGlspStorage } from '@hydranium/glsp-server';
|
|
1730
|
-
import { injectable } from 'inversify';
|
|
1731
|
-
import type { __ENTRY_RULE__ } from '../../language-server/ast.js';
|
|
1732
|
-
|
|
1733
|
-
@injectable()
|
|
1734
|
-
export class __GRAMMAR__GlspStorage extends HydraniumGlspStorage<__ENTRY_RULE__, FullTextSourceModel> {}
|
|
1735
|
-
`;
|
|
1736
|
-
|
|
1737
|
-
const submission = `// Submission handler for the __GRAMMAR__ diagram.
|
|
1738
|
-
//
|
|
1739
|
-
// Inherits the framework's \`readyEvent = IntegrityService.SettledState\`, which
|
|
1740
|
-
// is load-bearing rather than incidental: the GModel factory resolves
|
|
1741
|
-
// \`target.ref\`, so it needs a fully-linked AST. Without the gate those reads
|
|
1742
|
-
// fire mid-build and warn about resolution before scopes are computed.
|
|
1743
|
-
//
|
|
1744
|
-
// Only \`formatSourceRoot\` is overridden, so the submit log names the model and
|
|
1745
|
-
// its node count instead of a bare \`$type\`.
|
|
1746
|
-
|
|
1747
|
-
import { ModelState } from '@eclipse-glsp/server';
|
|
1748
|
-
import { type FullTextSourceModel, HydraniumGlspSubmissionHandler } from '@hydranium/glsp-server';
|
|
1749
|
-
import { inject, injectable } from 'inversify';
|
|
1750
|
-
import type { __ENTRY_RULE__ } from '../../language-server/ast.js';
|
|
1751
|
-
import type { __GRAMMAR__GlspState } from './state.js';
|
|
1752
|
-
|
|
1753
|
-
@injectable()
|
|
1754
|
-
export class __GRAMMAR__SubmissionHandler extends HydraniumGlspSubmissionHandler<__ENTRY_RULE__, FullTextSourceModel> {
|
|
1755
|
-
@inject(ModelState) declare protected modelState: __GRAMMAR__GlspState;
|
|
1756
|
-
|
|
1757
|
-
protected override formatSourceRoot(root: __ENTRY_RULE__ | undefined): string {
|
|
1758
|
-
return root ? \`__ENTRY_RULE__ nodes=\${root.nodes.length}\` : 'none';
|
|
1759
|
-
}
|
|
1760
|
-
}
|
|
1761
|
-
`;
|
|
1762
|
-
|
|
1763
|
-
const factory = `// AST → GModel for the __GRAMMAR__ diagram: one node per \`__NODE_RULE__\`, one
|
|
1764
|
-
// edge per resolved \`target\` reference.
|
|
1765
|
-
//
|
|
1766
|
-
// Ids come from the index rather than being composed here, so the id strategy
|
|
1767
|
-
// stays in one place. Nodes are emitted before edges, because a GLSP edge
|
|
1768
|
-
// pointing at an id that does not exist fails client-side with a far less
|
|
1769
|
-
// obvious error than a missing edge. An unresolved reference is SKIPPED rather
|
|
1770
|
-
// than treated as an error: a dangling reference is ordinary editing state, and
|
|
1771
|
-
// the LSP head already reports it as a diagnostic.
|
|
1772
|
-
|
|
1773
|
-
import { DefaultTypes, GEdge, GGraph, GLabel, type GModelFactory, GNode, ModelState } from '@eclipse-glsp/server';
|
|
1774
|
-
import { inject, injectable } from 'inversify';
|
|
1775
|
-
import type { __ENTRY_RULE__ } from '../../language-server/ast.js';
|
|
1776
|
-
import { ${upper}_EDGE_TYPE, ${upper}_NODE_TYPE } from './types.js';
|
|
1777
|
-
import type { __GRAMMAR__GlspState } from './state.js';
|
|
1778
|
-
|
|
1779
|
-
@injectable()
|
|
1780
|
-
export class __GRAMMAR__GModelFactory implements GModelFactory {
|
|
1781
|
-
@inject(ModelState) protected readonly modelState!: __GRAMMAR__GlspState;
|
|
1782
|
-
|
|
1783
|
-
createModel(): void {
|
|
1784
|
-
const root = this.modelState.sourceRoot;
|
|
1785
|
-
const graph = GGraph.builder().id(this.modelState.sourceUri).build();
|
|
1786
|
-
if (root) {
|
|
1787
|
-
this.buildGraph(root, graph);
|
|
1788
|
-
}
|
|
1789
|
-
this.modelState.updateRoot(graph);
|
|
1790
|
-
}
|
|
1791
|
-
|
|
1792
|
-
protected buildGraph(root: __ENTRY_RULE__, graph: GGraph): void {
|
|
1793
|
-
for (const node of root.nodes) {
|
|
1794
|
-
const id = this.modelState.index.createId(node);
|
|
1795
|
-
graph.children.push(
|
|
1796
|
-
GNode.builder()
|
|
1797
|
-
.id(id)
|
|
1798
|
-
.type(${upper}_NODE_TYPE)
|
|
1799
|
-
.add(GLabel.builder().id(\`\${id}_name\`).text(node.name).type(DefaultTypes.LABEL).build())
|
|
1800
|
-
.build()
|
|
1801
|
-
);
|
|
1802
|
-
}
|
|
1803
|
-
for (const node of root.nodes) {
|
|
1804
|
-
const target = node.target?.ref;
|
|
1805
|
-
if (!target) {
|
|
1806
|
-
continue;
|
|
1807
|
-
}
|
|
1808
|
-
const sourceId = this.modelState.index.createId(node);
|
|
1809
|
-
graph.children.push(
|
|
1810
|
-
GEdge.builder()
|
|
1811
|
-
.id(\`\${sourceId}_target\`)
|
|
1812
|
-
.type(${upper}_EDGE_TYPE)
|
|
1813
|
-
.sourceId(sourceId)
|
|
1814
|
-
.targetId(this.modelState.index.createId(target))
|
|
1815
|
-
.build()
|
|
1816
|
-
);
|
|
1817
|
-
}
|
|
1818
|
-
}
|
|
1819
|
-
}
|
|
1820
|
-
`;
|
|
1516
|
+
const fixed = (file: string): InitFile => grammarTemplate(`src/glsp/__GRAMMAR_ID__/${file}`, composition, grammar);
|
|
1517
|
+
const computed = (file: string, text: string): InitFile => ({
|
|
1518
|
+
path: `src/glsp/${grammar.grammarId}/${file}`,
|
|
1519
|
+
content: render(text, composition, grammar)
|
|
1520
|
+
});
|
|
1821
1521
|
|
|
1822
1522
|
const configuration = `// Diagram configuration for the __GRAMMAR__ diagram.
|
|
1823
1523
|
//
|
|
@@ -1842,7 +1542,7 @@ ${importList(
|
|
|
1842
1542
|
columns
|
|
1843
1543
|
)}
|
|
1844
1544
|
import { injectable } from 'inversify';
|
|
1845
|
-
import {
|
|
1545
|
+
import { __GRAMMAR_ID_CONST___EDGE_TYPE, __GRAMMAR_ID_CONST___NODE_TYPE } from './types.js';
|
|
1846
1546
|
|
|
1847
1547
|
@injectable()
|
|
1848
1548
|
export class __GRAMMAR__DiagramConfiguration implements DiagramConfiguration {
|
|
@@ -1854,7 +1554,7 @@ export class __GRAMMAR__DiagramConfiguration implements DiagramConfiguration {
|
|
|
1854
1554
|
|
|
1855
1555
|
readonly shapeTypeHints: ShapeTypeHint[] = [
|
|
1856
1556
|
{
|
|
1857
|
-
elementTypeId:
|
|
1557
|
+
elementTypeId: __GRAMMAR_ID_CONST___NODE_TYPE,
|
|
1858
1558
|
deletable: false,
|
|
1859
1559
|
reparentable: false,
|
|
1860
1560
|
repositionable: false,
|
|
@@ -1864,78 +1564,15 @@ export class __GRAMMAR__DiagramConfiguration implements DiagramConfiguration {
|
|
|
1864
1564
|
|
|
1865
1565
|
readonly edgeTypeHints: EdgeTypeHint[] = [
|
|
1866
1566
|
{
|
|
1867
|
-
elementTypeId:
|
|
1567
|
+
elementTypeId: __GRAMMAR_ID_CONST___EDGE_TYPE,
|
|
1868
1568
|
deletable: false,
|
|
1869
1569
|
repositionable: false,
|
|
1870
1570
|
routable: false,
|
|
1871
|
-
sourceElementTypeIds: [
|
|
1872
|
-
targetElementTypeIds: [
|
|
1571
|
+
sourceElementTypeIds: [__GRAMMAR_ID_CONST___NODE_TYPE],
|
|
1572
|
+
targetElementTypeIds: [__GRAMMAR_ID_CONST___NODE_TYPE]
|
|
1873
1573
|
}
|
|
1874
1574
|
];
|
|
1875
1575
|
}
|
|
1876
|
-
`;
|
|
1877
|
-
|
|
1878
|
-
const createHandler = `// The starter operation handler for the __GRAMMAR__ diagram: the tool-palette
|
|
1879
|
-
// entry that creates a \`__NODE_RULE__\`.
|
|
1880
|
-
//
|
|
1881
|
-
// **Deleting this file and its \`configureOperationHandlers\` registration in
|
|
1882
|
-
// \`diagram-module.ts\` gives a read-only viewer**, and nothing else has to change
|
|
1883
|
-
// — creation is offered through the palette rather than through a type hint, so
|
|
1884
|
-
// every hint in \`diagram-configuration.ts\` is already \`false\`. The scaffold emits
|
|
1885
|
-
// the editable direction because that asymmetry runs one way: editable →
|
|
1886
|
-
// read-only is a deletion the compiler checks, while read-only → editable is
|
|
1887
|
-
// authoring against a seam you have not used yet.
|
|
1888
|
-
//
|
|
1889
|
-
// **It edits the operation's working copy.** A handler runs inside an operation,
|
|
1890
|
-
// where \`sourceRoot\` is a copy of the built root: the recording command's
|
|
1891
|
-
// runnable appends to the copy, and the operation writes the copy's text through
|
|
1892
|
-
// the grammar's \`Serializer\` once, gated on the version the copy was made at,
|
|
1893
|
-
// with undo and redo applying that one change. A command that writes the
|
|
1894
|
-
// document itself instead reads no text off the copy, which has no \`$document\`,
|
|
1895
|
-
// and the operation's own write then overwrites it.
|
|
1896
|
-
//
|
|
1897
|
-
// **The drop location is discarded.** \`needsClientLayout\` is \`true\` and the
|
|
1898
|
-
// starter grammar persists no bounds, so there is nowhere to put a coordinate: the
|
|
1899
|
-
// node is appended at the end of the document and the client places it.
|
|
1900
|
-
//
|
|
1901
|
-
// Like \`types.ts\`, \`gmodel-factory.ts\` and \`diagram-configuration.ts\`, this file
|
|
1902
|
-
// knows the starter grammar's shape — the \`nodes\` list and the \`__NODE_RULE__\`
|
|
1903
|
-
// it holds. Replacing the grammar means replacing these four together.
|
|
1904
|
-
|
|
1905
|
-
import { type Command, type CreateNodeOperation, JsonCreateNodeOperationHandler, type MaybePromise } from '@eclipse-glsp/server';
|
|
1906
|
-
import { appendChild } from '@hydranium/core';
|
|
1907
|
-
import { HydraniumGlspRecordingCommand } from '@hydranium/glsp-server';
|
|
1908
|
-
import { findNextUnique } from '@hydranium/protocol';
|
|
1909
|
-
import { injectable } from 'inversify';
|
|
1910
|
-
import type { __NODE_RULE__ } from '../../language-server/ast.js';
|
|
1911
|
-
import { ${upper}_NODE_TYPE } from './types.js';
|
|
1912
|
-
import type { __GRAMMAR__GlspState } from './state.js';
|
|
1913
|
-
|
|
1914
|
-
/** Proposed name for a new node, uniquified against the ones the document already has. */
|
|
1915
|
-
const NODE_NAME_STEM = 'Node';
|
|
1916
|
-
|
|
1917
|
-
@injectable()
|
|
1918
|
-
export class __GRAMMAR__CreateNodeOperationHandler extends JsonCreateNodeOperationHandler {
|
|
1919
|
-
declare protected modelState: __GRAMMAR__GlspState;
|
|
1920
|
-
|
|
1921
|
-
/** The palette's word for the thing it creates, so a noun rather than an action. */
|
|
1922
|
-
override readonly label = '__NODE_RULE__';
|
|
1923
|
-
elementTypeIds = [${upper}_NODE_TYPE];
|
|
1924
|
-
|
|
1925
|
-
override createCommand(operation: CreateNodeOperation): MaybePromise<Command | undefined> {
|
|
1926
|
-
if (!this.elementTypeIds.includes(operation.elementTypeId)) {
|
|
1927
|
-
return undefined;
|
|
1928
|
-
}
|
|
1929
|
-
return new HydraniumGlspRecordingCommand(this.modelState, this.label, () => {
|
|
1930
|
-
const root = this.modelState.sourceRoot;
|
|
1931
|
-
const name = findNextUnique(
|
|
1932
|
-
NODE_NAME_STEM,
|
|
1933
|
-
root.nodes.map(node => node.name)
|
|
1934
|
-
);
|
|
1935
|
-
appendChild(root, 'nodes', root.nodes, { $type: '__NODE_RULE__', name } as __NODE_RULE__);
|
|
1936
|
-
});
|
|
1937
|
-
}
|
|
1938
|
-
}
|
|
1939
1576
|
`;
|
|
1940
1577
|
|
|
1941
1578
|
const module = `// GLSP diagram module for __GRAMMAR__ — the DI wiring of one diagram type.
|
|
@@ -1977,10 +1614,10 @@ import { __GRAMMAR__GModelFactory } from './gmodel-factory.js';
|
|
|
1977
1614
|
import { __GRAMMAR__GlspState } from './state.js';
|
|
1978
1615
|
import { __GRAMMAR__GlspStorage } from './storage.js';
|
|
1979
1616
|
import { __GRAMMAR__SubmissionHandler } from './submission-handler.js';
|
|
1980
|
-
import {
|
|
1617
|
+
import { __GRAMMAR_ID_CONST___DIAGRAM_TYPE } from './types.js';
|
|
1981
1618
|
|
|
1982
1619
|
export class __GRAMMAR__DiagramModule extends AbstractHydraniumGlspDiagramModule {
|
|
1983
|
-
readonly diagramType =
|
|
1620
|
+
readonly diagramType = __GRAMMAR_ID_CONST___DIAGRAM_TYPE;
|
|
1984
1621
|
|
|
1985
1622
|
protected override bindModelState(): BindingTarget<ModelState> {
|
|
1986
1623
|
return { service: __GRAMMAR__GlspState };
|
|
@@ -2017,88 +1654,18 @@ export class __GRAMMAR__DiagramModule extends AbstractHydraniumGlspDiagramModule
|
|
|
2017
1654
|
binding.add(__GRAMMAR__CreateNodeOperationHandler);
|
|
2018
1655
|
}
|
|
2019
1656
|
}
|
|
2020
|
-
`;
|
|
2021
|
-
|
|
2022
|
-
const diagramTest = `// The starter create-node handler, run the way a client runs it: an in-process
|
|
2023
|
-
// GLSP server opens a document, a create-node operation adds a node, and an undo
|
|
2024
|
-
// and a redo take it out and put it back. Every step must keep the nodes the
|
|
2025
|
-
// document already had, and its comments: the diagram writes the document from
|
|
2026
|
-
// its AST, which carries none.
|
|
2027
|
-
|
|
2028
|
-
import 'reflect-metadata';
|
|
2029
|
-
import { type Action, CreateNodeOperation, RedoAction, ServerModule, SetDirtyStateAction, UndoAction } from '@eclipse-glsp/server';
|
|
2030
|
-
import { initializeWorkspaceProgrammatically } from '@hydranium/core';
|
|
2031
|
-
import { makeScratchWorkspace, type ScratchWorkspace } from '@hydranium/core/testing/node';
|
|
2032
|
-
import { HydraniumGlspAppModule } from '@hydranium/glsp-server';
|
|
2033
|
-
import { type GlspHarness, makeGlspHarness } from '@hydranium/glsp-server/testing';
|
|
2034
|
-
import { waitFor } from '@hydranium/protocol/testing';
|
|
2035
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
2036
|
-
import { __GRAMMAR__DiagramModule } from '../src/glsp/__GRAMMAR_ID__/diagram-module.js';
|
|
2037
|
-
import type { __GRAMMAR__GlspState } from '../src/glsp/__GRAMMAR_ID__/state.js';
|
|
2038
|
-
import { ${upper}_DIAGRAM_TYPE, ${upper}_NODE_TYPE } from '../src/glsp/__GRAMMAR_ID__/types.js';
|
|
2039
|
-
import { createServices } from '../src/services.js';
|
|
2040
|
-
|
|
2041
|
-
let workspace: ScratchWorkspace | undefined;
|
|
2042
|
-
let diagram: GlspHarness<__GRAMMAR__GlspState> | undefined;
|
|
2043
|
-
|
|
2044
|
-
afterEach(() => {
|
|
2045
|
-
diagram?.dispose();
|
|
2046
|
-
workspace?.dispose();
|
|
2047
|
-
diagram = undefined;
|
|
2048
|
-
workspace = undefined;
|
|
2049
|
-
});
|
|
2050
|
-
|
|
2051
|
-
/** Send \`action\` and wait for the dirty state the server answers it with, which it sends once the edit is written. */
|
|
2052
|
-
async function send(harness: GlspHarness<__GRAMMAR__GlspState>, action: Action, reason: string): Promise<void> {
|
|
2053
|
-
const before = harness.actions.length;
|
|
2054
|
-
harness.dispatch(action);
|
|
2055
|
-
await waitFor(() => harness.actions.slice(before).some(sent => SetDirtyStateAction.is(sent) && sent.reason === reason), {
|
|
2056
|
-
message: \`no '\${reason}' dirty state\`
|
|
2057
|
-
});
|
|
2058
|
-
}
|
|
2059
|
-
|
|
2060
|
-
describe('__GRAMMAR__ diagram', () => {
|
|
2061
|
-
it('creates a node after the existing ones, keeping their comments, and undoes and redoes it', async () => {
|
|
2062
|
-
workspace = makeScratchWorkspace({ prefix: '__GRAMMAR_ID__-diagram-' });
|
|
2063
|
-
const original = '// keep this comment\\nnode first -> second\\n// about second\\nnode second';
|
|
2064
|
-
const file = workspace.write('model.__EXTENSION__', original);
|
|
2065
|
-
const { shared } = createServices();
|
|
2066
|
-
await initializeWorkspaceProgrammatically(shared, workspace.root);
|
|
2067
|
-
diagram = makeGlspHarness<__GRAMMAR__GlspState>({
|
|
2068
|
-
serverModule: new ServerModule().configureDiagramModule(new __GRAMMAR__DiagramModule()),
|
|
2069
|
-
diagramType: ${upper}_DIAGRAM_TYPE,
|
|
2070
|
-
appModules: [new HydraniumGlspAppModule({ shared })]
|
|
2071
|
-
});
|
|
2072
|
-
await diagram.start();
|
|
2073
|
-
await diagram.openDocument(file);
|
|
2074
|
-
const uri = workspace.uri('model.__EXTENSION__');
|
|
2075
|
-
const text = (): string | undefined => shared.workspace.TextDocuments.get(uri)?.getText();
|
|
2076
|
-
|
|
2077
|
-
await send(diagram, CreateNodeOperation.create(${upper}_NODE_TYPE), 'operation');
|
|
2078
|
-
const created = text();
|
|
2079
|
-
await send(diagram, UndoAction.create(), 'undo');
|
|
2080
|
-
const undone = text();
|
|
2081
|
-
await send(diagram, RedoAction.create(), 'redo');
|
|
2082
|
-
|
|
2083
|
-
expect({ created, undone, redone: text() }).toEqual({
|
|
2084
|
-
created: \`\${original}\\nnode Node\`,
|
|
2085
|
-
undone: original,
|
|
2086
|
-
redone: \`\${original}\\nnode Node\`
|
|
2087
|
-
});
|
|
2088
|
-
});
|
|
2089
|
-
});
|
|
2090
1657
|
`;
|
|
2091
1658
|
|
|
2092
1659
|
return [
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
1660
|
+
fixed('types.ts'),
|
|
1661
|
+
fixed('state.ts'),
|
|
1662
|
+
fixed('storage.ts'),
|
|
1663
|
+
fixed('submission-handler.ts'),
|
|
1664
|
+
fixed('gmodel-factory.ts'),
|
|
1665
|
+
computed('diagram-configuration.ts', configuration),
|
|
1666
|
+
fixed('create-node-operation-handler.ts'),
|
|
1667
|
+
computed('diagram-module.ts', module),
|
|
1668
|
+
grammarTemplate('test/__GRAMMAR_ID__-diagram.test.ts', composition, grammar)
|
|
2102
1669
|
];
|
|
2103
1670
|
}
|
|
2104
1671
|
|
|
@@ -2115,8 +1682,10 @@ export function buildInitTemplates(composition: InitComposition): InitFile[] {
|
|
|
2115
1682
|
path: `src/grammar/${grammar.grammarId}.langium`,
|
|
2116
1683
|
content: render(grammarFile(multi), composition, grammar)
|
|
2117
1684
|
}));
|
|
1685
|
+
// The shared lexical fragment, only for several grammars: each declaring its
|
|
1686
|
+
// own `WS` / `ID` lets the token sets drift apart.
|
|
2118
1687
|
if (multi) {
|
|
2119
|
-
grammarFiles.unshift(
|
|
1688
|
+
grammarFiles.unshift(projectTemplate('src/grammar/common.langium', composition));
|
|
2120
1689
|
}
|
|
2121
1690
|
|
|
2122
1691
|
const diagramFiles = composition.grammars.filter(grammar => grammar.diagram).flatMap(grammar => glspFiles(composition, grammar));
|
|
@@ -2125,20 +1694,36 @@ export function buildInitTemplates(composition: InitComposition): InitFile[] {
|
|
|
2125
1694
|
{ path: 'package.json', content: packageJson(composition) },
|
|
2126
1695
|
{ path: 'langium-config.json', content: langiumConfig(composition) },
|
|
2127
1696
|
{ path: 'tsconfig.json', content: tsconfigJson(composition) },
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
1697
|
+
// `isolatedModules` is what makes this check agree with the transform that
|
|
1698
|
+
// actually runs the tests. `npm test` is `typecheck:test && vitest run`, and
|
|
1699
|
+
// vitest compiles each file on its own — a transform with no type
|
|
1700
|
+
// information, which cannot tell a re-exported TYPE from a re-exported value.
|
|
1701
|
+
// Without the flag `tsc` accepts `export { SomeType } from './x'` that the
|
|
1702
|
+
// transform then emits as a real import of a symbol that does not exist at
|
|
1703
|
+
// runtime, so the typecheck passes and the suite fails with a confusing
|
|
1704
|
+
// missing-export.
|
|
1705
|
+
projectTemplate('tsconfig.test.json', composition),
|
|
1706
|
+
// Found by name, unlike `tsconfig.test.json`. Under vite 8, vitest's transform
|
|
1707
|
+
// applies a tsconfig only to the files its own `include` lists, so without this
|
|
1708
|
+
// a test declaring a decorated class fails to load.
|
|
1709
|
+
projectTemplate('test/tsconfig.json', composition),
|
|
1710
|
+
projectTemplate('vitest.config.ts', composition),
|
|
1711
|
+
// Written for a workspace member too: its root may not ignore `lib/` (a `yo
|
|
1712
|
+
// langium` root ignores `out/`), and a rule the root repeats costs nothing.
|
|
1713
|
+
// Stored without its dot, because `npm pack` drops a nested `.gitignore` from
|
|
1714
|
+
// the tarball and applies it as an ignore file.
|
|
1715
|
+
{ path: '.gitignore', content: template('server/gitignore') },
|
|
2132
1716
|
{ path: 'README.md', content: readme(composition) },
|
|
2133
1717
|
...grammarFiles,
|
|
2134
1718
|
{ path: `src/language-server/${composition.projectId}-module.ts`, content: moduleFile(composition) },
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
1719
|
+
grammarTemplate('src/language-server/ast.ts', composition, composition.grammars[0]),
|
|
1720
|
+
// Per grammar, because `services.serializer.Serializer` is a per-language slot
|
|
1721
|
+
// and a serializer is grammar-shaped by definition. That is also why the
|
|
1722
|
+
// framework refuses to default it: its `UnboundSerializer` throws, naming the
|
|
1723
|
+
// binding to add, rather than guessing a syntax.
|
|
1724
|
+
...composition.grammars.map(grammar => grammarTemplate('src/language-server/__GRAMMAR_ID__-serializer.ts', composition, grammar)),
|
|
2140
1725
|
{ path: 'src/index.ts', content: indexFile(composition) },
|
|
2141
|
-
|
|
1726
|
+
projectTemplate('src/services.ts', composition),
|
|
2142
1727
|
{ path: 'src/main.ts', content: mainFile(composition) },
|
|
2143
1728
|
...(composition.heads.includes('data') ? [{ path: 'src/data-server-main.ts', content: dataServerMainFile(composition) }] : []),
|
|
2144
1729
|
...(portCommandNames(composition).length ? [{ path: 'src/head-ports.ts', content: headPortsFile(composition) }] : []),
|