@hydranium/cli 1.0.0-next.307 → 1.0.0-next.309
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/README.md +11 -9
- package/lib/commands/headless-harness.js +1 -1
- package/lib/commands/headless-harness.js.map +1 -1
- package/lib/commands/init-args.d.ts.map +1 -1
- package/lib/commands/init-args.js +21 -16
- package/lib/commands/init-args.js.map +1 -1
- package/lib/commands/init-templates.d.ts +12 -6
- package/lib/commands/init-templates.d.ts.map +1 -1
- package/lib/commands/init-templates.js +394 -127
- package/lib/commands/init-templates.js.map +1 -1
- package/lib/commands/init-wizard.d.ts.map +1 -1
- package/lib/commands/init-wizard.js +8 -7
- package/lib/commands/init-wizard.js.map +1 -1
- package/lib/commands/init-workspace.d.ts +0 -8
- package/lib/commands/init-workspace.d.ts.map +1 -1
- package/lib/commands/init-workspace.js +0 -1
- package/lib/commands/init-workspace.js.map +1 -1
- package/lib/commands/init.d.ts +26 -1
- package/lib/commands/init.d.ts.map +1 -1
- package/lib/commands/init.js +64 -20
- package/lib/commands/init.js.map +1 -1
- package/package.json +11 -10
- package/src/commands/headless-harness.ts +1 -1
- package/src/commands/init-args.ts +21 -16
- package/src/commands/init-templates.ts +424 -133
- package/src/commands/init-wizard.ts +8 -7
- package/src/commands/init-workspace.ts +0 -9
- package/src/commands/init.ts +90 -20
- package/templates/gitignore +1 -0
- package/templates/protocol/gitignore +3 -0
- package/templates/protocol/src/__GRAMMAR_ID__-diagram-types.ts +41 -0
- package/templates/server/src/glsp/__GRAMMAR_ID__/create-node-operation-handler.ts +5 -4
- package/templates/server/src/glsp/__GRAMMAR_ID__/gmodel-factory.ts +6 -3
- package/templates/server/src/{services.ts → node/services.ts} +6 -2
- package/templates/server/test/__GRAMMAR_ID__-diagram.test.ts +28 -7
- package/templates/workspace/nodes.__EXTENSION__ +5 -0
- package/templates/workspace/references.__EXTENSION__ +2 -0
- package/templates/server/src/glsp/__GRAMMAR_ID__/types.ts +0 -26
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
* `bin` key, the DI module filename, and the data-server port command.
|
|
22
22
|
* - `__CONFIG_ROOT_CONST__` — SCREAMING_SNAKE project id, naming the
|
|
23
23
|
* `lsp.configurationRoot` constant a multi-grammar project binds.
|
|
24
|
+
* - `__PROTOCOL_PACKAGE__` — the protocol member's package name, scope
|
|
25
|
+
* included, which the server and every client import ids and wire types
|
|
26
|
+
* from.
|
|
24
27
|
*
|
|
25
28
|
* GRAMMAR tokens, substituted once per grammar:
|
|
26
29
|
*
|
|
@@ -50,9 +53,16 @@
|
|
|
50
53
|
* generator emits `interface <projectName>Element` as the base type every rule
|
|
51
54
|
* extends, which a rule of that name would collide with.
|
|
52
55
|
*
|
|
56
|
+
* The project is two packages and a starter workspace, each in its own
|
|
57
|
+
* directory: `server/` (the grammar, the language and its heads), `protocol/`
|
|
58
|
+
* (what the server and its clients both import, so a client never installs
|
|
59
|
+
* Langium), and `workspace/` (starter model files every host and the CLI
|
|
60
|
+
* open). A standalone project adds a root manifest naming them as npm
|
|
61
|
+
* workspaces; a workspace member joins the surrounding root instead.
|
|
62
|
+
*
|
|
53
63
|
* A file that is fixed text with tokens lives in the `templates/` tree the
|
|
54
64
|
* package ships beside `lib`, at the path it is emitted at, tokens included
|
|
55
|
-
* (
|
|
65
|
+
* (each ignore file without its dot); one computed from the composition is
|
|
56
66
|
* built here. The tree is read synchronously, so `runInit` stays synchronous,
|
|
57
67
|
* and it is neither formatted with the repo's settings nor given a license
|
|
58
68
|
* header, since either would change what `init` emits.
|
|
@@ -103,12 +113,47 @@ function frameworkPin(composition) {
|
|
|
103
113
|
function screamingSnake(projectId) {
|
|
104
114
|
return projectId.replace(/-/g, '_').toUpperCase();
|
|
105
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* A member's package name: `<id>-<member>`, under the scope when there is one.
|
|
118
|
+
*
|
|
119
|
+
* Suffixed even for the server, because a project of several packages has no
|
|
120
|
+
* member entitled to the bare id, and a later client or host would otherwise
|
|
121
|
+
* read as a part of the server.
|
|
122
|
+
*/
|
|
123
|
+
export function memberPackageName(composition, member) {
|
|
124
|
+
const { scope } = composition.packaging;
|
|
125
|
+
const name = `${composition.projectId}-${member}`;
|
|
126
|
+
return scope === undefined ? name : `${scope}/${name}`;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The composition as one member sees it: inside a workspace, its placement
|
|
130
|
+
* sits one directory below the project's, so its `extends` and its lint
|
|
131
|
+
* config path reach one level further up.
|
|
132
|
+
*/
|
|
133
|
+
function forMember(composition, member) {
|
|
134
|
+
const workspace = composition.packaging.workspace;
|
|
135
|
+
if (workspace === undefined) {
|
|
136
|
+
return composition;
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
...composition,
|
|
140
|
+
packaging: {
|
|
141
|
+
...composition.packaging,
|
|
142
|
+
workspace: {
|
|
143
|
+
...workspace,
|
|
144
|
+
targetPath: `${workspace.targetPath}/${member}`,
|
|
145
|
+
baseTsconfig: workspace.baseTsconfig === undefined ? undefined : `../${workspace.baseTsconfig}`
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
106
150
|
/** Substitute the project-tier tokens. */
|
|
107
151
|
function project(text, composition) {
|
|
108
152
|
return text
|
|
109
153
|
.replace(/__NAME__/g, composition.name)
|
|
110
154
|
.replace(/__PROJECT_ID__/g, composition.projectId)
|
|
111
|
-
.replace(/__CONFIG_ROOT_CONST__/g, screamingSnake(composition.projectId))
|
|
155
|
+
.replace(/__CONFIG_ROOT_CONST__/g, screamingSnake(composition.projectId))
|
|
156
|
+
.replace(/__PROTOCOL_PACKAGE__/g, memberPackageName(composition, 'protocol'));
|
|
112
157
|
}
|
|
113
158
|
/**
|
|
114
159
|
* Substitute the grammar-tier tokens for ONE grammar.
|
|
@@ -138,13 +183,18 @@ const TEMPLATES = new URL('../../templates/', import.meta.url);
|
|
|
138
183
|
function template(path) {
|
|
139
184
|
return readFileSync(new URL(path, TEMPLATES), 'utf-8');
|
|
140
185
|
}
|
|
141
|
-
/** A
|
|
186
|
+
/** A template emitted once per project at its own path, rendered with the project tokens. */
|
|
142
187
|
function projectTemplate(path, composition) {
|
|
143
|
-
return { path, content: project(template(
|
|
188
|
+
return { path, content: project(template(path), composition) };
|
|
144
189
|
}
|
|
145
|
-
/** A
|
|
190
|
+
/** A template emitted once per grammar, its path rendered like its content. */
|
|
146
191
|
function grammarTemplate(path, composition, grammar) {
|
|
147
|
-
return { path: render(path, composition, grammar), content: render(template(
|
|
192
|
+
return { path: render(path, composition, grammar), content: render(template(path), composition, grammar) };
|
|
193
|
+
}
|
|
194
|
+
/** An ignore file, stored without its dot because `npm pack` drops a nested `.gitignore` and applies it as an ignore file. */
|
|
195
|
+
function ignoreFile(directory) {
|
|
196
|
+
const prefix = directory === '' ? '' : `${directory}/`;
|
|
197
|
+
return { path: `${prefix}.gitignore`, content: template(`${prefix}gitignore`) };
|
|
148
198
|
}
|
|
149
199
|
/** A comma-separated list of single-quoted TypeScript/JS string literals. */
|
|
150
200
|
function quotedList(values) {
|
|
@@ -232,6 +282,16 @@ const HEAD_THIRD_PARTY_DEPENDENCIES = {
|
|
|
232
282
|
'reflect-metadata': '~0.2.2'
|
|
233
283
|
}
|
|
234
284
|
};
|
|
285
|
+
/**
|
|
286
|
+
* What each head adds to the protocol member, which carries only what a
|
|
287
|
+
* client may install. Keyed by head because the `glsp` head implies a diagram,
|
|
288
|
+
* whose element ids extend GLSP's default type names.
|
|
289
|
+
*/
|
|
290
|
+
const PROTOCOL_THIRD_PARTY_DEPENDENCIES = {
|
|
291
|
+
lsp: {},
|
|
292
|
+
data: {},
|
|
293
|
+
glsp: { '@eclipse-glsp/protocol': '2.7.0' }
|
|
294
|
+
};
|
|
235
295
|
/**
|
|
236
296
|
* The exact pins of `heads`, for a workspace root to declare too. npm installs a
|
|
237
297
|
* root's own dependencies at the top of the tree, so another member's version of
|
|
@@ -240,7 +300,7 @@ const HEAD_THIRD_PARTY_DEPENDENCIES = {
|
|
|
240
300
|
*/
|
|
241
301
|
export function workspaceRootPins(heads) {
|
|
242
302
|
return heads
|
|
243
|
-
.flatMap(head => Object.entries(HEAD_THIRD_PARTY_DEPENDENCIES[head]))
|
|
303
|
+
.flatMap(head => [...Object.entries(HEAD_THIRD_PARTY_DEPENDENCIES[head]), ...Object.entries(PROTOCOL_THIRD_PARTY_DEPENDENCIES[head])])
|
|
244
304
|
.filter(([, version]) => /^\d+\.\d+\.\d+$/.test(version))
|
|
245
305
|
.sort(([left], [right]) => left.localeCompare(right));
|
|
246
306
|
}
|
|
@@ -260,19 +320,30 @@ function pinsVitest(composition) {
|
|
|
260
320
|
const workspace = composition.packaging.workspace;
|
|
261
321
|
return !(installsVitest41(workspace?.packageManagerNpm) && installsVitest41(workspace?.npmVersion));
|
|
262
322
|
}
|
|
263
|
-
/**
|
|
323
|
+
/**
|
|
324
|
+
* The version a member pins a sibling member at: exact, at the version every
|
|
325
|
+
* member starts with. npm links a sibling only when the range admits its local
|
|
326
|
+
* version, and otherwise looks for it in the registry, where it was never
|
|
327
|
+
* published.
|
|
328
|
+
*/
|
|
329
|
+
const MEMBER_PIN = '0.0.0';
|
|
330
|
+
/** A dependency block, sorted as npm writes it. */
|
|
331
|
+
function sortedBlock(dependencies, indent) {
|
|
332
|
+
return Object.keys(dependencies)
|
|
333
|
+
.sort()
|
|
334
|
+
.map(name => `${indent}"${name}": "${dependencies[name]}"`)
|
|
335
|
+
.join(',\n');
|
|
336
|
+
}
|
|
337
|
+
/** The server's dependency block: the head set's packages and the protocol member. */
|
|
264
338
|
function dependencyBlock(composition, indent) {
|
|
265
|
-
const merged = {};
|
|
339
|
+
const merged = { [memberPackageName(composition, 'protocol')]: MEMBER_PIN };
|
|
266
340
|
for (const head of composition.heads) {
|
|
267
341
|
Object.assign(merged, HEAD_THIRD_PARTY_DEPENDENCIES[head]);
|
|
268
342
|
for (const framework of HEAD_FRAMEWORK_DEPENDENCIES[head]) {
|
|
269
343
|
merged[framework] = frameworkPin(composition);
|
|
270
344
|
}
|
|
271
345
|
}
|
|
272
|
-
return
|
|
273
|
-
.sort()
|
|
274
|
-
.map(name => `${indent}"${name}": "${merged[name]}"`)
|
|
275
|
-
.join(',\n');
|
|
346
|
+
return sortedBlock(merged, indent);
|
|
276
347
|
}
|
|
277
348
|
// JSON at two-space indent, not the three the `.ts` templates use: that is the
|
|
278
349
|
// npm convention, and `npm install` rewrites `package.json` at two spaces
|
|
@@ -298,6 +369,11 @@ function dependencyBlock(composition, indent) {
|
|
|
298
369
|
// from this package's own grammar and can reach a consumer from nowhere else; a
|
|
299
370
|
// listed path that does not exist yet is inert.
|
|
300
371
|
//
|
|
372
|
+
// `exports` puts `createServices`, the one module reading the real filesystem,
|
|
373
|
+
// behind `./node`, so the root loads in a browser worker that has no `node:fs`.
|
|
374
|
+
// `./lib/*` and `./package.json` stay reachable for a host that resolves
|
|
375
|
+
// `<server>/lib/main.js` to spawn the server.
|
|
376
|
+
//
|
|
301
377
|
// `repository` is deliberately NOT emitted while `author` is emitted empty. An
|
|
302
378
|
// empty `author` is the form `npm init` itself writes and asserts nothing, but
|
|
303
379
|
// `repository` is CONSUMED — the registry page, `npm repo` and publish
|
|
@@ -322,17 +398,29 @@ __BIN__ },
|
|
|
322
398
|
"type": "module",
|
|
323
399
|
"main": "lib/index.js",
|
|
324
400
|
"types": "lib/index.d.ts",
|
|
401
|
+
"exports": {
|
|
402
|
+
".": {
|
|
403
|
+
"types": "./lib/index.d.ts",
|
|
404
|
+
"default": "./lib/index.js"
|
|
405
|
+
},
|
|
406
|
+
"./node": {
|
|
407
|
+
"types": "./lib/node/services.d.ts",
|
|
408
|
+
"default": "./lib/node/services.js"
|
|
409
|
+
},
|
|
410
|
+
"./lib/*": "./lib/*",
|
|
411
|
+
"./package.json": "./package.json"
|
|
412
|
+
},
|
|
325
413
|
"scripts": {
|
|
326
|
-
"build": "npm run generate && tsc",
|
|
414
|
+
"build": "npm run generate && tsc -b",
|
|
327
415
|
"clean": "rimraf lib syntaxes src/language-server/generated src/language-server/generated-hydranium tsconfig.tsbuildinfo",
|
|
328
416
|
"generate": "npm run langium:generate && npm run generate:transfer-model",
|
|
329
|
-
"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/
|
|
417
|
+
"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 ../protocol/src/generated-hydranium/transfer-model.ts --ast-builder-file src/language-server/generated-hydranium/ast-builder.ts --languages-file ../protocol/src/generated-hydranium/languages.ts --element-type-name __NAME__Element --terminals-name __NAME__Terminals --regen-command \\"Run: __NPM_RUN__ generate:transfer-model\\"",
|
|
330
418
|
"langium:generate": "langium generate",
|
|
331
419
|
"langium:watch": "langium generate --watch",
|
|
332
420
|
__LINT__ "start": "node lib/main.js --stdio",
|
|
333
421
|
"test": "npm run typecheck:test && vitest run",
|
|
334
422
|
"typecheck:test": "tsc --noEmit -p tsconfig.test.json",
|
|
335
|
-
"watch": "tsc --watch"
|
|
423
|
+
"watch": "tsc -b --watch"
|
|
336
424
|
},
|
|
337
425
|
"dependencies": {
|
|
338
426
|
__DEPENDENCIES__
|
|
@@ -351,16 +439,17 @@ __DEPENDENCIES__
|
|
|
351
439
|
}
|
|
352
440
|
`;
|
|
353
441
|
/**
|
|
354
|
-
* How the scaffold names `npm run`
|
|
442
|
+
* How the scaffold names `npm run` for one member's script.
|
|
355
443
|
*
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
444
|
+
* Always with `--prefix`, from the directory commands are run in: the
|
|
445
|
+
* workspace root for a member, the project root for a standalone project. A
|
|
446
|
+
* bare `npm run` there reaches the ROOT manifest and fails, and the regen hint
|
|
447
|
+
* a generated file prints is read in `protocol/` as well, where the script it
|
|
448
|
+
* names does not exist.
|
|
360
449
|
*/
|
|
361
|
-
function npmRun(composition) {
|
|
450
|
+
function npmRun(composition, member) {
|
|
362
451
|
const workspace = composition.packaging.workspace;
|
|
363
|
-
return workspace === undefined ?
|
|
452
|
+
return `npm --prefix ${workspace === undefined ? member : `${workspace.targetPath}/${member}`} run`;
|
|
364
453
|
}
|
|
365
454
|
/**
|
|
366
455
|
* The `bin` targets, one per executable entry the head set emits.
|
|
@@ -381,35 +470,36 @@ function binBlock(composition) {
|
|
|
381
470
|
}
|
|
382
471
|
return entries.map(([name, target]) => ` "${name}": "${target}"`).join(',\n') + '\n';
|
|
383
472
|
}
|
|
384
|
-
/**
|
|
473
|
+
/**
|
|
474
|
+
* A member's `lint` script line, over `directories`, or nothing.
|
|
475
|
+
*
|
|
476
|
+
* Emitted only where a root lint config says the repo lints. The absence is
|
|
477
|
+
* the dangerous direction: a task runner runs a script only where one is
|
|
478
|
+
* declared, so a package with no `lint` is SKIPPED rather than reported, and
|
|
479
|
+
* that reads as a clean lint. A wrong invocation is one visible line to edit.
|
|
480
|
+
*/
|
|
481
|
+
function lintScript(member, directories) {
|
|
482
|
+
const workspace = member.packaging.workspace;
|
|
483
|
+
if (workspace?.oxlintConfig !== undefined) {
|
|
484
|
+
const config = `${'../'.repeat(workspace.targetPath.split('/').length)}${workspace.oxlintConfig}`;
|
|
485
|
+
return ` "lint": "oxlint --report-unused-disable-directives --format stylish --config ${config} ${directories} --max-warnings 0",\n`;
|
|
486
|
+
}
|
|
487
|
+
return workspace?.eslintConfig !== undefined ? ` "lint": "eslint ${directories} --max-warnings 0",\n` : '';
|
|
488
|
+
}
|
|
489
|
+
/** The server's `package.json`, with the name, the private flag and the dependency block all derived. */
|
|
385
490
|
function packageJson(composition) {
|
|
386
|
-
const { scope } = composition.packaging;
|
|
387
|
-
const packageName = scope === undefined ? composition.projectId : `${scope}/${composition.projectId}`;
|
|
388
491
|
const keywords = ['hydranium', 'langium', 'language-server', composition.projectId]
|
|
389
492
|
.sort()
|
|
390
493
|
.map(keyword => ` ${JSON.stringify(keyword)}`)
|
|
391
494
|
.join(',\n') + '\n';
|
|
392
|
-
// Emitted only where a root lint config says the repo lints. The absence is
|
|
393
|
-
// the dangerous direction: a task runner runs a script only where one is
|
|
394
|
-
// declared, so a package with no `lint` is SKIPPED rather than reported, and
|
|
395
|
-
// that reads as a clean lint. A wrong invocation is one visible line to edit.
|
|
396
|
-
const workspace = composition.packaging.workspace;
|
|
397
|
-
const oxlintConfig = workspace?.oxlintConfig === undefined
|
|
398
|
-
? undefined
|
|
399
|
-
: `${'../'.repeat(workspace.targetPath.split('/').length)}${workspace.oxlintConfig}`;
|
|
400
|
-
const lint = workspace?.oxlintConfig !== undefined
|
|
401
|
-
? ` "lint": "oxlint --report-unused-disable-directives --format stylish --config ${oxlintConfig} src test --max-warnings 0",\n`
|
|
402
|
-
: workspace?.eslintConfig !== undefined
|
|
403
|
-
? ' "lint": "eslint src test --max-warnings 0",\n'
|
|
404
|
-
: '';
|
|
405
495
|
return project(PACKAGE_JSON, composition)
|
|
406
|
-
.replace('__PACKAGE_NAME__',
|
|
496
|
+
.replace('__PACKAGE_NAME__', memberPackageName(composition, 'server'))
|
|
407
497
|
.replace('__KEYWORDS__', keywords)
|
|
408
498
|
.replace('__BIN__', binBlock(composition))
|
|
409
|
-
.replace('__LINT__',
|
|
499
|
+
.replace('__LINT__', lintScript(forMember(composition, 'server'), 'src test'))
|
|
410
500
|
.replace('__VITEST__', pinsVitest(composition) ? '~4.0.18' : '^4.0.0')
|
|
411
501
|
.replace('__PRIVATE__', composition.packaging.private ? ' "private": true,\n' : '')
|
|
412
|
-
.replace('__NPM_RUN__', npmRun(composition))
|
|
502
|
+
.replace('__NPM_RUN__', npmRun(composition, 'server'))
|
|
413
503
|
.replace('__FRAMEWORK_PIN__', frameworkPin(composition))
|
|
414
504
|
.replace('__DEPENDENCIES__', dependencyBlock(composition, ' '));
|
|
415
505
|
}
|
|
@@ -472,6 +562,23 @@ const TSCONFIG_COMPILER_OPTIONS = [
|
|
|
472
562
|
['forceConsistentCasingInFileNames', true],
|
|
473
563
|
['types', ['node']]
|
|
474
564
|
];
|
|
565
|
+
/** The protocol member's compiler options, pruned against a workspace base like the server's. */
|
|
566
|
+
const PROTOCOL_COMPILER_OPTIONS = [
|
|
567
|
+
['target', 'ES2022'],
|
|
568
|
+
['lib', ['ES2022']],
|
|
569
|
+
['module', 'NodeNext'],
|
|
570
|
+
['moduleResolution', 'NodeNext'],
|
|
571
|
+
['rootDir', 'src'],
|
|
572
|
+
['outDir', 'lib'],
|
|
573
|
+
['strict', true],
|
|
574
|
+
['skipLibCheck', true],
|
|
575
|
+
['declaration', true],
|
|
576
|
+
// The server's `tsc -b` builds this project through a reference, which
|
|
577
|
+
// `tsc` refuses unless the referenced project is composite.
|
|
578
|
+
['composite', true],
|
|
579
|
+
// No ambient Node types: a browser client compiles against this package.
|
|
580
|
+
['types', []]
|
|
581
|
+
];
|
|
475
582
|
/**
|
|
476
583
|
* The options that describe where THIS package's own files live. Never pruned,
|
|
477
584
|
* and emitted first in the workspace case: a base config that supplied them
|
|
@@ -500,19 +607,30 @@ function optionEquals(key, left, right) {
|
|
|
500
607
|
* the same key: inheriting `"module": "CommonJS"` where the scaffold needs
|
|
501
608
|
* `NodeNext` would break every emitted import, so a differing base value is a
|
|
502
609
|
* reason to keep the override rather than to trust the parent.
|
|
610
|
+
*
|
|
611
|
+
* `member` is the composition as that member sees it, so `extends` reaches the
|
|
612
|
+
* workspace base from the member's own directory.
|
|
503
613
|
*/
|
|
504
|
-
function tsconfigJson(
|
|
505
|
-
const workspace =
|
|
614
|
+
function tsconfigJson(member, compilerOptions, include, references = '') {
|
|
615
|
+
const workspace = member.packaging.workspace;
|
|
506
616
|
const base = workspace?.baseTsconfig === undefined ? undefined : (workspace.baseCompilerOptions ?? {});
|
|
507
617
|
const options = base === undefined
|
|
508
|
-
?
|
|
618
|
+
? compilerOptions
|
|
509
619
|
: [
|
|
510
|
-
...
|
|
511
|
-
...
|
|
620
|
+
...compilerOptions.filter(([key]) => PACKAGE_LOCAL_OPTIONS.includes(key)),
|
|
621
|
+
...compilerOptions.filter(([key, value]) => !PACKAGE_LOCAL_OPTIONS.includes(key) && !optionEquals(key, base[key], value))
|
|
512
622
|
];
|
|
513
623
|
const extendsLine = workspace?.baseTsconfig === undefined ? '' : ` "extends": "${workspace.baseTsconfig}",\n`;
|
|
514
624
|
const body = options.map(([key, value]) => ` "${key}": ${JSON.stringify(value)}`).join(',\n');
|
|
515
|
-
|
|
625
|
+
const referencesLine = references === '' ? '' : `,\n "references": ${references}`;
|
|
626
|
+
return `{\n${extendsLine} "compilerOptions": {\n${body}\n },\n "include": ${include}${referencesLine}\n}\n`;
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* The server's `tsconfig.json`. It references the protocol member, so `tsc -b`
|
|
630
|
+
* compiles protocol first, after `generate` has written its generated modules.
|
|
631
|
+
*/
|
|
632
|
+
function serverTsconfigJson(composition) {
|
|
633
|
+
return tsconfigJson(forMember(composition, 'server'), TSCONFIG_COMPILER_OPTIONS, '["src", "src/**/*.json"]', '[{ "path": "../protocol" }]');
|
|
516
634
|
}
|
|
517
635
|
/**
|
|
518
636
|
* The terminals every grammar needs: the shared fragment's text after its
|
|
@@ -705,14 +823,13 @@ function portCommandNames(composition) {
|
|
|
705
823
|
];
|
|
706
824
|
}
|
|
707
825
|
/**
|
|
708
|
-
* The socket heads' discovery commands,
|
|
826
|
+
* The socket heads' discovery commands, in the protocol member.
|
|
709
827
|
*
|
|
710
|
-
*
|
|
711
|
-
*
|
|
712
|
-
*
|
|
713
|
-
*
|
|
714
|
-
*
|
|
715
|
-
* rather than failing.
|
|
828
|
+
* A host shell has to name the same string the server publishes, and reaches
|
|
829
|
+
* it here without depending on the server. A host that retypes the literal
|
|
830
|
+
* instead gets no error when it drifts: the framework's port poll defaults to
|
|
831
|
+
* `findPortAttempts = -1`, so a wrong command retries forever rather than
|
|
832
|
+
* failing.
|
|
716
833
|
*/
|
|
717
834
|
function headPortsFile(composition) {
|
|
718
835
|
const upper = screamingSnake(composition.projectId);
|
|
@@ -736,35 +853,34 @@ function headPortsFile(composition) {
|
|
|
736
853
|
// process serves every registered grammar, so a language-derived name would tie
|
|
737
854
|
// a project-level endpoint to whichever grammar was scaffolded first.
|
|
738
855
|
//
|
|
739
|
-
//
|
|
740
|
-
//
|
|
741
|
-
//
|
|
742
|
-
//
|
|
743
|
-
// these instead, and assert any host-side copy against them.
|
|
856
|
+
// The server publishes them and a host shell has to name the same string to
|
|
857
|
+
// reach the head; both import them from here. A shell that retypes the literal
|
|
858
|
+
// gets no error when it drifts: the framework's port poll retries indefinitely
|
|
859
|
+
// by default.
|
|
744
860
|
|
|
745
861
|
${blocks.join('\n\n')}
|
|
746
862
|
`;
|
|
747
863
|
}
|
|
748
|
-
|
|
749
|
-
|
|
864
|
+
/**
|
|
865
|
+
* The server's root entry: the DI factory, the generated AST and each diagram
|
|
866
|
+
* module, all loadable in a browser worker.
|
|
867
|
+
*/
|
|
868
|
+
function indexFile(composition) {
|
|
869
|
+
const diagrams = composition.grammars.filter(grammar => grammar.diagram);
|
|
870
|
+
const diagramPhrase = diagrams.length === 0 ? '' : diagrams.length === 1 ? ', plus the diagram module' : ', plus the diagram modules';
|
|
871
|
+
const text = `// Public surface of the __NAME__ language server: the DI factory and the
|
|
872
|
+
// generated AST${diagramPhrase}.
|
|
750
873
|
//
|
|
751
|
-
//
|
|
752
|
-
//
|
|
753
|
-
//
|
|
874
|
+
// Everything here loads in a browser worker as well as in Node, so a host can
|
|
875
|
+
// compose the language wherever it runs it. \`createServices\`, which reads the
|
|
876
|
+
// real filesystem, is the package's \`./node\` entry instead. The entry points
|
|
877
|
+
// under \`src/\` are not re-exported either: each opens a transport at module
|
|
878
|
+
// scope, so importing one starts a server, which is why they are \`bin\` targets.
|
|
754
879
|
|
|
755
880
|
export * from './language-server/__PROJECT_ID__-module.js';
|
|
756
881
|
export * from './language-server/ast.js';
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
/**
|
|
760
|
-
* The public surface, plus the head-port commands when there is a socket head.
|
|
761
|
-
*
|
|
762
|
-
* The commands belong here rather than only in `head-ports.ts` so a host shell
|
|
763
|
-
* reaches them from the package root, the same way it reaches `createServices`.
|
|
764
|
-
*/
|
|
765
|
-
function indexFile(composition) {
|
|
766
|
-
const base = project(INDEX, composition);
|
|
767
|
-
return portCommandNames(composition).length ? `${base}export * from './head-ports.js';\n` : base;
|
|
882
|
+
${diagrams.map(grammar => `export * from './glsp/${grammar.grammarId}/diagram-module.js';\n`).join('')}`;
|
|
883
|
+
return project(text, composition);
|
|
768
884
|
}
|
|
769
885
|
/**
|
|
770
886
|
* The `#!` line every emitted `bin` target carries.
|
|
@@ -821,14 +937,14 @@ import { startGlspServer } from '@hydranium/glsp-server/node';
|
|
|
821
937
|
// \`TransferElement\` structurally, so naming the AST type here compiles fine and
|
|
822
938
|
// silently tells every typed client that a reference is a resolvable object
|
|
823
939
|
// rather than a name.
|
|
824
|
-
${importList(roots, '
|
|
940
|
+
${importList(roots, memberPackageName(composition, 'protocol'), columns, true)}
|
|
825
941
|
`
|
|
826
942
|
: '';
|
|
827
|
-
// The commands live in
|
|
828
|
-
// the same string to reach the head, and this file is an executable
|
|
829
|
-
// nothing can import from it.
|
|
943
|
+
// The commands live in the protocol member, not here: a host shell has to
|
|
944
|
+
// name the same string to reach the head, and this file is an executable
|
|
945
|
+
// entry, so nothing can import from it.
|
|
830
946
|
const portCommands = portCommandNames(composition).length
|
|
831
|
-
? importList(portCommandNames(composition), '
|
|
947
|
+
? importList(portCommandNames(composition), memberPackageName(composition, 'protocol'), columns) + '\n'
|
|
832
948
|
: '';
|
|
833
949
|
const dataBlock = data
|
|
834
950
|
? `
|
|
@@ -958,7 +1074,7 @@ function dataServerMainFile(composition) {
|
|
|
958
1074
|
${importList(['NodeFileSystem', 'startStdioServer'], '@hydranium/core/node', columns)}
|
|
959
1075
|
import { DataServer } from '@hydranium/data-server';
|
|
960
1076
|
// The TRANSFER root${plural}, not the AST one${plural} — same reasoning as \`main.ts\`.
|
|
961
|
-
${importList(roots, '
|
|
1077
|
+
${importList(roots, memberPackageName(composition, 'protocol'), columns, true)}
|
|
962
1078
|
import { create__NAME__Services } from './language-server/__PROJECT_ID__-module.js';
|
|
963
1079
|
|
|
964
1080
|
const { shared } = create__NAME__Services({ ...NodeFileSystem });
|
|
@@ -1006,7 +1122,7 @@ function servicesTest(composition) {
|
|
|
1006
1122
|
// is missing from the composition.
|
|
1007
1123
|
|
|
1008
1124
|
import { describe, expect, it } from 'vitest';
|
|
1009
|
-
import { createServices } from '../src/services.js';
|
|
1125
|
+
import { createServices } from '../src/node/services.js';
|
|
1010
1126
|
|
|
1011
1127
|
describe('__NAME__ services', () => {
|
|
1012
1128
|
it('composes the DI tree and registers ${title}', () => {
|
|
@@ -1085,8 +1201,8 @@ function serializationTest(composition) {
|
|
|
1085
1201
|
import { parseHelper } from '@hydranium/core/testing';
|
|
1086
1202
|
import { describe, expect, it } from 'vitest';
|
|
1087
1203
|
${importList(astTypes, '../src/language-server/ast.js', columns, true)}
|
|
1088
|
-
${importList(transferTypes, '
|
|
1089
|
-
import { createServices } from '../src/services.js';
|
|
1204
|
+
${importList(transferTypes, memberPackageName(composition, 'protocol'), columns, true)}
|
|
1205
|
+
import { createServices } from '../src/node/services.js';
|
|
1090
1206
|
|
|
1091
1207
|
${composition.grammars.map(grammar => render(SERIALIZATION_SUITE, composition, grammar)).join('\n\n')}
|
|
1092
1208
|
`;
|
|
@@ -1209,7 +1325,7 @@ function grammarTest(composition, header, imports, suite) {
|
|
|
1209
1325
|
import { parseHelper } from '@hydranium/core/testing';
|
|
1210
1326
|
import { describe, expect, it } from 'vitest';
|
|
1211
1327
|
${imports.join('\n')}${imports.length ? '\n' : ''}${importList(astTypes, '../src/language-server/ast.js', columns, true)}
|
|
1212
|
-
import { createServices } from '../src/services.js';
|
|
1328
|
+
import { createServices } from '../src/node/services.js';
|
|
1213
1329
|
|
|
1214
1330
|
${composition.grammars.map(grammar => render(suite, composition, grammar)).join('\n\n')}
|
|
1215
1331
|
`;
|
|
@@ -1236,6 +1352,7 @@ function readme(composition) {
|
|
|
1236
1352
|
const multi = composition.grammars.length > 1;
|
|
1237
1353
|
const data = composition.heads.includes('data');
|
|
1238
1354
|
const primaryExtension = composition.grammars[0].extensions[0];
|
|
1355
|
+
const diagramCount = composition.grammars.filter(grammar => grammar.diagram).length;
|
|
1239
1356
|
const grammarBullets = composition.grammars.map(grammar => `- \`src/grammar/${grammar.grammarId}.langium\` — the \`${grammar.grammar}\` grammar (.${grammar.extensions.join(', .')})`);
|
|
1240
1357
|
const lines = [
|
|
1241
1358
|
'# __NAME__',
|
|
@@ -1258,13 +1375,13 @@ function readme(composition) {
|
|
|
1258
1375
|
// nowhere else.
|
|
1259
1376
|
'```bash',
|
|
1260
1377
|
'# Grammar / AST reflection',
|
|
1261
|
-
'npx hydranium-cli reflect --services ./lib/services.js',
|
|
1378
|
+
'npx hydranium-cli reflect --services ./lib/node/services.js',
|
|
1262
1379
|
'',
|
|
1263
1380
|
'# Grammar-convention lint (CI gate)',
|
|
1264
|
-
'npx hydranium-cli lint-grammar --services ./lib/services.js',
|
|
1381
|
+
'npx hydranium-cli lint-grammar --services ./lib/node/services.js',
|
|
1265
1382
|
'',
|
|
1266
1383
|
'# Validate a workspace of model files (non-zero exit on errors)',
|
|
1267
|
-
'npx hydranium-cli validate --services ./lib/services.js
|
|
1384
|
+
'npx hydranium-cli validate --services ./lib/node/services.js ../workspace',
|
|
1268
1385
|
...(data
|
|
1269
1386
|
? [
|
|
1270
1387
|
'',
|
|
@@ -1275,8 +1392,8 @@ function readme(composition) {
|
|
|
1275
1392
|
'#',
|
|
1276
1393
|
'# The workspace goes to the ENTRY, not to `--cwd`: `--cwd` re-roots the child,',
|
|
1277
1394
|
'# so a relative entry path is refused by name (an absolute one is fine).',
|
|
1278
|
-
'npx hydranium-cli projects --server "node ./lib/data-server-main.js
|
|
1279
|
-
`npx hydranium-cli query --server "node ./lib/data-server-main.js
|
|
1395
|
+
'npx hydranium-cli projects --server "node ./lib/data-server-main.js ../workspace"',
|
|
1396
|
+
`npx hydranium-cli query --server "node ./lib/data-server-main.js ../workspace" --uri <file:// URI of ../workspace/nodes.${primaryExtension}>`
|
|
1280
1397
|
]
|
|
1281
1398
|
: []),
|
|
1282
1399
|
'```',
|
|
@@ -1287,8 +1404,8 @@ function readme(composition) {
|
|
|
1287
1404
|
...(multi ? ['- `src/grammar/common.langium` — shared terminals, imported by each grammar (not a language)'] : []),
|
|
1288
1405
|
'- `src/language-server/__PROJECT_ID__-module.ts` — `create__NAME__Services` DI wiring',
|
|
1289
1406
|
...composition.grammars.map(grammar => `- \`src/language-server/${grammar.grammarId}-serializer.ts\` — emits \`${grammar.grammar}\` back to text (the framework defaults this to a throw)`),
|
|
1290
|
-
|
|
1291
|
-
|
|
1407
|
+
"- `src/node/services.ts` — zero-arg `createServices()` for the headless CLI, the package's `./node` entry",
|
|
1408
|
+
`- \`src/index.ts\` — the package root, loadable in a browser worker: DI factory + generated AST${diagramCount === 0 ? '' : diagramCount === 1 ? ' + diagram module' : ' + diagram modules'}`,
|
|
1292
1409
|
`- \`src/main.ts\` — starts ${composition.heads.join(' + ')}, the \`__PROJECT_ID__\` bin entry`,
|
|
1293
1410
|
...(data
|
|
1294
1411
|
? [
|
|
@@ -1298,7 +1415,8 @@ function readme(composition) {
|
|
|
1298
1415
|
: []),
|
|
1299
1416
|
...composition.grammars
|
|
1300
1417
|
.filter(grammar => grammar.diagram)
|
|
1301
|
-
.map(grammar => `- \`src/glsp/${grammar.grammarId}/\` — the \`${grammar.grammar}\` diagram:
|
|
1418
|
+
.map(grammar => `- \`src/glsp/${grammar.grammarId}/\` — the \`${grammar.grammar}\` diagram: state, storage, submission handler, GModel factory, configuration, create-node handler, DI module`),
|
|
1419
|
+
`- \`../protocol/\` — the \`${memberPackageName(composition, 'protocol')}\` package: what this server and its clients both import (language ids, ${portCommandNames(composition).length ? 'port commands, ' : ''}${composition.grammars.some(grammar => grammar.diagram) ? 'diagram ids, ' : ''}the transfer model), written partly by \`npm run generate\``,
|
|
1302
1420
|
'- `test/services.test.ts` — the DI tree composes; grows as your language does',
|
|
1303
1421
|
'- `test/parsing.test.ts` / `linking.test.ts` / `validating.test.ts` — the three tiers a new language breaks first',
|
|
1304
1422
|
'- `test/serialization.test.ts` — each grammar round-trips through its serializer',
|
|
@@ -1403,9 +1521,9 @@ function readme(composition) {
|
|
|
1403
1521
|
*/
|
|
1404
1522
|
function glspFiles(composition, grammar) {
|
|
1405
1523
|
const columns = columnsFor(composition);
|
|
1406
|
-
const fixed = (file) => grammarTemplate(`src/glsp/__GRAMMAR_ID__/${file}`, composition, grammar);
|
|
1524
|
+
const fixed = (file) => grammarTemplate(`server/src/glsp/__GRAMMAR_ID__/${file}`, composition, grammar);
|
|
1407
1525
|
const computed = (file, text) => ({
|
|
1408
|
-
path: `src/glsp/${grammar.grammarId}/${file}`,
|
|
1526
|
+
path: `server/src/glsp/${grammar.grammarId}/${file}`,
|
|
1409
1527
|
content: render(text, composition, grammar)
|
|
1410
1528
|
});
|
|
1411
1529
|
const configuration = `// Diagram configuration for the __GRAMMAR__ diagram.
|
|
@@ -1427,7 +1545,7 @@ function glspFiles(composition, grammar) {
|
|
|
1427
1545
|
import { type GModelElementConstructor } from '@eclipse-glsp/graph';
|
|
1428
1546
|
${importList(['type DiagramConfiguration', 'type EdgeTypeHint', 'ServerLayoutKind', 'type ShapeTypeHint', 'getDefaultMapping'], '@eclipse-glsp/server', columns)}
|
|
1429
1547
|
import { injectable } from 'inversify';
|
|
1430
|
-
import { __GRAMMAR_ID_CONST___EDGE_TYPE, __GRAMMAR_ID_CONST___NODE_TYPE } from '
|
|
1548
|
+
import { __GRAMMAR_ID_CONST___EDGE_TYPE, __GRAMMAR_ID_CONST___NODE_TYPE } from '__PROTOCOL_PACKAGE__';
|
|
1431
1549
|
|
|
1432
1550
|
@injectable()
|
|
1433
1551
|
export class __GRAMMAR__DiagramConfiguration implements DiagramConfiguration {
|
|
@@ -1489,7 +1607,7 @@ import { __GRAMMAR__GModelFactory } from './gmodel-factory.js';
|
|
|
1489
1607
|
import { __GRAMMAR__GlspState } from './state.js';
|
|
1490
1608
|
import { __GRAMMAR__GlspStorage } from './storage.js';
|
|
1491
1609
|
import { __GRAMMAR__SubmissionHandler } from './submission-handler.js';
|
|
1492
|
-
import { __GRAMMAR_ID_CONST___DIAGRAM_TYPE } from '
|
|
1610
|
+
import { __GRAMMAR_ID_CONST___DIAGRAM_TYPE } from '__PROTOCOL_PACKAGE__';
|
|
1493
1611
|
|
|
1494
1612
|
export class __GRAMMAR__DiagramModule extends AbstractHydraniumGlspDiagramModule {
|
|
1495
1613
|
readonly diagramType = __GRAMMAR_ID_CONST___DIAGRAM_TYPE;
|
|
@@ -1531,7 +1649,6 @@ export class __GRAMMAR__DiagramModule extends AbstractHydraniumGlspDiagramModule
|
|
|
1531
1649
|
}
|
|
1532
1650
|
`;
|
|
1533
1651
|
return [
|
|
1534
|
-
fixed('types.ts'),
|
|
1535
1652
|
fixed('state.ts'),
|
|
1536
1653
|
fixed('storage.ts'),
|
|
1537
1654
|
fixed('submission-handler.ts'),
|
|
@@ -1539,32 +1656,32 @@ export class __GRAMMAR__DiagramModule extends AbstractHydraniumGlspDiagramModule
|
|
|
1539
1656
|
computed('diagram-configuration.ts', configuration),
|
|
1540
1657
|
fixed('create-node-operation-handler.ts'),
|
|
1541
1658
|
computed('diagram-module.ts', module),
|
|
1542
|
-
grammarTemplate('test/__GRAMMAR_ID__-diagram.test.ts', composition, grammar)
|
|
1659
|
+
grammarTemplate('server/test/__GRAMMAR_ID__-diagram.test.ts', composition, grammar)
|
|
1543
1660
|
];
|
|
1544
1661
|
}
|
|
1545
1662
|
/**
|
|
1546
|
-
*
|
|
1663
|
+
* The server member: the grammar, the language and its heads.
|
|
1547
1664
|
*
|
|
1548
1665
|
* Grammar-count-dependent emissions live here rather than in a template string:
|
|
1549
1666
|
* one `.langium` file per grammar, and the shared `common.langium` fragment only
|
|
1550
1667
|
* when there is more than one grammar to share it.
|
|
1551
1668
|
*/
|
|
1552
|
-
|
|
1669
|
+
function serverFiles(composition) {
|
|
1553
1670
|
const multi = composition.grammars.length > 1;
|
|
1554
1671
|
const grammarFiles = composition.grammars.map(grammar => ({
|
|
1555
|
-
path: `src/grammar/${grammar.grammarId}.langium`,
|
|
1672
|
+
path: `server/src/grammar/${grammar.grammarId}.langium`,
|
|
1556
1673
|
content: render(grammarFile(multi), composition, grammar)
|
|
1557
1674
|
}));
|
|
1558
1675
|
// The shared lexical fragment, only for several grammars: each declaring its
|
|
1559
1676
|
// own `WS` / `ID` lets the token sets drift apart.
|
|
1560
1677
|
if (multi) {
|
|
1561
|
-
grammarFiles.unshift(projectTemplate('src/grammar/common.langium', composition));
|
|
1678
|
+
grammarFiles.unshift(projectTemplate('server/src/grammar/common.langium', composition));
|
|
1562
1679
|
}
|
|
1563
1680
|
const diagramFiles = composition.grammars.filter(grammar => grammar.diagram).flatMap(grammar => glspFiles(composition, grammar));
|
|
1564
1681
|
return [
|
|
1565
|
-
{ path: 'package.json', content: packageJson(composition) },
|
|
1566
|
-
{ path: 'langium-config.json', content: langiumConfig(composition) },
|
|
1567
|
-
{ path: 'tsconfig.json', content:
|
|
1682
|
+
{ path: 'server/package.json', content: packageJson(composition) },
|
|
1683
|
+
{ path: 'server/langium-config.json', content: langiumConfig(composition) },
|
|
1684
|
+
{ path: 'server/tsconfig.json', content: serverTsconfigJson(composition) },
|
|
1568
1685
|
// `isolatedModules` is what makes this check agree with the transform that
|
|
1569
1686
|
// actually runs the tests. `npm test` is `typecheck:test && vitest run`, and
|
|
1570
1687
|
// vitest compiles each file on its own — a transform with no type
|
|
@@ -1573,43 +1690,193 @@ export function buildInitTemplates(composition) {
|
|
|
1573
1690
|
// transform then emits as a real import of a symbol that does not exist at
|
|
1574
1691
|
// runtime, so the typecheck passes and the suite fails with a confusing
|
|
1575
1692
|
// missing-export.
|
|
1576
|
-
projectTemplate('tsconfig.test.json', composition),
|
|
1693
|
+
projectTemplate('server/tsconfig.test.json', composition),
|
|
1577
1694
|
// Found by name, unlike `tsconfig.test.json`. Under vite 8, vitest's transform
|
|
1578
1695
|
// applies a tsconfig only to the files its own `include` lists, so without this
|
|
1579
1696
|
// a test declaring a decorated class fails to load.
|
|
1580
|
-
projectTemplate('test/tsconfig.json', composition),
|
|
1581
|
-
projectTemplate('vitest.config.ts', composition),
|
|
1697
|
+
projectTemplate('server/test/tsconfig.json', composition),
|
|
1698
|
+
projectTemplate('server/vitest.config.ts', composition),
|
|
1582
1699
|
// Written for a workspace member too: its root may not ignore `lib/` (a `yo
|
|
1583
1700
|
// langium` root ignores `out/`), and a rule the root repeats costs nothing.
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
{ path: '.gitignore', content: template('server/gitignore') },
|
|
1587
|
-
{ path: 'README.md', content: readme(composition) },
|
|
1701
|
+
ignoreFile('server'),
|
|
1702
|
+
{ path: 'server/README.md', content: readme(composition) },
|
|
1588
1703
|
...grammarFiles,
|
|
1589
|
-
{ path: `src/language-server/${composition.projectId}-module.ts`, content: moduleFile(composition) },
|
|
1590
|
-
grammarTemplate('src/language-server/ast.ts', composition, composition.grammars[0]),
|
|
1704
|
+
{ path: `server/src/language-server/${composition.projectId}-module.ts`, content: moduleFile(composition) },
|
|
1705
|
+
grammarTemplate('server/src/language-server/ast.ts', composition, composition.grammars[0]),
|
|
1591
1706
|
// Per grammar, because `services.serializer.Serializer` is a per-language slot
|
|
1592
1707
|
// and a serializer is grammar-shaped by definition. That is also why the
|
|
1593
1708
|
// framework refuses to default it: its `UnboundSerializer` throws, naming the
|
|
1594
1709
|
// binding to add, rather than guessing a syntax.
|
|
1595
|
-
...composition.grammars.map(grammar => grammarTemplate('src/language-server/__GRAMMAR_ID__-serializer.ts', composition, grammar)),
|
|
1596
|
-
{ path: 'src/index.ts', content: indexFile(composition) },
|
|
1597
|
-
projectTemplate('src/services.ts', composition),
|
|
1598
|
-
{ path: 'src/main.ts', content: mainFile(composition) },
|
|
1599
|
-
...(composition.heads.includes('data') ? [{ path: 'src/data-server-main.ts', content: dataServerMainFile(composition) }] : []),
|
|
1600
|
-
...(portCommandNames(composition).length ? [{ path: 'src/head-ports.ts', content: headPortsFile(composition) }] : []),
|
|
1710
|
+
...composition.grammars.map(grammar => grammarTemplate('server/src/language-server/__GRAMMAR_ID__-serializer.ts', composition, grammar)),
|
|
1711
|
+
{ path: 'server/src/index.ts', content: indexFile(composition) },
|
|
1712
|
+
projectTemplate('server/src/node/services.ts', composition),
|
|
1713
|
+
{ path: 'server/src/main.ts', content: mainFile(composition) },
|
|
1714
|
+
...(composition.heads.includes('data') ? [{ path: 'server/src/data-server-main.ts', content: dataServerMainFile(composition) }] : []),
|
|
1601
1715
|
...diagramFiles,
|
|
1602
|
-
{ path: 'test/services.test.ts', content: servicesTest(composition) },
|
|
1603
|
-
{ path: 'test/parsing.test.ts', content: grammarTest(composition, PARSING_HEADER, [], PARSING_SUITE) },
|
|
1716
|
+
{ path: 'server/test/services.test.ts', content: servicesTest(composition) },
|
|
1717
|
+
{ path: 'server/test/parsing.test.ts', content: grammarTest(composition, PARSING_HEADER, [], PARSING_SUITE) },
|
|
1604
1718
|
{
|
|
1605
|
-
path: 'test/linking.test.ts',
|
|
1719
|
+
path: 'server/test/linking.test.ts',
|
|
1606
1720
|
content: grammarTest(composition, LINKING_HEADER, ["import { AstUtils } from '@hydranium/langium';"], LINKING_SUITE)
|
|
1607
1721
|
},
|
|
1608
1722
|
{
|
|
1609
|
-
path: 'test/validating.test.ts',
|
|
1723
|
+
path: 'server/test/validating.test.ts',
|
|
1610
1724
|
content: grammarTest(composition, VALIDATING_HEADER, ["import { DiagnosticSeverity } from 'vscode-languageserver';"], VALIDATING_SUITE)
|
|
1611
1725
|
},
|
|
1612
|
-
{ path: 'test/serialization.test.ts', content: serializationTest(composition) }
|
|
1726
|
+
{ path: 'server/test/serialization.test.ts', content: serializationTest(composition) }
|
|
1727
|
+
];
|
|
1728
|
+
}
|
|
1729
|
+
/**
|
|
1730
|
+
* The protocol member's `package.json`.
|
|
1731
|
+
*
|
|
1732
|
+
* Its `build` runs the server's `generate` first, because the server writes
|
|
1733
|
+
* this package's `generated-hydranium/` modules from the grammar: a runner that
|
|
1734
|
+
* builds in dependency order reaches this package before the server, and would
|
|
1735
|
+
* otherwise compile modules that do not exist yet. A full build therefore
|
|
1736
|
+
* generates twice; the second pass rewrites nothing.
|
|
1737
|
+
*/
|
|
1738
|
+
function protocolPackageJson(composition) {
|
|
1739
|
+
const member = forMember(composition, 'protocol');
|
|
1740
|
+
const dependencies = {};
|
|
1741
|
+
for (const head of composition.heads) {
|
|
1742
|
+
Object.assign(dependencies, PROTOCOL_THIRD_PARTY_DEPENDENCIES[head]);
|
|
1743
|
+
}
|
|
1744
|
+
const dependencyLines = Object.keys(dependencies).length === 0 ? '' : ` "dependencies": {\n${sortedBlock(dependencies, ' ')}\n },\n`;
|
|
1745
|
+
return `{
|
|
1746
|
+
"name": "${memberPackageName(composition, 'protocol')}",
|
|
1747
|
+
"version": "0.0.0",
|
|
1748
|
+
${composition.packaging.private ? ' "private": true,\n' : ''} "description": "What the ${composition.name} language server and its clients share: language ids, wire types and diagram ids.",
|
|
1749
|
+
"license": "UNLICENSED",
|
|
1750
|
+
"author": "",
|
|
1751
|
+
"files": [
|
|
1752
|
+
"lib",
|
|
1753
|
+
"src"
|
|
1754
|
+
],
|
|
1755
|
+
"type": "module",
|
|
1756
|
+
"main": "lib/index.js",
|
|
1757
|
+
"types": "lib/index.d.ts",
|
|
1758
|
+
"scripts": {
|
|
1759
|
+
"build": "npm --prefix ../server run generate && tsc -b",
|
|
1760
|
+
"clean": "rimraf lib tsconfig.tsbuildinfo",
|
|
1761
|
+
${lintScript(member, 'src')} "watch": "tsc -b --watch"
|
|
1762
|
+
},
|
|
1763
|
+
${dependencyLines} "devDependencies": {
|
|
1764
|
+
"rimraf": "^5.0.0",
|
|
1765
|
+
"typescript": "^7.0.2"
|
|
1766
|
+
},
|
|
1767
|
+
"engines": {
|
|
1768
|
+
"node": ">=22.13"
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
`;
|
|
1772
|
+
}
|
|
1773
|
+
/** The protocol member's root entry, one re-export per module. */
|
|
1774
|
+
function protocolIndexFile(composition) {
|
|
1775
|
+
const modules = [
|
|
1776
|
+
'./generated-hydranium/languages.js',
|
|
1777
|
+
'./generated-hydranium/transfer-model.js',
|
|
1778
|
+
...(portCommandNames(composition).length ? ['./head-ports.js'] : []),
|
|
1779
|
+
...composition.grammars.filter(grammar => grammar.diagram).map(grammar => `./${grammar.grammarId}-diagram-types.js`)
|
|
1780
|
+
];
|
|
1781
|
+
const text = `// What the __NAME__ server and its clients both import. A client depends on
|
|
1782
|
+
// this package instead of the server, so it never installs Langium to reach a
|
|
1783
|
+
// constant or a wire type.
|
|
1784
|
+
//
|
|
1785
|
+
// The server's \`generate\` writes \`generated-hydranium/\` from the grammar;
|
|
1786
|
+
// commit those modules with the grammar change that produced them.
|
|
1787
|
+
|
|
1788
|
+
${modules.map(module => `export * from '${module}';`).join('\n')}
|
|
1789
|
+
`;
|
|
1790
|
+
return project(text, composition);
|
|
1791
|
+
}
|
|
1792
|
+
/** The protocol member's README. */
|
|
1793
|
+
function protocolReadme(composition) {
|
|
1794
|
+
const lines = [
|
|
1795
|
+
`# ${memberPackageName(composition, 'protocol')}`,
|
|
1796
|
+
'',
|
|
1797
|
+
'What the __NAME__ language server and its clients both import, so a client',
|
|
1798
|
+
'never depends on the server or installs Langium.',
|
|
1799
|
+
'',
|
|
1800
|
+
'- `src/generated-hydranium/languages.ts` — each language id and its file extensions',
|
|
1801
|
+
'- `src/generated-hydranium/transfer-model.ts` — the wire types of the model, references as plain strings',
|
|
1802
|
+
...(portCommandNames(composition).length ? ['- `src/head-ports.ts` — the LSP requests a host sends to find the socket heads'] : []),
|
|
1803
|
+
...composition.grammars
|
|
1804
|
+
.filter(grammar => grammar.diagram)
|
|
1805
|
+
.map(grammar => `- \`src/${grammar.grammarId}-diagram-types.ts\` — the \`${grammar.grammar}\` diagram's type ids`),
|
|
1806
|
+
'',
|
|
1807
|
+
"The server's `npm run generate` writes `src/generated-hydranium/` from the",
|
|
1808
|
+
'grammar, and `npm run build` here runs it first. Commit the generated',
|
|
1809
|
+
'modules with the grammar change that produced them.',
|
|
1810
|
+
''
|
|
1811
|
+
];
|
|
1812
|
+
return project(lines.join('\n'), composition);
|
|
1813
|
+
}
|
|
1814
|
+
/** The protocol member: everything the server and its clients both import. */
|
|
1815
|
+
function protocolFiles(composition) {
|
|
1816
|
+
return [
|
|
1817
|
+
{ path: 'protocol/package.json', content: protocolPackageJson(composition) },
|
|
1818
|
+
{ path: 'protocol/tsconfig.json', content: tsconfigJson(forMember(composition, 'protocol'), PROTOCOL_COMPILER_OPTIONS, '["src"]') },
|
|
1819
|
+
ignoreFile('protocol'),
|
|
1820
|
+
{ path: 'protocol/README.md', content: protocolReadme(composition) },
|
|
1821
|
+
{ path: 'protocol/src/index.ts', content: protocolIndexFile(composition) },
|
|
1822
|
+
...(portCommandNames(composition).length ? [{ path: 'protocol/src/head-ports.ts', content: headPortsFile(composition) }] : []),
|
|
1823
|
+
...composition.grammars
|
|
1824
|
+
.filter(grammar => grammar.diagram)
|
|
1825
|
+
.map(grammar => grammarTemplate('protocol/src/__GRAMMAR_ID__-diagram-types.ts', composition, grammar))
|
|
1826
|
+
];
|
|
1827
|
+
}
|
|
1828
|
+
/**
|
|
1829
|
+
* The starter model files every host and the CLI open, two per language: one
|
|
1830
|
+
* declaring nodes, one referencing a node in the other file.
|
|
1831
|
+
*
|
|
1832
|
+
* Named by extension alone, since `init` refuses two grammars sharing one.
|
|
1833
|
+
*/
|
|
1834
|
+
function workspaceFiles(composition) {
|
|
1835
|
+
return composition.grammars.flatMap(grammar => [
|
|
1836
|
+
grammarTemplate('workspace/nodes.__EXTENSION__', composition, grammar),
|
|
1837
|
+
grammarTemplate('workspace/references.__EXTENSION__', composition, grammar)
|
|
1838
|
+
]);
|
|
1839
|
+
}
|
|
1840
|
+
/**
|
|
1841
|
+
* A standalone project's root: the members as npm workspaces.
|
|
1842
|
+
*
|
|
1843
|
+
* Always private: it is never published, whatever `--public` makes of the
|
|
1844
|
+
* members. npm runs a script across the members in the order listed here, not
|
|
1845
|
+
* by their dependencies; listing protocol first is safe because its `build`
|
|
1846
|
+
* runs the server's `generate` before compiling.
|
|
1847
|
+
*/
|
|
1848
|
+
function rootFiles(composition) {
|
|
1849
|
+
const content = `{
|
|
1850
|
+
"name": "${composition.projectId}-root",
|
|
1851
|
+
"version": "0.0.0",
|
|
1852
|
+
"private": true,
|
|
1853
|
+
"workspaces": [
|
|
1854
|
+
"protocol",
|
|
1855
|
+
"server"
|
|
1856
|
+
],
|
|
1857
|
+
"scripts": {
|
|
1858
|
+
"build": "npm run build --workspaces",
|
|
1859
|
+
"clean": "npm run clean --workspaces --if-present",
|
|
1860
|
+
"test": "npm test --workspaces --if-present"
|
|
1861
|
+
},
|
|
1862
|
+
"engines": {
|
|
1863
|
+
"node": ">=22.13"
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
`;
|
|
1867
|
+
return [{ path: 'package.json', content }, ignoreFile('')];
|
|
1868
|
+
}
|
|
1869
|
+
/**
|
|
1870
|
+
* Every file the scaffold emits, fully rendered, at its path in the project
|
|
1871
|
+
* directory. A workspace member's project gets no root of its own: it joins the
|
|
1872
|
+
* surrounding workspace, whose manifest `init` prints the changes for.
|
|
1873
|
+
*/
|
|
1874
|
+
export function buildInitTemplates(composition) {
|
|
1875
|
+
return [
|
|
1876
|
+
...(composition.packaging.workspace === undefined ? rootFiles(composition) : []),
|
|
1877
|
+
...protocolFiles(composition),
|
|
1878
|
+
...serverFiles(composition),
|
|
1879
|
+
...workspaceFiles(composition)
|
|
1613
1880
|
];
|
|
1614
1881
|
}
|
|
1615
1882
|
//# sourceMappingURL=init-templates.js.map
|