@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.
Files changed (38) hide show
  1. package/README.md +11 -9
  2. package/lib/commands/headless-harness.js +1 -1
  3. package/lib/commands/headless-harness.js.map +1 -1
  4. package/lib/commands/init-args.d.ts.map +1 -1
  5. package/lib/commands/init-args.js +21 -16
  6. package/lib/commands/init-args.js.map +1 -1
  7. package/lib/commands/init-templates.d.ts +12 -6
  8. package/lib/commands/init-templates.d.ts.map +1 -1
  9. package/lib/commands/init-templates.js +394 -127
  10. package/lib/commands/init-templates.js.map +1 -1
  11. package/lib/commands/init-wizard.d.ts.map +1 -1
  12. package/lib/commands/init-wizard.js +8 -7
  13. package/lib/commands/init-wizard.js.map +1 -1
  14. package/lib/commands/init-workspace.d.ts +0 -8
  15. package/lib/commands/init-workspace.d.ts.map +1 -1
  16. package/lib/commands/init-workspace.js +0 -1
  17. package/lib/commands/init-workspace.js.map +1 -1
  18. package/lib/commands/init.d.ts +26 -1
  19. package/lib/commands/init.d.ts.map +1 -1
  20. package/lib/commands/init.js +64 -20
  21. package/lib/commands/init.js.map +1 -1
  22. package/package.json +11 -10
  23. package/src/commands/headless-harness.ts +1 -1
  24. package/src/commands/init-args.ts +21 -16
  25. package/src/commands/init-templates.ts +424 -133
  26. package/src/commands/init-wizard.ts +8 -7
  27. package/src/commands/init-workspace.ts +0 -9
  28. package/src/commands/init.ts +90 -20
  29. package/templates/gitignore +1 -0
  30. package/templates/protocol/gitignore +3 -0
  31. package/templates/protocol/src/__GRAMMAR_ID__-diagram-types.ts +41 -0
  32. package/templates/server/src/glsp/__GRAMMAR_ID__/create-node-operation-handler.ts +5 -4
  33. package/templates/server/src/glsp/__GRAMMAR_ID__/gmodel-factory.ts +6 -3
  34. package/templates/server/src/{services.ts → node/services.ts} +6 -2
  35. package/templates/server/test/__GRAMMAR_ID__-diagram.test.ts +28 -7
  36. package/templates/workspace/nodes.__EXTENSION__ +5 -0
  37. package/templates/workspace/references.__EXTENSION__ +2 -0
  38. 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
- * (the ignore file without its dot); one computed from the composition is
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 `server/` template emitted once per project, rendered with the project tokens. */
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(`server/${path}`), composition) };
188
+ return { path, content: project(template(path), composition) };
144
189
  }
145
- /** A `server/` template emitted once per grammar, its path rendered like its content. */
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(`server/${path}`), composition, grammar) };
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
- /** The dependency block for a head set, merged and sorted as npm writes it. */
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 Object.keys(merged)
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/language-server/generated-hydranium/transfer-model.ts --ast-builder-file src/language-server/generated-hydranium/ast-builder.ts --element-type-name __NAME__Element --terminals-name __NAME__Terminals --regen-command \\"Run: __NPM_RUN__ generate:transfer-model\\"",
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` when talking about itself.
442
+ * How the scaffold names `npm run` for one member's script.
355
443
  *
356
- * A workspace member's scripts are routinely invoked from the repo root, where
357
- * a bare `npm run` reaches the ROOT manifest and fails — so the regen hint a
358
- * generated file prints has to name the package, or it sends every reader who
359
- * follows it to the wrong place.
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 ? 'npm run' : `npm --prefix ${workspace.targetPath} run`;
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
- /** `package.json` with the name, the private flag and the dependency block all derived. */
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__', packageName)
496
+ .replace('__PACKAGE_NAME__', memberPackageName(composition, 'server'))
407
497
  .replace('__KEYWORDS__', keywords)
408
498
  .replace('__BIN__', binBlock(composition))
409
- .replace('__LINT__', 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(composition) {
505
- const workspace = composition.packaging.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
- ? TSCONFIG_COMPILER_OPTIONS
618
+ ? compilerOptions
509
619
  : [
510
- ...TSCONFIG_COMPILER_OPTIONS.filter(([key]) => PACKAGE_LOCAL_OPTIONS.includes(key)),
511
- ...TSCONFIG_COMPILER_OPTIONS.filter(([key, value]) => !PACKAGE_LOCAL_OPTIONS.includes(key) && !optionEquals(key, base[key], value))
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
- return `{\n${extendsLine} "compilerOptions": {\n${body}\n },\n "include": ["src", "src/**/*.json"]\n}\n`;
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, as an importable module.
826
+ * The socket heads' discovery commands, in the protocol member.
709
827
  *
710
- * Deliberately NOT declared in `main.ts`. A host shell has to name the same
711
- * string to reach the head, and `main.ts` opens a connection at module scope,
712
- * so importing a constant from it would start a server. A host that retypes the
713
- * literal instead gets no error when it drifts: the framework's port poll
714
- * defaults to `findPortAttempts = -1`, so a wrong command retries forever
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
- // They live here rather than in \`main.ts\` because a host shell has to name the
740
- // same string to reach the head, and \`main.ts\` is an executable entry — nothing
741
- // can import from it. A shell that retypes the literal gets no error when it
742
- // drifts: the framework's port poll retries indefinitely by default. Import
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
- const INDEX = `// Public surface of the __NAME__ language server: the DI factory, the generated
749
- // AST, and the headless \`createServices\` entry.
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
- // The entry points under \`src/\` are deliberately NOT re-exported. Each opens a
752
- // transport at module scope, so importing one starts a server as a side effect
753
- // — which is why they are \`bin\` targets and this file is \`main\`.
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
- export { createServices } from './services.js';
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, './language-server/generated-hydranium/transfer-model.js', columns, true)}
940
+ ${importList(roots, memberPackageName(composition, 'protocol'), columns, true)}
825
941
  `
826
942
  : '';
827
- // The commands live in `head-ports.ts`, not here: a host shell has to name
828
- // the same string to reach the head, and this file is an executable entry, so
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), './head-ports.js', columns) + '\n'
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, './language-server/generated-hydranium/transfer-model.js', columns, true)}
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, '../src/language-server/generated-hydranium/transfer-model.js', columns, true)}
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 <workspace-dir>',
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 <workspace-dir>"',
1279
- `npx hydranium-cli query --server "node ./lib/data-server-main.js <workspace-dir>" --uri <file:// URI of a .${primaryExtension} file>`
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
- '- `src/services.ts` — zero-arg `createServices()` for the headless CLI',
1291
- '- `src/index.ts` — the package entry (`main`): DI factory + generated AST',
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: type ids, state, storage, submission handler, GModel factory, configuration, create-node handler, DI module`),
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 './types.js';
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 './types.js';
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
- * Every file the scaffold emits, fully rendered.
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
- export function buildInitTemplates(composition) {
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: tsconfigJson(composition) },
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
- // Stored without its dot, because `npm pack` drops a nested `.gitignore` from
1585
- // the tarball and applies it as an ignore file.
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