@hydranium/cli 1.0.0-next.308 → 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
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
* `bin` key, the DI module filename, and the data-server port command.
|
|
23
23
|
* - `__CONFIG_ROOT_CONST__` — SCREAMING_SNAKE project id, naming the
|
|
24
24
|
* `lsp.configurationRoot` constant a multi-grammar project binds.
|
|
25
|
+
* - `__PROTOCOL_PACKAGE__` — the protocol member's package name, scope
|
|
26
|
+
* included, which the server and every client import ids and wire types
|
|
27
|
+
* from.
|
|
25
28
|
*
|
|
26
29
|
* GRAMMAR tokens, substituted once per grammar:
|
|
27
30
|
*
|
|
@@ -51,9 +54,16 @@
|
|
|
51
54
|
* generator emits `interface <projectName>Element` as the base type every rule
|
|
52
55
|
* extends, which a rule of that name would collide with.
|
|
53
56
|
*
|
|
57
|
+
* The project is two packages and a starter workspace, each in its own
|
|
58
|
+
* directory: `server/` (the grammar, the language and its heads), `protocol/`
|
|
59
|
+
* (what the server and its clients both import, so a client never installs
|
|
60
|
+
* Langium), and `workspace/` (starter model files every host and the CLI
|
|
61
|
+
* open). A standalone project adds a root manifest naming them as npm
|
|
62
|
+
* workspaces; a workspace member joins the surrounding root instead.
|
|
63
|
+
*
|
|
54
64
|
* A file that is fixed text with tokens lives in the `templates/` tree the
|
|
55
65
|
* package ships beside `lib`, at the path it is emitted at, tokens included
|
|
56
|
-
* (
|
|
66
|
+
* (each ignore file without its dot); one computed from the composition is
|
|
57
67
|
* built here. The tree is read synchronously, so `runInit` stays synchronous,
|
|
58
68
|
* and it is neither formatted with the repo's settings nor given a license
|
|
59
69
|
* header, since either would change what `init` emits.
|
|
@@ -78,7 +88,7 @@
|
|
|
78
88
|
|
|
79
89
|
import { readFileSync } from 'node:fs';
|
|
80
90
|
import { readCliVersion } from '../cli-version.js';
|
|
81
|
-
import type { InitComposition, InitFile, InitGrammarNames, InitHead } from './init.js';
|
|
91
|
+
import type { InitComposition, InitFile, InitGrammarNames, InitHead, InitMember } from './init.js';
|
|
82
92
|
import type { JsonValue } from './init-workspace.js';
|
|
83
93
|
|
|
84
94
|
/** One template file: its path (tokens allowed) and its content (tokens allowed). */
|
|
@@ -117,12 +127,49 @@ function screamingSnake(projectId: string): string {
|
|
|
117
127
|
return projectId.replace(/-/g, '_').toUpperCase();
|
|
118
128
|
}
|
|
119
129
|
|
|
130
|
+
/**
|
|
131
|
+
* A member's package name: `<id>-<member>`, under the scope when there is one.
|
|
132
|
+
*
|
|
133
|
+
* Suffixed even for the server, because a project of several packages has no
|
|
134
|
+
* member entitled to the bare id, and a later client or host would otherwise
|
|
135
|
+
* read as a part of the server.
|
|
136
|
+
*/
|
|
137
|
+
export function memberPackageName(composition: InitComposition, member: InitMember): string {
|
|
138
|
+
const { scope } = composition.packaging;
|
|
139
|
+
const name = `${composition.projectId}-${member}`;
|
|
140
|
+
return scope === undefined ? name : `${scope}/${name}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The composition as one member sees it: inside a workspace, its placement
|
|
145
|
+
* sits one directory below the project's, so its `extends` and its lint
|
|
146
|
+
* config path reach one level further up.
|
|
147
|
+
*/
|
|
148
|
+
function forMember(composition: InitComposition, member: InitMember): InitComposition {
|
|
149
|
+
const workspace = composition.packaging.workspace;
|
|
150
|
+
if (workspace === undefined) {
|
|
151
|
+
return composition;
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
...composition,
|
|
155
|
+
packaging: {
|
|
156
|
+
...composition.packaging,
|
|
157
|
+
workspace: {
|
|
158
|
+
...workspace,
|
|
159
|
+
targetPath: `${workspace.targetPath}/${member}`,
|
|
160
|
+
baseTsconfig: workspace.baseTsconfig === undefined ? undefined : `../${workspace.baseTsconfig}`
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
120
166
|
/** Substitute the project-tier tokens. */
|
|
121
167
|
function project(text: string, composition: InitComposition): string {
|
|
122
168
|
return text
|
|
123
169
|
.replace(/__NAME__/g, composition.name)
|
|
124
170
|
.replace(/__PROJECT_ID__/g, composition.projectId)
|
|
125
|
-
.replace(/__CONFIG_ROOT_CONST__/g, screamingSnake(composition.projectId))
|
|
171
|
+
.replace(/__CONFIG_ROOT_CONST__/g, screamingSnake(composition.projectId))
|
|
172
|
+
.replace(/__PROTOCOL_PACKAGE__/g, memberPackageName(composition, 'protocol'));
|
|
126
173
|
}
|
|
127
174
|
|
|
128
175
|
/**
|
|
@@ -157,14 +204,20 @@ function template(path: string): string {
|
|
|
157
204
|
return readFileSync(new URL(path, TEMPLATES), 'utf-8');
|
|
158
205
|
}
|
|
159
206
|
|
|
160
|
-
/** A
|
|
207
|
+
/** A template emitted once per project at its own path, rendered with the project tokens. */
|
|
161
208
|
function projectTemplate(path: string, composition: InitComposition): InitFile {
|
|
162
|
-
return { path, content: project(template(
|
|
209
|
+
return { path, content: project(template(path), composition) };
|
|
163
210
|
}
|
|
164
211
|
|
|
165
|
-
/** A
|
|
212
|
+
/** A template emitted once per grammar, its path rendered like its content. */
|
|
166
213
|
function grammarTemplate(path: string, composition: InitComposition, grammar: InitGrammarNames): InitFile {
|
|
167
|
-
return { path: render(path, composition, grammar), content: render(template(
|
|
214
|
+
return { path: render(path, composition, grammar), content: render(template(path), composition, grammar) };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** An ignore file, stored without its dot because `npm pack` drops a nested `.gitignore` and applies it as an ignore file. */
|
|
218
|
+
function ignoreFile(directory: string): InitFile {
|
|
219
|
+
const prefix = directory === '' ? '' : `${directory}/`;
|
|
220
|
+
return { path: `${prefix}.gitignore`, content: template(`${prefix}gitignore`) };
|
|
168
221
|
}
|
|
169
222
|
|
|
170
223
|
/** A comma-separated list of single-quoted TypeScript/JS string literals. */
|
|
@@ -260,6 +313,17 @@ const HEAD_THIRD_PARTY_DEPENDENCIES: Record<InitHead, Readonly<Record<string, st
|
|
|
260
313
|
}
|
|
261
314
|
};
|
|
262
315
|
|
|
316
|
+
/**
|
|
317
|
+
* What each head adds to the protocol member, which carries only what a
|
|
318
|
+
* client may install. Keyed by head because the `glsp` head implies a diagram,
|
|
319
|
+
* whose element ids extend GLSP's default type names.
|
|
320
|
+
*/
|
|
321
|
+
const PROTOCOL_THIRD_PARTY_DEPENDENCIES: Record<InitHead, Readonly<Record<string, string>>> = {
|
|
322
|
+
lsp: {},
|
|
323
|
+
data: {},
|
|
324
|
+
glsp: { '@eclipse-glsp/protocol': '2.7.0' }
|
|
325
|
+
};
|
|
326
|
+
|
|
263
327
|
/**
|
|
264
328
|
* The exact pins of `heads`, for a workspace root to declare too. npm installs a
|
|
265
329
|
* root's own dependencies at the top of the tree, so another member's version of
|
|
@@ -268,7 +332,7 @@ const HEAD_THIRD_PARTY_DEPENDENCIES: Record<InitHead, Readonly<Record<string, st
|
|
|
268
332
|
*/
|
|
269
333
|
export function workspaceRootPins(heads: readonly InitHead[]): ReadonlyArray<readonly [string, string]> {
|
|
270
334
|
return heads
|
|
271
|
-
.flatMap(head => Object.entries(HEAD_THIRD_PARTY_DEPENDENCIES[head]))
|
|
335
|
+
.flatMap(head => [...Object.entries(HEAD_THIRD_PARTY_DEPENDENCIES[head]), ...Object.entries(PROTOCOL_THIRD_PARTY_DEPENDENCIES[head])])
|
|
272
336
|
.filter(([, version]) => /^\d+\.\d+\.\d+$/.test(version))
|
|
273
337
|
.sort(([left], [right]) => left.localeCompare(right));
|
|
274
338
|
}
|
|
@@ -291,19 +355,32 @@ function pinsVitest(composition: InitComposition): boolean {
|
|
|
291
355
|
return !(installsVitest41(workspace?.packageManagerNpm) && installsVitest41(workspace?.npmVersion));
|
|
292
356
|
}
|
|
293
357
|
|
|
294
|
-
/**
|
|
358
|
+
/**
|
|
359
|
+
* The version a member pins a sibling member at: exact, at the version every
|
|
360
|
+
* member starts with. npm links a sibling only when the range admits its local
|
|
361
|
+
* version, and otherwise looks for it in the registry, where it was never
|
|
362
|
+
* published.
|
|
363
|
+
*/
|
|
364
|
+
const MEMBER_PIN = '0.0.0';
|
|
365
|
+
|
|
366
|
+
/** A dependency block, sorted as npm writes it. */
|
|
367
|
+
function sortedBlock(dependencies: Readonly<Record<string, string>>, indent: string): string {
|
|
368
|
+
return Object.keys(dependencies)
|
|
369
|
+
.sort()
|
|
370
|
+
.map(name => `${indent}"${name}": "${dependencies[name]}"`)
|
|
371
|
+
.join(',\n');
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** The server's dependency block: the head set's packages and the protocol member. */
|
|
295
375
|
function dependencyBlock(composition: InitComposition, indent: string): string {
|
|
296
|
-
const merged: Record<string, string> = {};
|
|
376
|
+
const merged: Record<string, string> = { [memberPackageName(composition, 'protocol')]: MEMBER_PIN };
|
|
297
377
|
for (const head of composition.heads) {
|
|
298
378
|
Object.assign(merged, HEAD_THIRD_PARTY_DEPENDENCIES[head]);
|
|
299
379
|
for (const framework of HEAD_FRAMEWORK_DEPENDENCIES[head]) {
|
|
300
380
|
merged[framework] = frameworkPin(composition);
|
|
301
381
|
}
|
|
302
382
|
}
|
|
303
|
-
return
|
|
304
|
-
.sort()
|
|
305
|
-
.map(name => `${indent}"${name}": "${merged[name]}"`)
|
|
306
|
-
.join(',\n');
|
|
383
|
+
return sortedBlock(merged, indent);
|
|
307
384
|
}
|
|
308
385
|
|
|
309
386
|
// JSON at two-space indent, not the three the `.ts` templates use: that is the
|
|
@@ -330,6 +407,11 @@ function dependencyBlock(composition: InitComposition, indent: string): string {
|
|
|
330
407
|
// from this package's own grammar and can reach a consumer from nowhere else; a
|
|
331
408
|
// listed path that does not exist yet is inert.
|
|
332
409
|
//
|
|
410
|
+
// `exports` puts `createServices`, the one module reading the real filesystem,
|
|
411
|
+
// behind `./node`, so the root loads in a browser worker that has no `node:fs`.
|
|
412
|
+
// `./lib/*` and `./package.json` stay reachable for a host that resolves
|
|
413
|
+
// `<server>/lib/main.js` to spawn the server.
|
|
414
|
+
//
|
|
333
415
|
// `repository` is deliberately NOT emitted while `author` is emitted empty. An
|
|
334
416
|
// empty `author` is the form `npm init` itself writes and asserts nothing, but
|
|
335
417
|
// `repository` is CONSUMED — the registry page, `npm repo` and publish
|
|
@@ -354,17 +436,29 @@ __BIN__ },
|
|
|
354
436
|
"type": "module",
|
|
355
437
|
"main": "lib/index.js",
|
|
356
438
|
"types": "lib/index.d.ts",
|
|
439
|
+
"exports": {
|
|
440
|
+
".": {
|
|
441
|
+
"types": "./lib/index.d.ts",
|
|
442
|
+
"default": "./lib/index.js"
|
|
443
|
+
},
|
|
444
|
+
"./node": {
|
|
445
|
+
"types": "./lib/node/services.d.ts",
|
|
446
|
+
"default": "./lib/node/services.js"
|
|
447
|
+
},
|
|
448
|
+
"./lib/*": "./lib/*",
|
|
449
|
+
"./package.json": "./package.json"
|
|
450
|
+
},
|
|
357
451
|
"scripts": {
|
|
358
|
-
"build": "npm run generate && tsc",
|
|
452
|
+
"build": "npm run generate && tsc -b",
|
|
359
453
|
"clean": "rimraf lib syntaxes src/language-server/generated src/language-server/generated-hydranium tsconfig.tsbuildinfo",
|
|
360
454
|
"generate": "npm run langium:generate && npm run generate:transfer-model",
|
|
361
|
-
"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/
|
|
455
|
+
"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\\"",
|
|
362
456
|
"langium:generate": "langium generate",
|
|
363
457
|
"langium:watch": "langium generate --watch",
|
|
364
458
|
__LINT__ "start": "node lib/main.js --stdio",
|
|
365
459
|
"test": "npm run typecheck:test && vitest run",
|
|
366
460
|
"typecheck:test": "tsc --noEmit -p tsconfig.test.json",
|
|
367
|
-
"watch": "tsc --watch"
|
|
461
|
+
"watch": "tsc -b --watch"
|
|
368
462
|
},
|
|
369
463
|
"dependencies": {
|
|
370
464
|
__DEPENDENCIES__
|
|
@@ -384,16 +478,17 @@ __DEPENDENCIES__
|
|
|
384
478
|
`;
|
|
385
479
|
|
|
386
480
|
/**
|
|
387
|
-
* How the scaffold names `npm run`
|
|
481
|
+
* How the scaffold names `npm run` for one member's script.
|
|
388
482
|
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
*
|
|
483
|
+
* Always with `--prefix`, from the directory commands are run in: the
|
|
484
|
+
* workspace root for a member, the project root for a standalone project. A
|
|
485
|
+
* bare `npm run` there reaches the ROOT manifest and fails, and the regen hint
|
|
486
|
+
* a generated file prints is read in `protocol/` as well, where the script it
|
|
487
|
+
* names does not exist.
|
|
393
488
|
*/
|
|
394
|
-
function npmRun(composition: InitComposition): string {
|
|
489
|
+
function npmRun(composition: InitComposition, member: InitMember): string {
|
|
395
490
|
const workspace = composition.packaging.workspace;
|
|
396
|
-
return workspace === undefined ?
|
|
491
|
+
return `npm --prefix ${workspace === undefined ? member : `${workspace.targetPath}/${member}`} run`;
|
|
397
492
|
}
|
|
398
493
|
|
|
399
494
|
/**
|
|
@@ -416,38 +511,38 @@ function binBlock(composition: InitComposition): string {
|
|
|
416
511
|
return entries.map(([name, target]) => ` "${name}": "${target}"`).join(',\n') + '\n';
|
|
417
512
|
}
|
|
418
513
|
|
|
419
|
-
/**
|
|
514
|
+
/**
|
|
515
|
+
* A member's `lint` script line, over `directories`, or nothing.
|
|
516
|
+
*
|
|
517
|
+
* Emitted only where a root lint config says the repo lints. The absence is
|
|
518
|
+
* the dangerous direction: a task runner runs a script only where one is
|
|
519
|
+
* declared, so a package with no `lint` is SKIPPED rather than reported, and
|
|
520
|
+
* that reads as a clean lint. A wrong invocation is one visible line to edit.
|
|
521
|
+
*/
|
|
522
|
+
function lintScript(member: InitComposition, directories: string): string {
|
|
523
|
+
const workspace = member.packaging.workspace;
|
|
524
|
+
if (workspace?.oxlintConfig !== undefined) {
|
|
525
|
+
const config = `${'../'.repeat(workspace.targetPath.split('/').length)}${workspace.oxlintConfig}`;
|
|
526
|
+
return ` "lint": "oxlint --report-unused-disable-directives --format stylish --config ${config} ${directories} --max-warnings 0",\n`;
|
|
527
|
+
}
|
|
528
|
+
return workspace?.eslintConfig !== undefined ? ` "lint": "eslint ${directories} --max-warnings 0",\n` : '';
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/** The server's `package.json`, with the name, the private flag and the dependency block all derived. */
|
|
420
532
|
function packageJson(composition: InitComposition): string {
|
|
421
|
-
const { scope } = composition.packaging;
|
|
422
|
-
const packageName = scope === undefined ? composition.projectId : `${scope}/${composition.projectId}`;
|
|
423
533
|
const keywords =
|
|
424
534
|
['hydranium', 'langium', 'language-server', composition.projectId]
|
|
425
535
|
.sort()
|
|
426
536
|
.map(keyword => ` ${JSON.stringify(keyword)}`)
|
|
427
537
|
.join(',\n') + '\n';
|
|
428
|
-
// Emitted only where a root lint config says the repo lints. The absence is
|
|
429
|
-
// the dangerous direction: a task runner runs a script only where one is
|
|
430
|
-
// declared, so a package with no `lint` is SKIPPED rather than reported, and
|
|
431
|
-
// that reads as a clean lint. A wrong invocation is one visible line to edit.
|
|
432
|
-
const workspace = composition.packaging.workspace;
|
|
433
|
-
const oxlintConfig =
|
|
434
|
-
workspace?.oxlintConfig === undefined
|
|
435
|
-
? undefined
|
|
436
|
-
: `${'../'.repeat(workspace.targetPath.split('/').length)}${workspace.oxlintConfig}`;
|
|
437
|
-
const lint =
|
|
438
|
-
workspace?.oxlintConfig !== undefined
|
|
439
|
-
? ` "lint": "oxlint --report-unused-disable-directives --format stylish --config ${oxlintConfig} src test --max-warnings 0",\n`
|
|
440
|
-
: workspace?.eslintConfig !== undefined
|
|
441
|
-
? ' "lint": "eslint src test --max-warnings 0",\n'
|
|
442
|
-
: '';
|
|
443
538
|
return project(PACKAGE_JSON, composition)
|
|
444
|
-
.replace('__PACKAGE_NAME__',
|
|
539
|
+
.replace('__PACKAGE_NAME__', memberPackageName(composition, 'server'))
|
|
445
540
|
.replace('__KEYWORDS__', keywords)
|
|
446
541
|
.replace('__BIN__', binBlock(composition))
|
|
447
|
-
.replace('__LINT__',
|
|
542
|
+
.replace('__LINT__', lintScript(forMember(composition, 'server'), 'src test'))
|
|
448
543
|
.replace('__VITEST__', pinsVitest(composition) ? '~4.0.18' : '^4.0.0')
|
|
449
544
|
.replace('__PRIVATE__', composition.packaging.private ? ' "private": true,\n' : '')
|
|
450
|
-
.replace('__NPM_RUN__', npmRun(composition))
|
|
545
|
+
.replace('__NPM_RUN__', npmRun(composition, 'server'))
|
|
451
546
|
.replace('__FRAMEWORK_PIN__', frameworkPin(composition))
|
|
452
547
|
.replace('__DEPENDENCIES__', dependencyBlock(composition, ' '));
|
|
453
548
|
}
|
|
@@ -515,6 +610,24 @@ const TSCONFIG_COMPILER_OPTIONS: ReadonlyArray<readonly [string, JsonValue]> = [
|
|
|
515
610
|
['types', ['node']]
|
|
516
611
|
];
|
|
517
612
|
|
|
613
|
+
/** The protocol member's compiler options, pruned against a workspace base like the server's. */
|
|
614
|
+
const PROTOCOL_COMPILER_OPTIONS: ReadonlyArray<readonly [string, JsonValue]> = [
|
|
615
|
+
['target', 'ES2022'],
|
|
616
|
+
['lib', ['ES2022']],
|
|
617
|
+
['module', 'NodeNext'],
|
|
618
|
+
['moduleResolution', 'NodeNext'],
|
|
619
|
+
['rootDir', 'src'],
|
|
620
|
+
['outDir', 'lib'],
|
|
621
|
+
['strict', true],
|
|
622
|
+
['skipLibCheck', true],
|
|
623
|
+
['declaration', true],
|
|
624
|
+
// The server's `tsc -b` builds this project through a reference, which
|
|
625
|
+
// `tsc` refuses unless the referenced project is composite.
|
|
626
|
+
['composite', true],
|
|
627
|
+
// No ambient Node types: a browser client compiles against this package.
|
|
628
|
+
['types', []]
|
|
629
|
+
];
|
|
630
|
+
|
|
518
631
|
/**
|
|
519
632
|
* The options that describe where THIS package's own files live. Never pruned,
|
|
520
633
|
* and emitted first in the workspace case: a base config that supplied them
|
|
@@ -546,22 +659,42 @@ function optionEquals(key: string, left: JsonValue | undefined, right: JsonValue
|
|
|
546
659
|
* the same key: inheriting `"module": "CommonJS"` where the scaffold needs
|
|
547
660
|
* `NodeNext` would break every emitted import, so a differing base value is a
|
|
548
661
|
* reason to keep the override rather than to trust the parent.
|
|
662
|
+
*
|
|
663
|
+
* `member` is the composition as that member sees it, so `extends` reaches the
|
|
664
|
+
* workspace base from the member's own directory.
|
|
549
665
|
*/
|
|
550
|
-
function tsconfigJson(
|
|
551
|
-
|
|
666
|
+
function tsconfigJson(
|
|
667
|
+
member: InitComposition,
|
|
668
|
+
compilerOptions: ReadonlyArray<readonly [string, JsonValue]>,
|
|
669
|
+
include: string,
|
|
670
|
+
references = ''
|
|
671
|
+
): string {
|
|
672
|
+
const workspace = member.packaging.workspace;
|
|
552
673
|
const base = workspace?.baseTsconfig === undefined ? undefined : (workspace.baseCompilerOptions ?? {});
|
|
553
674
|
const options =
|
|
554
675
|
base === undefined
|
|
555
|
-
?
|
|
676
|
+
? compilerOptions
|
|
556
677
|
: [
|
|
557
|
-
...
|
|
558
|
-
...
|
|
559
|
-
([key, value]) => !PACKAGE_LOCAL_OPTIONS.includes(key) && !optionEquals(key, base[key], value)
|
|
560
|
-
)
|
|
678
|
+
...compilerOptions.filter(([key]) => PACKAGE_LOCAL_OPTIONS.includes(key)),
|
|
679
|
+
...compilerOptions.filter(([key, value]) => !PACKAGE_LOCAL_OPTIONS.includes(key) && !optionEquals(key, base[key], value))
|
|
561
680
|
];
|
|
562
681
|
const extendsLine = workspace?.baseTsconfig === undefined ? '' : ` "extends": "${workspace.baseTsconfig}",\n`;
|
|
563
682
|
const body = options.map(([key, value]) => ` "${key}": ${JSON.stringify(value)}`).join(',\n');
|
|
564
|
-
|
|
683
|
+
const referencesLine = references === '' ? '' : `,\n "references": ${references}`;
|
|
684
|
+
return `{\n${extendsLine} "compilerOptions": {\n${body}\n },\n "include": ${include}${referencesLine}\n}\n`;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* The server's `tsconfig.json`. It references the protocol member, so `tsc -b`
|
|
689
|
+
* compiles protocol first, after `generate` has written its generated modules.
|
|
690
|
+
*/
|
|
691
|
+
function serverTsconfigJson(composition: InitComposition): string {
|
|
692
|
+
return tsconfigJson(
|
|
693
|
+
forMember(composition, 'server'),
|
|
694
|
+
TSCONFIG_COMPILER_OPTIONS,
|
|
695
|
+
'["src", "src/**/*.json"]',
|
|
696
|
+
'[{ "path": "../protocol" }]'
|
|
697
|
+
);
|
|
565
698
|
}
|
|
566
699
|
|
|
567
700
|
/**
|
|
@@ -778,14 +911,13 @@ function portCommandNames(composition: InitComposition): string[] {
|
|
|
778
911
|
}
|
|
779
912
|
|
|
780
913
|
/**
|
|
781
|
-
* The socket heads' discovery commands,
|
|
914
|
+
* The socket heads' discovery commands, in the protocol member.
|
|
782
915
|
*
|
|
783
|
-
*
|
|
784
|
-
*
|
|
785
|
-
*
|
|
786
|
-
*
|
|
787
|
-
*
|
|
788
|
-
* rather than failing.
|
|
916
|
+
* A host shell has to name the same string the server publishes, and reaches
|
|
917
|
+
* it here without depending on the server. A host that retypes the literal
|
|
918
|
+
* instead gets no error when it drifts: the framework's port poll defaults to
|
|
919
|
+
* `findPortAttempts = -1`, so a wrong command retries forever rather than
|
|
920
|
+
* failing.
|
|
789
921
|
*/
|
|
790
922
|
function headPortsFile(composition: InitComposition): string {
|
|
791
923
|
const upper = screamingSnake(composition.projectId);
|
|
@@ -809,37 +941,35 @@ function headPortsFile(composition: InitComposition): string {
|
|
|
809
941
|
// process serves every registered grammar, so a language-derived name would tie
|
|
810
942
|
// a project-level endpoint to whichever grammar was scaffolded first.
|
|
811
943
|
//
|
|
812
|
-
//
|
|
813
|
-
//
|
|
814
|
-
//
|
|
815
|
-
//
|
|
816
|
-
// these instead, and assert any host-side copy against them.
|
|
944
|
+
// The server publishes them and a host shell has to name the same string to
|
|
945
|
+
// reach the head; both import them from here. A shell that retypes the literal
|
|
946
|
+
// gets no error when it drifts: the framework's port poll retries indefinitely
|
|
947
|
+
// by default.
|
|
817
948
|
|
|
818
949
|
${blocks.join('\n\n')}
|
|
819
950
|
`;
|
|
820
951
|
}
|
|
821
952
|
|
|
822
|
-
|
|
823
|
-
|
|
953
|
+
/**
|
|
954
|
+
* The server's root entry: the DI factory, the generated AST and each diagram
|
|
955
|
+
* module, all loadable in a browser worker.
|
|
956
|
+
*/
|
|
957
|
+
function indexFile(composition: InitComposition): string {
|
|
958
|
+
const diagrams = composition.grammars.filter(grammar => grammar.diagram);
|
|
959
|
+
const diagramPhrase = diagrams.length === 0 ? '' : diagrams.length === 1 ? ', plus the diagram module' : ', plus the diagram modules';
|
|
960
|
+
const text = `// Public surface of the __NAME__ language server: the DI factory and the
|
|
961
|
+
// generated AST${diagramPhrase}.
|
|
824
962
|
//
|
|
825
|
-
//
|
|
826
|
-
//
|
|
827
|
-
//
|
|
963
|
+
// Everything here loads in a browser worker as well as in Node, so a host can
|
|
964
|
+
// compose the language wherever it runs it. \`createServices\`, which reads the
|
|
965
|
+
// real filesystem, is the package's \`./node\` entry instead. The entry points
|
|
966
|
+
// under \`src/\` are not re-exported either: each opens a transport at module
|
|
967
|
+
// scope, so importing one starts a server, which is why they are \`bin\` targets.
|
|
828
968
|
|
|
829
969
|
export * from './language-server/__PROJECT_ID__-module.js';
|
|
830
970
|
export * from './language-server/ast.js';
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
/**
|
|
835
|
-
* The public surface, plus the head-port commands when there is a socket head.
|
|
836
|
-
*
|
|
837
|
-
* The commands belong here rather than only in `head-ports.ts` so a host shell
|
|
838
|
-
* reaches them from the package root, the same way it reaches `createServices`.
|
|
839
|
-
*/
|
|
840
|
-
function indexFile(composition: InitComposition): string {
|
|
841
|
-
const base = project(INDEX, composition);
|
|
842
|
-
return portCommandNames(composition).length ? `${base}export * from './head-ports.js';\n` : base;
|
|
971
|
+
${diagrams.map(grammar => `export * from './glsp/${grammar.grammarId}/diagram-module.js';\n`).join('')}`;
|
|
972
|
+
return project(text, composition);
|
|
843
973
|
}
|
|
844
974
|
|
|
845
975
|
/**
|
|
@@ -903,15 +1033,15 @@ import { startGlspServer } from '@hydranium/glsp-server/node';
|
|
|
903
1033
|
// \`TransferElement\` structurally, so naming the AST type here compiles fine and
|
|
904
1034
|
// silently tells every typed client that a reference is a resolvable object
|
|
905
1035
|
// rather than a name.
|
|
906
|
-
${importList(roots, '
|
|
1036
|
+
${importList(roots, memberPackageName(composition, 'protocol'), columns, true)}
|
|
907
1037
|
`
|
|
908
1038
|
: '';
|
|
909
1039
|
|
|
910
|
-
// The commands live in
|
|
911
|
-
// the same string to reach the head, and this file is an executable
|
|
912
|
-
// nothing can import from it.
|
|
1040
|
+
// The commands live in the protocol member, not here: a host shell has to
|
|
1041
|
+
// name the same string to reach the head, and this file is an executable
|
|
1042
|
+
// entry, so nothing can import from it.
|
|
913
1043
|
const portCommands = portCommandNames(composition).length
|
|
914
|
-
? importList(portCommandNames(composition), '
|
|
1044
|
+
? importList(portCommandNames(composition), memberPackageName(composition, 'protocol'), columns) + '\n'
|
|
915
1045
|
: '';
|
|
916
1046
|
|
|
917
1047
|
const dataBlock = data
|
|
@@ -1048,7 +1178,7 @@ function dataServerMainFile(composition: InitComposition): string {
|
|
|
1048
1178
|
${importList(['NodeFileSystem', 'startStdioServer'], '@hydranium/core/node', columns)}
|
|
1049
1179
|
import { DataServer } from '@hydranium/data-server';
|
|
1050
1180
|
// The TRANSFER root${plural}, not the AST one${plural} — same reasoning as \`main.ts\`.
|
|
1051
|
-
${importList(roots, '
|
|
1181
|
+
${importList(roots, memberPackageName(composition, 'protocol'), columns, true)}
|
|
1052
1182
|
import { create__NAME__Services } from './language-server/__PROJECT_ID__-module.js';
|
|
1053
1183
|
|
|
1054
1184
|
const { shared } = create__NAME__Services({ ...NodeFileSystem });
|
|
@@ -1097,7 +1227,7 @@ function servicesTest(composition: InitComposition): string {
|
|
|
1097
1227
|
// is missing from the composition.
|
|
1098
1228
|
|
|
1099
1229
|
import { describe, expect, it } from 'vitest';
|
|
1100
|
-
import { createServices } from '../src/services.js';
|
|
1230
|
+
import { createServices } from '../src/node/services.js';
|
|
1101
1231
|
|
|
1102
1232
|
describe('__NAME__ services', () => {
|
|
1103
1233
|
it('composes the DI tree and registers ${title}', () => {
|
|
@@ -1178,8 +1308,8 @@ function serializationTest(composition: InitComposition): string {
|
|
|
1178
1308
|
import { parseHelper } from '@hydranium/core/testing';
|
|
1179
1309
|
import { describe, expect, it } from 'vitest';
|
|
1180
1310
|
${importList(astTypes, '../src/language-server/ast.js', columns, true)}
|
|
1181
|
-
${importList(transferTypes, '
|
|
1182
|
-
import { createServices } from '../src/services.js';
|
|
1311
|
+
${importList(transferTypes, memberPackageName(composition, 'protocol'), columns, true)}
|
|
1312
|
+
import { createServices } from '../src/node/services.js';
|
|
1183
1313
|
|
|
1184
1314
|
${composition.grammars.map(grammar => render(SERIALIZATION_SUITE, composition, grammar)).join('\n\n')}
|
|
1185
1315
|
`;
|
|
@@ -1306,7 +1436,7 @@ function grammarTest(composition: InitComposition, header: string, imports: read
|
|
|
1306
1436
|
import { parseHelper } from '@hydranium/core/testing';
|
|
1307
1437
|
import { describe, expect, it } from 'vitest';
|
|
1308
1438
|
${imports.join('\n')}${imports.length ? '\n' : ''}${importList(astTypes, '../src/language-server/ast.js', columns, true)}
|
|
1309
|
-
import { createServices } from '../src/services.js';
|
|
1439
|
+
import { createServices } from '../src/node/services.js';
|
|
1310
1440
|
|
|
1311
1441
|
${composition.grammars.map(grammar => render(suite, composition, grammar)).join('\n\n')}
|
|
1312
1442
|
`;
|
|
@@ -1337,6 +1467,7 @@ function readme(composition: InitComposition): string {
|
|
|
1337
1467
|
const multi = composition.grammars.length > 1;
|
|
1338
1468
|
const data = composition.heads.includes('data');
|
|
1339
1469
|
const primaryExtension = composition.grammars[0].extensions[0];
|
|
1470
|
+
const diagramCount = composition.grammars.filter(grammar => grammar.diagram).length;
|
|
1340
1471
|
const grammarBullets = composition.grammars.map(
|
|
1341
1472
|
grammar => `- \`src/grammar/${grammar.grammarId}.langium\` — the \`${grammar.grammar}\` grammar (.${grammar.extensions.join(', .')})`
|
|
1342
1473
|
);
|
|
@@ -1361,13 +1492,13 @@ function readme(composition: InitComposition): string {
|
|
|
1361
1492
|
// nowhere else.
|
|
1362
1493
|
'```bash',
|
|
1363
1494
|
'# Grammar / AST reflection',
|
|
1364
|
-
'npx hydranium-cli reflect --services ./lib/services.js',
|
|
1495
|
+
'npx hydranium-cli reflect --services ./lib/node/services.js',
|
|
1365
1496
|
'',
|
|
1366
1497
|
'# Grammar-convention lint (CI gate)',
|
|
1367
|
-
'npx hydranium-cli lint-grammar --services ./lib/services.js',
|
|
1498
|
+
'npx hydranium-cli lint-grammar --services ./lib/node/services.js',
|
|
1368
1499
|
'',
|
|
1369
1500
|
'# Validate a workspace of model files (non-zero exit on errors)',
|
|
1370
|
-
'npx hydranium-cli validate --services ./lib/services.js
|
|
1501
|
+
'npx hydranium-cli validate --services ./lib/node/services.js ../workspace',
|
|
1371
1502
|
...(data
|
|
1372
1503
|
? [
|
|
1373
1504
|
'',
|
|
@@ -1378,8 +1509,8 @@ function readme(composition: InitComposition): string {
|
|
|
1378
1509
|
'#',
|
|
1379
1510
|
'# The workspace goes to the ENTRY, not to `--cwd`: `--cwd` re-roots the child,',
|
|
1380
1511
|
'# so a relative entry path is refused by name (an absolute one is fine).',
|
|
1381
|
-
'npx hydranium-cli projects --server "node ./lib/data-server-main.js
|
|
1382
|
-
`npx hydranium-cli query --server "node ./lib/data-server-main.js
|
|
1512
|
+
'npx hydranium-cli projects --server "node ./lib/data-server-main.js ../workspace"',
|
|
1513
|
+
`npx hydranium-cli query --server "node ./lib/data-server-main.js ../workspace" --uri <file:// URI of ../workspace/nodes.${primaryExtension}>`
|
|
1383
1514
|
]
|
|
1384
1515
|
: []),
|
|
1385
1516
|
'```',
|
|
@@ -1393,8 +1524,8 @@ function readme(composition: InitComposition): string {
|
|
|
1393
1524
|
grammar =>
|
|
1394
1525
|
`- \`src/language-server/${grammar.grammarId}-serializer.ts\` — emits \`${grammar.grammar}\` back to text (the framework defaults this to a throw)`
|
|
1395
1526
|
),
|
|
1396
|
-
|
|
1397
|
-
|
|
1527
|
+
"- `src/node/services.ts` — zero-arg `createServices()` for the headless CLI, the package's `./node` entry",
|
|
1528
|
+
`- \`src/index.ts\` — the package root, loadable in a browser worker: DI factory + generated AST${diagramCount === 0 ? '' : diagramCount === 1 ? ' + diagram module' : ' + diagram modules'}`,
|
|
1398
1529
|
`- \`src/main.ts\` — starts ${composition.heads.join(' + ')}, the \`__PROJECT_ID__\` bin entry`,
|
|
1399
1530
|
...(data
|
|
1400
1531
|
? [
|
|
@@ -1406,8 +1537,9 @@ function readme(composition: InitComposition): string {
|
|
|
1406
1537
|
.filter(grammar => grammar.diagram)
|
|
1407
1538
|
.map(
|
|
1408
1539
|
grammar =>
|
|
1409
|
-
`- \`src/glsp/${grammar.grammarId}/\` — the \`${grammar.grammar}\` diagram:
|
|
1540
|
+
`- \`src/glsp/${grammar.grammarId}/\` — the \`${grammar.grammar}\` diagram: state, storage, submission handler, GModel factory, configuration, create-node handler, DI module`
|
|
1410
1541
|
),
|
|
1542
|
+
`- \`../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\``,
|
|
1411
1543
|
'- `test/services.test.ts` — the DI tree composes; grows as your language does',
|
|
1412
1544
|
'- `test/parsing.test.ts` / `linking.test.ts` / `validating.test.ts` — the three tiers a new language breaks first',
|
|
1413
1545
|
'- `test/serialization.test.ts` — each grammar round-trips through its serializer',
|
|
@@ -1513,9 +1645,9 @@ function readme(composition: InitComposition): string {
|
|
|
1513
1645
|
*/
|
|
1514
1646
|
function glspFiles(composition: InitComposition, grammar: InitGrammarNames): InitFile[] {
|
|
1515
1647
|
const columns = columnsFor(composition);
|
|
1516
|
-
const fixed = (file: string): InitFile => grammarTemplate(`src/glsp/__GRAMMAR_ID__/${file}`, composition, grammar);
|
|
1648
|
+
const fixed = (file: string): InitFile => grammarTemplate(`server/src/glsp/__GRAMMAR_ID__/${file}`, composition, grammar);
|
|
1517
1649
|
const computed = (file: string, text: string): InitFile => ({
|
|
1518
|
-
path: `src/glsp/${grammar.grammarId}/${file}`,
|
|
1650
|
+
path: `server/src/glsp/${grammar.grammarId}/${file}`,
|
|
1519
1651
|
content: render(text, composition, grammar)
|
|
1520
1652
|
});
|
|
1521
1653
|
|
|
@@ -1542,7 +1674,7 @@ ${importList(
|
|
|
1542
1674
|
columns
|
|
1543
1675
|
)}
|
|
1544
1676
|
import { injectable } from 'inversify';
|
|
1545
|
-
import { __GRAMMAR_ID_CONST___EDGE_TYPE, __GRAMMAR_ID_CONST___NODE_TYPE } from '
|
|
1677
|
+
import { __GRAMMAR_ID_CONST___EDGE_TYPE, __GRAMMAR_ID_CONST___NODE_TYPE } from '__PROTOCOL_PACKAGE__';
|
|
1546
1678
|
|
|
1547
1679
|
@injectable()
|
|
1548
1680
|
export class __GRAMMAR__DiagramConfiguration implements DiagramConfiguration {
|
|
@@ -1614,7 +1746,7 @@ import { __GRAMMAR__GModelFactory } from './gmodel-factory.js';
|
|
|
1614
1746
|
import { __GRAMMAR__GlspState } from './state.js';
|
|
1615
1747
|
import { __GRAMMAR__GlspStorage } from './storage.js';
|
|
1616
1748
|
import { __GRAMMAR__SubmissionHandler } from './submission-handler.js';
|
|
1617
|
-
import { __GRAMMAR_ID_CONST___DIAGRAM_TYPE } from '
|
|
1749
|
+
import { __GRAMMAR_ID_CONST___DIAGRAM_TYPE } from '__PROTOCOL_PACKAGE__';
|
|
1618
1750
|
|
|
1619
1751
|
export class __GRAMMAR__DiagramModule extends AbstractHydraniumGlspDiagramModule {
|
|
1620
1752
|
readonly diagramType = __GRAMMAR_ID_CONST___DIAGRAM_TYPE;
|
|
@@ -1657,7 +1789,6 @@ export class __GRAMMAR__DiagramModule extends AbstractHydraniumGlspDiagramModule
|
|
|
1657
1789
|
`;
|
|
1658
1790
|
|
|
1659
1791
|
return [
|
|
1660
|
-
fixed('types.ts'),
|
|
1661
1792
|
fixed('state.ts'),
|
|
1662
1793
|
fixed('storage.ts'),
|
|
1663
1794
|
fixed('submission-handler.ts'),
|
|
@@ -1665,35 +1796,35 @@ export class __GRAMMAR__DiagramModule extends AbstractHydraniumGlspDiagramModule
|
|
|
1665
1796
|
computed('diagram-configuration.ts', configuration),
|
|
1666
1797
|
fixed('create-node-operation-handler.ts'),
|
|
1667
1798
|
computed('diagram-module.ts', module),
|
|
1668
|
-
grammarTemplate('test/__GRAMMAR_ID__-diagram.test.ts', composition, grammar)
|
|
1799
|
+
grammarTemplate('server/test/__GRAMMAR_ID__-diagram.test.ts', composition, grammar)
|
|
1669
1800
|
];
|
|
1670
1801
|
}
|
|
1671
1802
|
|
|
1672
1803
|
/**
|
|
1673
|
-
*
|
|
1804
|
+
* The server member: the grammar, the language and its heads.
|
|
1674
1805
|
*
|
|
1675
1806
|
* Grammar-count-dependent emissions live here rather than in a template string:
|
|
1676
1807
|
* one `.langium` file per grammar, and the shared `common.langium` fragment only
|
|
1677
1808
|
* when there is more than one grammar to share it.
|
|
1678
1809
|
*/
|
|
1679
|
-
|
|
1810
|
+
function serverFiles(composition: InitComposition): InitFile[] {
|
|
1680
1811
|
const multi = composition.grammars.length > 1;
|
|
1681
1812
|
const grammarFiles: InitFile[] = composition.grammars.map(grammar => ({
|
|
1682
|
-
path: `src/grammar/${grammar.grammarId}.langium`,
|
|
1813
|
+
path: `server/src/grammar/${grammar.grammarId}.langium`,
|
|
1683
1814
|
content: render(grammarFile(multi), composition, grammar)
|
|
1684
1815
|
}));
|
|
1685
1816
|
// The shared lexical fragment, only for several grammars: each declaring its
|
|
1686
1817
|
// own `WS` / `ID` lets the token sets drift apart.
|
|
1687
1818
|
if (multi) {
|
|
1688
|
-
grammarFiles.unshift(projectTemplate('src/grammar/common.langium', composition));
|
|
1819
|
+
grammarFiles.unshift(projectTemplate('server/src/grammar/common.langium', composition));
|
|
1689
1820
|
}
|
|
1690
1821
|
|
|
1691
1822
|
const diagramFiles = composition.grammars.filter(grammar => grammar.diagram).flatMap(grammar => glspFiles(composition, grammar));
|
|
1692
1823
|
|
|
1693
1824
|
return [
|
|
1694
|
-
{ path: 'package.json', content: packageJson(composition) },
|
|
1695
|
-
{ path: 'langium-config.json', content: langiumConfig(composition) },
|
|
1696
|
-
{ path: 'tsconfig.json', content:
|
|
1825
|
+
{ path: 'server/package.json', content: packageJson(composition) },
|
|
1826
|
+
{ path: 'server/langium-config.json', content: langiumConfig(composition) },
|
|
1827
|
+
{ path: 'server/tsconfig.json', content: serverTsconfigJson(composition) },
|
|
1697
1828
|
// `isolatedModules` is what makes this check agree with the transform that
|
|
1698
1829
|
// actually runs the tests. `npm test` is `typecheck:test && vitest run`, and
|
|
1699
1830
|
// vitest compiles each file on its own — a transform with no type
|
|
@@ -1702,40 +1833,39 @@ export function buildInitTemplates(composition: InitComposition): InitFile[] {
|
|
|
1702
1833
|
// transform then emits as a real import of a symbol that does not exist at
|
|
1703
1834
|
// runtime, so the typecheck passes and the suite fails with a confusing
|
|
1704
1835
|
// missing-export.
|
|
1705
|
-
projectTemplate('tsconfig.test.json', composition),
|
|
1836
|
+
projectTemplate('server/tsconfig.test.json', composition),
|
|
1706
1837
|
// Found by name, unlike `tsconfig.test.json`. Under vite 8, vitest's transform
|
|
1707
1838
|
// applies a tsconfig only to the files its own `include` lists, so without this
|
|
1708
1839
|
// a test declaring a decorated class fails to load.
|
|
1709
|
-
projectTemplate('test/tsconfig.json', composition),
|
|
1710
|
-
projectTemplate('vitest.config.ts', composition),
|
|
1840
|
+
projectTemplate('server/test/tsconfig.json', composition),
|
|
1841
|
+
projectTemplate('server/vitest.config.ts', composition),
|
|
1711
1842
|
// Written for a workspace member too: its root may not ignore `lib/` (a `yo
|
|
1712
1843
|
// langium` root ignores `out/`), and a rule the root repeats costs nothing.
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
{ path: '.gitignore', content: template('server/gitignore') },
|
|
1716
|
-
{ path: 'README.md', content: readme(composition) },
|
|
1844
|
+
ignoreFile('server'),
|
|
1845
|
+
{ path: 'server/README.md', content: readme(composition) },
|
|
1717
1846
|
...grammarFiles,
|
|
1718
|
-
{ path: `src/language-server/${composition.projectId}-module.ts`, content: moduleFile(composition) },
|
|
1719
|
-
grammarTemplate('src/language-server/ast.ts', composition, composition.grammars[0]),
|
|
1847
|
+
{ path: `server/src/language-server/${composition.projectId}-module.ts`, content: moduleFile(composition) },
|
|
1848
|
+
grammarTemplate('server/src/language-server/ast.ts', composition, composition.grammars[0]),
|
|
1720
1849
|
// Per grammar, because `services.serializer.Serializer` is a per-language slot
|
|
1721
1850
|
// and a serializer is grammar-shaped by definition. That is also why the
|
|
1722
1851
|
// framework refuses to default it: its `UnboundSerializer` throws, naming the
|
|
1723
1852
|
// binding to add, rather than guessing a syntax.
|
|
1724
|
-
...composition.grammars.map(grammar =>
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
{ path: 'src/
|
|
1728
|
-
|
|
1729
|
-
|
|
1853
|
+
...composition.grammars.map(grammar =>
|
|
1854
|
+
grammarTemplate('server/src/language-server/__GRAMMAR_ID__-serializer.ts', composition, grammar)
|
|
1855
|
+
),
|
|
1856
|
+
{ path: 'server/src/index.ts', content: indexFile(composition) },
|
|
1857
|
+
projectTemplate('server/src/node/services.ts', composition),
|
|
1858
|
+
{ path: 'server/src/main.ts', content: mainFile(composition) },
|
|
1859
|
+
...(composition.heads.includes('data') ? [{ path: 'server/src/data-server-main.ts', content: dataServerMainFile(composition) }] : []),
|
|
1730
1860
|
...diagramFiles,
|
|
1731
|
-
{ path: 'test/services.test.ts', content: servicesTest(composition) },
|
|
1732
|
-
{ path: 'test/parsing.test.ts', content: grammarTest(composition, PARSING_HEADER, [], PARSING_SUITE) },
|
|
1861
|
+
{ path: 'server/test/services.test.ts', content: servicesTest(composition) },
|
|
1862
|
+
{ path: 'server/test/parsing.test.ts', content: grammarTest(composition, PARSING_HEADER, [], PARSING_SUITE) },
|
|
1733
1863
|
{
|
|
1734
|
-
path: 'test/linking.test.ts',
|
|
1864
|
+
path: 'server/test/linking.test.ts',
|
|
1735
1865
|
content: grammarTest(composition, LINKING_HEADER, ["import { AstUtils } from '@hydranium/langium';"], LINKING_SUITE)
|
|
1736
1866
|
},
|
|
1737
1867
|
{
|
|
1738
|
-
path: 'test/validating.test.ts',
|
|
1868
|
+
path: 'server/test/validating.test.ts',
|
|
1739
1869
|
content: grammarTest(
|
|
1740
1870
|
composition,
|
|
1741
1871
|
VALIDATING_HEADER,
|
|
@@ -1743,6 +1873,167 @@ export function buildInitTemplates(composition: InitComposition): InitFile[] {
|
|
|
1743
1873
|
VALIDATING_SUITE
|
|
1744
1874
|
)
|
|
1745
1875
|
},
|
|
1746
|
-
{ path: 'test/serialization.test.ts', content: serializationTest(composition) }
|
|
1876
|
+
{ path: 'server/test/serialization.test.ts', content: serializationTest(composition) }
|
|
1877
|
+
];
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
/**
|
|
1881
|
+
* The protocol member's `package.json`.
|
|
1882
|
+
*
|
|
1883
|
+
* Its `build` runs the server's `generate` first, because the server writes
|
|
1884
|
+
* this package's `generated-hydranium/` modules from the grammar: a runner that
|
|
1885
|
+
* builds in dependency order reaches this package before the server, and would
|
|
1886
|
+
* otherwise compile modules that do not exist yet. A full build therefore
|
|
1887
|
+
* generates twice; the second pass rewrites nothing.
|
|
1888
|
+
*/
|
|
1889
|
+
function protocolPackageJson(composition: InitComposition): string {
|
|
1890
|
+
const member = forMember(composition, 'protocol');
|
|
1891
|
+
const dependencies: Record<string, string> = {};
|
|
1892
|
+
for (const head of composition.heads) {
|
|
1893
|
+
Object.assign(dependencies, PROTOCOL_THIRD_PARTY_DEPENDENCIES[head]);
|
|
1894
|
+
}
|
|
1895
|
+
const dependencyLines =
|
|
1896
|
+
Object.keys(dependencies).length === 0 ? '' : ` "dependencies": {\n${sortedBlock(dependencies, ' ')}\n },\n`;
|
|
1897
|
+
return `{
|
|
1898
|
+
"name": "${memberPackageName(composition, 'protocol')}",
|
|
1899
|
+
"version": "0.0.0",
|
|
1900
|
+
${composition.packaging.private ? ' "private": true,\n' : ''} "description": "What the ${composition.name} language server and its clients share: language ids, wire types and diagram ids.",
|
|
1901
|
+
"license": "UNLICENSED",
|
|
1902
|
+
"author": "",
|
|
1903
|
+
"files": [
|
|
1904
|
+
"lib",
|
|
1905
|
+
"src"
|
|
1906
|
+
],
|
|
1907
|
+
"type": "module",
|
|
1908
|
+
"main": "lib/index.js",
|
|
1909
|
+
"types": "lib/index.d.ts",
|
|
1910
|
+
"scripts": {
|
|
1911
|
+
"build": "npm --prefix ../server run generate && tsc -b",
|
|
1912
|
+
"clean": "rimraf lib tsconfig.tsbuildinfo",
|
|
1913
|
+
${lintScript(member, 'src')} "watch": "tsc -b --watch"
|
|
1914
|
+
},
|
|
1915
|
+
${dependencyLines} "devDependencies": {
|
|
1916
|
+
"rimraf": "^5.0.0",
|
|
1917
|
+
"typescript": "^7.0.2"
|
|
1918
|
+
},
|
|
1919
|
+
"engines": {
|
|
1920
|
+
"node": ">=22.13"
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
`;
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
/** The protocol member's root entry, one re-export per module. */
|
|
1927
|
+
function protocolIndexFile(composition: InitComposition): string {
|
|
1928
|
+
const modules = [
|
|
1929
|
+
'./generated-hydranium/languages.js',
|
|
1930
|
+
'./generated-hydranium/transfer-model.js',
|
|
1931
|
+
...(portCommandNames(composition).length ? ['./head-ports.js'] : []),
|
|
1932
|
+
...composition.grammars.filter(grammar => grammar.diagram).map(grammar => `./${grammar.grammarId}-diagram-types.js`)
|
|
1933
|
+
];
|
|
1934
|
+
const text = `// What the __NAME__ server and its clients both import. A client depends on
|
|
1935
|
+
// this package instead of the server, so it never installs Langium to reach a
|
|
1936
|
+
// constant or a wire type.
|
|
1937
|
+
//
|
|
1938
|
+
// The server's \`generate\` writes \`generated-hydranium/\` from the grammar;
|
|
1939
|
+
// commit those modules with the grammar change that produced them.
|
|
1940
|
+
|
|
1941
|
+
${modules.map(module => `export * from '${module}';`).join('\n')}
|
|
1942
|
+
`;
|
|
1943
|
+
return project(text, composition);
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
/** The protocol member's README. */
|
|
1947
|
+
function protocolReadme(composition: InitComposition): string {
|
|
1948
|
+
const lines = [
|
|
1949
|
+
`# ${memberPackageName(composition, 'protocol')}`,
|
|
1950
|
+
'',
|
|
1951
|
+
'What the __NAME__ language server and its clients both import, so a client',
|
|
1952
|
+
'never depends on the server or installs Langium.',
|
|
1953
|
+
'',
|
|
1954
|
+
'- `src/generated-hydranium/languages.ts` — each language id and its file extensions',
|
|
1955
|
+
'- `src/generated-hydranium/transfer-model.ts` — the wire types of the model, references as plain strings',
|
|
1956
|
+
...(portCommandNames(composition).length ? ['- `src/head-ports.ts` — the LSP requests a host sends to find the socket heads'] : []),
|
|
1957
|
+
...composition.grammars
|
|
1958
|
+
.filter(grammar => grammar.diagram)
|
|
1959
|
+
.map(grammar => `- \`src/${grammar.grammarId}-diagram-types.ts\` — the \`${grammar.grammar}\` diagram's type ids`),
|
|
1960
|
+
'',
|
|
1961
|
+
"The server's `npm run generate` writes `src/generated-hydranium/` from the",
|
|
1962
|
+
'grammar, and `npm run build` here runs it first. Commit the generated',
|
|
1963
|
+
'modules with the grammar change that produced them.',
|
|
1964
|
+
''
|
|
1965
|
+
];
|
|
1966
|
+
return project(lines.join('\n'), composition);
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
/** The protocol member: everything the server and its clients both import. */
|
|
1970
|
+
function protocolFiles(composition: InitComposition): InitFile[] {
|
|
1971
|
+
return [
|
|
1972
|
+
{ path: 'protocol/package.json', content: protocolPackageJson(composition) },
|
|
1973
|
+
{ path: 'protocol/tsconfig.json', content: tsconfigJson(forMember(composition, 'protocol'), PROTOCOL_COMPILER_OPTIONS, '["src"]') },
|
|
1974
|
+
ignoreFile('protocol'),
|
|
1975
|
+
{ path: 'protocol/README.md', content: protocolReadme(composition) },
|
|
1976
|
+
{ path: 'protocol/src/index.ts', content: protocolIndexFile(composition) },
|
|
1977
|
+
...(portCommandNames(composition).length ? [{ path: 'protocol/src/head-ports.ts', content: headPortsFile(composition) }] : []),
|
|
1978
|
+
...composition.grammars
|
|
1979
|
+
.filter(grammar => grammar.diagram)
|
|
1980
|
+
.map(grammar => grammarTemplate('protocol/src/__GRAMMAR_ID__-diagram-types.ts', composition, grammar))
|
|
1981
|
+
];
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
/**
|
|
1985
|
+
* The starter model files every host and the CLI open, two per language: one
|
|
1986
|
+
* declaring nodes, one referencing a node in the other file.
|
|
1987
|
+
*
|
|
1988
|
+
* Named by extension alone, since `init` refuses two grammars sharing one.
|
|
1989
|
+
*/
|
|
1990
|
+
function workspaceFiles(composition: InitComposition): InitFile[] {
|
|
1991
|
+
return composition.grammars.flatMap(grammar => [
|
|
1992
|
+
grammarTemplate('workspace/nodes.__EXTENSION__', composition, grammar),
|
|
1993
|
+
grammarTemplate('workspace/references.__EXTENSION__', composition, grammar)
|
|
1994
|
+
]);
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
/**
|
|
1998
|
+
* A standalone project's root: the members as npm workspaces.
|
|
1999
|
+
*
|
|
2000
|
+
* Always private: it is never published, whatever `--public` makes of the
|
|
2001
|
+
* members. npm runs a script across the members in the order listed here, not
|
|
2002
|
+
* by their dependencies; listing protocol first is safe because its `build`
|
|
2003
|
+
* runs the server's `generate` before compiling.
|
|
2004
|
+
*/
|
|
2005
|
+
function rootFiles(composition: InitComposition): InitFile[] {
|
|
2006
|
+
const content = `{
|
|
2007
|
+
"name": "${composition.projectId}-root",
|
|
2008
|
+
"version": "0.0.0",
|
|
2009
|
+
"private": true,
|
|
2010
|
+
"workspaces": [
|
|
2011
|
+
"protocol",
|
|
2012
|
+
"server"
|
|
2013
|
+
],
|
|
2014
|
+
"scripts": {
|
|
2015
|
+
"build": "npm run build --workspaces",
|
|
2016
|
+
"clean": "npm run clean --workspaces --if-present",
|
|
2017
|
+
"test": "npm test --workspaces --if-present"
|
|
2018
|
+
},
|
|
2019
|
+
"engines": {
|
|
2020
|
+
"node": ">=22.13"
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
`;
|
|
2024
|
+
return [{ path: 'package.json', content }, ignoreFile('')];
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
/**
|
|
2028
|
+
* Every file the scaffold emits, fully rendered, at its path in the project
|
|
2029
|
+
* directory. A workspace member's project gets no root of its own: it joins the
|
|
2030
|
+
* surrounding workspace, whose manifest `init` prints the changes for.
|
|
2031
|
+
*/
|
|
2032
|
+
export function buildInitTemplates(composition: InitComposition): InitFile[] {
|
|
2033
|
+
return [
|
|
2034
|
+
...(composition.packaging.workspace === undefined ? rootFiles(composition) : []),
|
|
2035
|
+
...protocolFiles(composition),
|
|
2036
|
+
...serverFiles(composition),
|
|
2037
|
+
...workspaceFiles(composition)
|
|
1747
2038
|
];
|
|
1748
2039
|
}
|