@hydranium/cli 1.0.0-next.18 → 1.0.0-next.181
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 +14 -0
- package/bin/hydranium-cli.js +13 -0
- package/lib/cli-version.js.map +1 -1
- package/lib/cli.js +0 -0
- package/lib/commands/analyze-heap-args.js.map +1 -1
- package/lib/commands/analyze-heap.d.ts +3 -0
- package/lib/commands/analyze-heap.d.ts.map +1 -1
- package/lib/commands/analyze-heap.js +2 -1
- package/lib/commands/analyze-heap.js.map +1 -1
- package/lib/commands/ast-ground-truth-args.js.map +1 -1
- package/lib/commands/ast-ground-truth.js +1 -1
- package/lib/commands/ast-ground-truth.js.map +1 -1
- package/lib/commands/generate-ast-builder.d.ts +49 -0
- package/lib/commands/generate-ast-builder.d.ts.map +1 -0
- package/lib/commands/generate-ast-builder.js +200 -0
- package/lib/commands/generate-ast-builder.js.map +1 -0
- package/lib/commands/generate-transfer-model-args.d.ts.map +1 -1
- package/lib/commands/generate-transfer-model-args.js +11 -1
- package/lib/commands/generate-transfer-model-args.js.map +1 -1
- package/lib/commands/generate-transfer-model-config.d.ts.map +1 -1
- package/lib/commands/generate-transfer-model-config.js +3 -1
- package/lib/commands/generate-transfer-model-config.js.map +1 -1
- package/lib/commands/generate-transfer-model.d.ts +9 -0
- package/lib/commands/generate-transfer-model.d.ts.map +1 -1
- package/lib/commands/generate-transfer-model.js +16 -3
- package/lib/commands/generate-transfer-model.js.map +1 -1
- package/lib/commands/harness-args.js.map +1 -1
- package/lib/commands/headless-harness.d.ts +18 -3
- package/lib/commands/headless-harness.d.ts.map +1 -1
- package/lib/commands/headless-harness.js +12 -4
- package/lib/commands/headless-harness.js.map +1 -1
- package/lib/commands/init-args.js.map +1 -1
- package/lib/commands/init-prompt.d.ts.map +1 -1
- package/lib/commands/init-templates.d.ts +0 -9
- package/lib/commands/init-templates.d.ts.map +1 -1
- package/lib/commands/init-templates.js +78 -55
- package/lib/commands/init-templates.js.map +1 -1
- package/lib/commands/init-wizard.js.map +1 -1
- package/lib/commands/init-workspace.d.ts +7 -3
- package/lib/commands/init-workspace.d.ts.map +1 -1
- package/lib/commands/init-workspace.js +11 -6
- package/lib/commands/init-workspace.js.map +1 -1
- package/lib/commands/init.d.ts +4 -2
- package/lib/commands/init.d.ts.map +1 -1
- package/lib/commands/init.js +7 -10
- package/lib/commands/init.js.map +1 -1
- package/lib/commands/lint-grammar-args.js.map +1 -1
- package/lib/commands/lint-grammar-report.js.map +1 -1
- package/lib/commands/lint-grammar.js +1 -1
- package/lib/commands/lint-grammar.js.map +1 -1
- package/lib/commands/measure-memory-args.js.map +1 -1
- package/lib/commands/measure-memory-driver.js +3 -2
- package/lib/commands/measure-memory-driver.js.map +1 -1
- package/lib/commands/measure-memory.js +1 -1
- package/lib/commands/measure-memory.js.map +1 -1
- package/lib/commands/model-docs-args.js.map +1 -1
- package/lib/commands/model-docs.js +1 -1
- package/lib/commands/model-docs.js.map +1 -1
- package/lib/commands/projects-args.js.map +1 -1
- package/lib/commands/query-args.js.map +1 -1
- package/lib/commands/reflect-args.js.map +1 -1
- package/lib/commands/reflect-report.js.map +1 -1
- package/lib/commands/reflect.js +1 -1
- package/lib/commands/reflect.js.map +1 -1
- package/lib/commands/save-args.js.map +1 -1
- package/lib/commands/save.d.ts +5 -0
- package/lib/commands/save.d.ts.map +1 -1
- package/lib/commands/save.js +29 -4
- package/lib/commands/save.js.map +1 -1
- package/lib/commands/validate-args.js.map +1 -1
- package/lib/commands/validate-report.js.map +1 -1
- package/lib/commands/validate.js +1 -1
- package/lib/commands/validate.js.map +1 -1
- package/lib/commands/watch-args.js.map +1 -1
- package/lib/commands/watch.d.ts.map +1 -1
- package/lib/commands/watch.js +18 -0
- package/lib/commands/watch.js.map +1 -1
- package/lib/driver-heap.d.ts +52 -0
- package/lib/driver-heap.d.ts.map +1 -0
- package/lib/driver-heap.js +51 -0
- package/lib/driver-heap.js.map +1 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/package.json +35 -34
- package/src/commands/analyze-heap.ts +4 -1
- package/src/commands/ast-ground-truth.ts +1 -1
- package/src/commands/generate-ast-builder.ts +246 -0
- package/src/commands/generate-transfer-model-args.ts +11 -1
- package/src/commands/generate-transfer-model-config.ts +3 -1
- package/src/commands/generate-transfer-model.ts +25 -3
- package/src/commands/headless-harness.ts +22 -4
- package/src/commands/init-templates.ts +81 -56
- package/src/commands/init-workspace.ts +17 -7
- package/src/commands/init.ts +12 -13
- package/src/commands/lint-grammar.ts +1 -1
- package/src/commands/measure-memory-driver.ts +3 -2
- package/src/commands/measure-memory.ts +1 -1
- package/src/commands/model-docs.ts +1 -1
- package/src/commands/reflect.ts +1 -1
- package/src/commands/save.ts +45 -4
- package/src/commands/validate.ts +1 -1
- package/src/commands/watch.ts +24 -1
- package/src/driver-heap.ts +65 -0
- package/src/index.ts +1 -0
|
@@ -82,16 +82,6 @@ export interface InitTemplate {
|
|
|
82
82
|
content: string;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
/**
|
|
86
|
-
* The version every framework package carries before the first release.
|
|
87
|
-
*
|
|
88
|
-
* A scaffold made by a CLI still at this version pins a range the registry
|
|
89
|
-
* cannot serve, so the emitted README keeps its yalc note and `init` keeps its
|
|
90
|
-
* install warning while — and only while — it holds: from a published CLI the
|
|
91
|
-
* derived pins resolve and either note would be false as printed.
|
|
92
|
-
*/
|
|
93
|
-
export const UNPUBLISHED_FRAMEWORK_VERSION = '0.0.0';
|
|
94
|
-
|
|
95
85
|
/**
|
|
96
86
|
* The version `init` pins every `@hydranium/*` dependency at: the scaffolding
|
|
97
87
|
* CLI's own.
|
|
@@ -177,7 +167,7 @@ function columnsFor(composition: InitComposition): number {
|
|
|
177
167
|
|
|
178
168
|
/**
|
|
179
169
|
* A named import on one line while it fits the column budget, else one symbol
|
|
180
|
-
* per line. Emitted files are supposed to pass a
|
|
170
|
+
* per line. Emitted files are supposed to pass a format check as-is, and the
|
|
181
171
|
* generated-module import grows by one symbol per grammar.
|
|
182
172
|
*/
|
|
183
173
|
function importList(symbols: readonly string[], from: string, columns: number, typeOnly = false): string {
|
|
@@ -260,7 +250,7 @@ function dependencyBlock(composition: InitComposition, indent: string): string {
|
|
|
260
250
|
// npm convention, and `npm install` rewrites `package.json` at two spaces
|
|
261
251
|
// regardless — so three would not survive the adopter's first install.
|
|
262
252
|
//
|
|
263
|
-
// The KEY ORDER is
|
|
253
|
+
// The KEY ORDER is the one Oxfmt's `sortPackageJson` canonicalises to, `private`
|
|
264
254
|
// before `license` included. Any other order is rewritten by the scaffolded
|
|
265
255
|
// project's own first `format` run — a diff on a file the adopter never touched
|
|
266
256
|
// — and it makes a byte-compare against a formatted copy of this emission
|
|
@@ -291,16 +281,9 @@ const PACKAGE_JSON = `{
|
|
|
291
281
|
"version": "0.0.0",
|
|
292
282
|
__PRIVATE__ "description": "__NAME__ language server, built with the Hydranium framework.",
|
|
293
283
|
"keywords": [
|
|
294
|
-
|
|
295
|
-
"langium",
|
|
296
|
-
"language-server",
|
|
297
|
-
"__PROJECT_ID__"
|
|
298
|
-
],
|
|
284
|
+
__KEYWORDS__ ],
|
|
299
285
|
"license": "UNLICENSED",
|
|
300
286
|
"author": "",
|
|
301
|
-
"type": "module",
|
|
302
|
-
"main": "lib/index.js",
|
|
303
|
-
"types": "lib/index.d.ts",
|
|
304
287
|
"bin": {
|
|
305
288
|
__BIN__ },
|
|
306
289
|
"files": [
|
|
@@ -308,11 +291,14 @@ __BIN__ },
|
|
|
308
291
|
"src",
|
|
309
292
|
"syntaxes"
|
|
310
293
|
],
|
|
294
|
+
"type": "module",
|
|
295
|
+
"main": "lib/index.js",
|
|
296
|
+
"types": "lib/index.d.ts",
|
|
311
297
|
"scripts": {
|
|
312
298
|
"build": "npm run generate && tsc",
|
|
313
|
-
"clean": "rimraf lib syntaxes src/language-server/generated src/language-server/generated-
|
|
299
|
+
"clean": "rimraf lib syntaxes src/language-server/generated src/language-server/generated-hydranium tsconfig.tsbuildinfo",
|
|
314
300
|
"generate": "npm run langium:generate && npm run generate:transfer-model",
|
|
315
|
-
"generate:transfer-model": "hydranium-cli generate-transfer-model --ast-file src/language-server/generated/ast.ts --augmentation-file src/language-server/ast.ts --out-file src/language-server/generated-
|
|
301
|
+
"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\\"",
|
|
316
302
|
"langium:generate": "langium generate",
|
|
317
303
|
"langium:watch": "langium generate --watch",
|
|
318
304
|
__LINT__ "start": "node lib/main.js --stdio",
|
|
@@ -328,7 +314,7 @@ __DEPENDENCIES__
|
|
|
328
314
|
"@types/node": "^22.0.0",
|
|
329
315
|
"langium-cli": "4.3.0",
|
|
330
316
|
"rimraf": "^5.0.0",
|
|
331
|
-
"typescript": "^
|
|
317
|
+
"typescript": "^7.0.2",
|
|
332
318
|
"vitest": "^4.0.0"
|
|
333
319
|
},
|
|
334
320
|
"engines": {
|
|
@@ -359,7 +345,7 @@ function npmRun(composition: InitComposition): string {
|
|
|
359
345
|
* name. Without the second key a scaffolded project has no command line that
|
|
360
346
|
* reaches its own data head at all.
|
|
361
347
|
*
|
|
362
|
-
* Alphabetical, because `
|
|
348
|
+
* Alphabetical, because Oxfmt's `sortPackageJson` sorts `bin` and any other
|
|
363
349
|
* order is rewritten by the scaffolded project's first `format` run.
|
|
364
350
|
*/
|
|
365
351
|
function binBlock(composition: InitComposition): string {
|
|
@@ -374,13 +360,29 @@ function binBlock(composition: InitComposition): string {
|
|
|
374
360
|
function packageJson(composition: InitComposition): string {
|
|
375
361
|
const { scope } = composition.packaging;
|
|
376
362
|
const packageName = scope === undefined ? composition.projectId : `${scope}/${composition.projectId}`;
|
|
377
|
-
|
|
363
|
+
const keywords =
|
|
364
|
+
['hydranium', 'langium', 'language-server', composition.projectId]
|
|
365
|
+
.sort()
|
|
366
|
+
.map(keyword => ` ${JSON.stringify(keyword)}`)
|
|
367
|
+
.join(',\n') + '\n';
|
|
368
|
+
// Emitted only where a root lint config says the repo lints. The absence is
|
|
378
369
|
// the dangerous direction: a task runner runs a script only where one is
|
|
379
370
|
// declared, so a package with no `lint` is SKIPPED rather than reported, and
|
|
380
371
|
// that reads as a clean lint. A wrong invocation is one visible line to edit.
|
|
381
|
-
const
|
|
372
|
+
const workspace = composition.packaging.workspace;
|
|
373
|
+
const oxlintConfig =
|
|
374
|
+
workspace?.oxlintConfig === undefined
|
|
375
|
+
? undefined
|
|
376
|
+
: `${'../'.repeat(workspace.targetPath.split('/').length)}${workspace.oxlintConfig}`;
|
|
377
|
+
const lint =
|
|
378
|
+
workspace?.oxlintConfig !== undefined
|
|
379
|
+
? ` "lint": "oxlint --report-unused-disable-directives --format stylish --config ${oxlintConfig} src test --max-warnings 0",\n`
|
|
380
|
+
: workspace?.eslintConfig !== undefined
|
|
381
|
+
? ' "lint": "eslint src test --max-warnings 0",\n'
|
|
382
|
+
: '';
|
|
382
383
|
return project(PACKAGE_JSON, composition)
|
|
383
384
|
.replace('__PACKAGE_NAME__', packageName)
|
|
385
|
+
.replace('__KEYWORDS__', keywords)
|
|
384
386
|
.replace('__BIN__', binBlock(composition))
|
|
385
387
|
.replace('__LINT__', lint)
|
|
386
388
|
.replace('__PRIVATE__', composition.packaging.private ? ' "private": true,\n' : '')
|
|
@@ -439,6 +441,11 @@ const TSCONFIG_COMPILER_OPTIONS: ReadonlyArray<readonly [string, JsonValue]> = [
|
|
|
439
441
|
['outDir', 'lib'],
|
|
440
442
|
['strict', true],
|
|
441
443
|
['esModuleInterop', true],
|
|
444
|
+
// A server that renders its own user-facing messages loads a catalogue, and
|
|
445
|
+
// a catalogue is JSON. Without this the import does not resolve at all, and
|
|
446
|
+
// without the `include` glob below a composite project rejects it with
|
|
447
|
+
// TS6307 naming neither cause.
|
|
448
|
+
['resolveJsonModule', true],
|
|
442
449
|
['skipLibCheck', true],
|
|
443
450
|
['declaration', true],
|
|
444
451
|
['experimentalDecorators', true],
|
|
@@ -480,7 +487,7 @@ function tsconfigJson(composition: InitComposition): string {
|
|
|
480
487
|
];
|
|
481
488
|
const extendsLine = workspace?.baseTsconfig === undefined ? '' : ` "extends": "${workspace.baseTsconfig}",\n`;
|
|
482
489
|
const body = options.map(([key, value]) => ` "${key}": ${JSON.stringify(value)}`).join(',\n');
|
|
483
|
-
return `{\n${extendsLine} "compilerOptions": {\n${body}\n },\n "include": ["src"]\n}\n`;
|
|
490
|
+
return `{\n${extendsLine} "compilerOptions": {\n${body}\n },\n "include": ["src", "src/**/*.json"]\n}\n`;
|
|
484
491
|
}
|
|
485
492
|
|
|
486
493
|
// `isolatedModules` is what makes this check agree with the transform that
|
|
@@ -498,7 +505,7 @@ const TSCONFIG_TEST = `{
|
|
|
498
505
|
"isolatedModules": true,
|
|
499
506
|
"types": ["node"]
|
|
500
507
|
},
|
|
501
|
-
"include": ["src", "test"]
|
|
508
|
+
"include": ["src", "test", "src/**/*.json"]
|
|
502
509
|
}
|
|
503
510
|
`;
|
|
504
511
|
|
|
@@ -708,13 +715,35 @@ export type __NAME__Services = LangiumServices &
|
|
|
708
715
|
LspServerAddedServices & {
|
|
709
716
|
shared: __NAME__SharedServices;
|
|
710
717
|
};
|
|
718
|
+
|
|
719
|
+
/** What a host or a test may vary about this composition. */
|
|
720
|
+
export interface __NAME__Options {
|
|
721
|
+
/**
|
|
722
|
+
* Shared modules layered in after the framework's own bindings.
|
|
723
|
+
*
|
|
724
|
+
* The framework constructs most shared services with no options —
|
|
725
|
+
* \`DocumentBuilder: services => new HydraniumDocumentBuilder(services)\` — so
|
|
726
|
+
* rebinding the slot is the only way to boot one configured differently, and
|
|
727
|
+
* a factory that hard-codes its composition leaves a test nowhere to do it.
|
|
728
|
+
* That is what this is for: pass a module binding \`workspace.DocumentBuilder\`
|
|
729
|
+
* to exercise a builder option, or to substitute a subclass.
|
|
730
|
+
*
|
|
731
|
+
* Composed LAST, after \`__NAME__SharedModule\`, so it wins over every other
|
|
732
|
+
* tier including this file's own bindings — which is what makes it usable for
|
|
733
|
+
* a slot the adopter overrides. Production code should not reach for it.
|
|
734
|
+
*/
|
|
735
|
+
readonly extraSharedModules?: ReadonlyArray<Module<__NAME__SharedServices, DeepPartial<__NAME__SharedServices>>>;
|
|
736
|
+
}
|
|
711
737
|
${configurationRoot}
|
|
712
738
|
${sharedModule}
|
|
713
739
|
|
|
714
740
|
${languageModules}
|
|
715
741
|
|
|
716
742
|
/** Compose the Langium DI tree for __NAME__ — returns the shared + language services. */
|
|
717
|
-
export function create__NAME__Services(
|
|
743
|
+
export function create__NAME__Services(
|
|
744
|
+
context: Partial<ServerModuleContext> = EmptyFileSystem,
|
|
745
|
+
options: __NAME__Options = {}
|
|
746
|
+
): {
|
|
718
747
|
shared: __NAME__SharedServices;
|
|
719
748
|
${returnType}
|
|
720
749
|
} {
|
|
@@ -724,7 +753,8 @@ ${returnType}
|
|
|
724
753
|
sharedModules: {
|
|
725
754
|
generated: __NAME__GeneratedSharedModule,
|
|
726
755
|
adopter: __NAME__SharedModule,
|
|
727
|
-
extra: [createLspServerSharedModule(fullContext)]
|
|
756
|
+
extra: [createLspServerSharedModule(fullContext)],
|
|
757
|
+
overrides: options.extraSharedModules
|
|
728
758
|
},
|
|
729
759
|
languageModules: {
|
|
730
760
|
generated: ${primary.grammar}GeneratedModule,
|
|
@@ -791,9 +821,9 @@ const SERIALIZER = `// Concrete-syntax emitter for __GRAMMAR__ — the parser's
|
|
|
791
821
|
//
|
|
792
822
|
// Without it every structured write fails: the framework's default binding at
|
|
793
823
|
// \`services.serializer.Serializer\` THROWS, because a concrete syntax is
|
|
794
|
-
// language knowledge no framework can derive. \`
|
|
795
|
-
// the data head's \`saveModelDocument\` and a GLSP \`SaveModelAction\`
|
|
796
|
-
// it.
|
|
824
|
+
// language knowledge no framework can derive. A client session's \`update\` /
|
|
825
|
+
// \`save\`, the data head's \`saveModelDocument\` and a GLSP \`SaveModelAction\`
|
|
826
|
+
// all reach it.
|
|
797
827
|
//
|
|
798
828
|
// \`AbstractSerializer\`'s generic property walk lays out FORMAT-structured
|
|
799
829
|
// output — its YAML and JSON subclasses are what it exists for — and cannot
|
|
@@ -980,7 +1010,7 @@ import { startGlspServer } from '@hydranium/glsp-server/node';
|
|
|
980
1010
|
// \`TransferElement\` structurally, so naming the AST type here compiles fine and
|
|
981
1011
|
// silently tells every typed client that a reference is a resolvable object
|
|
982
1012
|
// rather than a name.
|
|
983
|
-
${importList(roots, './language-server/generated-
|
|
1013
|
+
${importList(roots, './language-server/generated-hydranium/transfer-model.js', columns, true)}
|
|
984
1014
|
`
|
|
985
1015
|
: '';
|
|
986
1016
|
|
|
@@ -1063,11 +1093,17 @@ void glspServer;
|
|
|
1063
1093
|
// than a library entry — import \`./index.js\` instead to compose the language.
|
|
1064
1094
|
|
|
1065
1095
|
${reflectImport}${glspImports}${importList(coreNodeSymbols, '@hydranium/core/node', columns)}
|
|
1066
|
-
${data ? "import { DataServer } from '@hydranium/data-server';\n" : ''}
|
|
1096
|
+
${data ? "import { DataServer } from '@hydranium/data-server';\n" : ''}// The framework's entry point, NOT Langium's \`@hydranium/langium/lsp\` one. It is
|
|
1097
|
+
// signature-compatible and delegates straight through; what it adds first is
|
|
1098
|
+
// \`assertLspHeadComposed\`, which fails the start if the LSP head's SHARED module
|
|
1099
|
+
// was never composed. Reaching for Langium's is the natural mistake and it is
|
|
1100
|
+
// silent: the server boots, links and completes, while echo suppression, the
|
|
1101
|
+
// didChangeContent debounce and the last-client-close rebuild are all inert.
|
|
1102
|
+
import { startLanguageServer, withHydraniumLspFeatures } from '@hydranium/core/lsp';
|
|
1067
1103
|
import { ProposedFeatures, createConnection } from 'vscode-languageserver/node';
|
|
1068
1104
|
${diagramImports}${transferImports}import { create__NAME__Services } from './language-server/__PROJECT_ID__-module.js';
|
|
1069
1105
|
${portCommands === '' ? '' : '\n' + portCommands}
|
|
1070
|
-
const connection = createConnection(ProposedFeatures.all);
|
|
1106
|
+
const connection = createConnection(withHydraniumLspFeatures(ProposedFeatures.all));
|
|
1071
1107
|
const { shared } = create__NAME__Services({ connection, ...NodeFileSystem });
|
|
1072
1108
|
startLanguageServer(shared);
|
|
1073
1109
|
${dataBlock}${glspBlock}`;
|
|
@@ -1117,7 +1153,7 @@ function dataServerMainFile(composition: InitComposition): string {
|
|
|
1117
1153
|
${importList(['NodeFileSystem', 'startStdioServer'], '@hydranium/core/node', columns)}
|
|
1118
1154
|
import { DataServer } from '@hydranium/data-server';
|
|
1119
1155
|
// The TRANSFER root${plural}, not the AST one${plural} — same reasoning as \`main.ts\`.
|
|
1120
|
-
${importList(roots, './language-server/generated-
|
|
1156
|
+
${importList(roots, './language-server/generated-hydranium/transfer-model.js', columns, true)}
|
|
1121
1157
|
import { create__NAME__Services } from './language-server/__PROJECT_ID__-module.js';
|
|
1122
1158
|
|
|
1123
1159
|
const { shared } = create__NAME__Services({ ...NodeFileSystem });
|
|
@@ -1236,8 +1272,8 @@ function serializationTest(composition: InitComposition): string {
|
|
|
1236
1272
|
const transferTypes = composition.grammars.map(grammar => `${grammar.entryRule} as Transfer${grammar.entryRule}`);
|
|
1237
1273
|
const text = `// Round-trips each grammar through its serializer: parse the source, serialize
|
|
1238
1274
|
// the model, compare the text. The serializer is the parser's inverse, so this
|
|
1239
|
-
// is the assertion that keeps \`
|
|
1240
|
-
// language server can read back.
|
|
1275
|
+
// is the assertion that keeps a client session's \`update\` / \`save\` writing
|
|
1276
|
+
// files the language server can read back.
|
|
1241
1277
|
//
|
|
1242
1278
|
// The transfer case is the one that catches the mistake worth catching.
|
|
1243
1279
|
// \`ModelService.modelToText\` short-circuits only a RAW STRING, so a typed model
|
|
@@ -1247,7 +1283,7 @@ function serializationTest(composition: InitComposition): string {
|
|
|
1247
1283
|
import { parseHelper } from '@hydranium/core/testing';
|
|
1248
1284
|
import { describe, expect, it } from 'vitest';
|
|
1249
1285
|
${importList(astTypes, '../src/language-server/ast.js', columns, true)}
|
|
1250
|
-
${importList(transferTypes, '../src/language-server/generated-
|
|
1286
|
+
${importList(transferTypes, '../src/language-server/generated-hydranium/transfer-model.js', columns, true)}
|
|
1251
1287
|
import { createServices } from '../src/services.js';
|
|
1252
1288
|
|
|
1253
1289
|
${composition.grammars.map(grammar => render(SERIALIZATION_SUITE, composition, grammar)).join('\n\n')}
|
|
@@ -1416,20 +1452,6 @@ function readme(composition: InitComposition): string {
|
|
|
1416
1452
|
'',
|
|
1417
1453
|
'## Getting started',
|
|
1418
1454
|
'',
|
|
1419
|
-
// Conditional on the pin this scaffold actually carries — see
|
|
1420
|
-
// `UNPUBLISHED_FRAMEWORK_VERSION`.
|
|
1421
|
-
...(composition.frameworkVersion === UNPUBLISHED_FRAMEWORK_VERSION
|
|
1422
|
-
? [
|
|
1423
|
-
'> **Pre-publish note.** `@hydranium/*` is not on npm yet, so the `0.0.0`',
|
|
1424
|
-
'> pins below are placeholders and `npm install` will fail with a 404 until',
|
|
1425
|
-
'> the framework is released. Until then, supply the packages from a local',
|
|
1426
|
-
'> framework checkout with [yalc](https://github.com/wclr/yalc) — a plain',
|
|
1427
|
-
'> `file:` path or `npm link` is not enough, because the framework packages',
|
|
1428
|
-
'> depend on each other by version and npm would try to fetch those from the',
|
|
1429
|
-
'> registry too.',
|
|
1430
|
-
''
|
|
1431
|
-
]
|
|
1432
|
-
: []),
|
|
1433
1455
|
'```bash',
|
|
1434
1456
|
'npm install',
|
|
1435
1457
|
'npm run langium:generate # generate the AST from the grammar',
|
|
@@ -1633,8 +1655,11 @@ export class __GRAMMAR__GlspState extends FullTextHydraniumGlspState<__ENTRY_RUL
|
|
|
1633
1655
|
|
|
1634
1656
|
const storage = `// Source-model storage for the __GRAMMAR__ diagram, inheriting both framework
|
|
1635
1657
|
// defaults: \`loadSourceModel\` (open + settle + \`setSourceRoot\`) and
|
|
1636
|
-
// \`saveSourceModel\` (
|
|
1637
|
-
//
|
|
1658
|
+
// \`saveSourceModel\` (flush the store's text for the primary and every tracked
|
|
1659
|
+
// secondary through \`AstDocumentManager.save\` → \`WritableFileSystemProvider\`).
|
|
1660
|
+
//
|
|
1661
|
+
// No serializer runs on the save path — a save persists what the diagram's
|
|
1662
|
+
// operations already wrote to the store.
|
|
1638
1663
|
//
|
|
1639
1664
|
// The subclass exists so a bespoke load or save has a stable place to land.
|
|
1640
1665
|
|
|
@@ -186,10 +186,12 @@ export interface WorkspaceDetection {
|
|
|
186
186
|
readonly baseTsconfig?: string;
|
|
187
187
|
/** That tsconfig's `compilerOptions`, so the emitted one can drop what it inherits. */
|
|
188
188
|
readonly baseCompilerOptions?: Readonly<Record<string, JsonValue>>;
|
|
189
|
-
/** The root
|
|
189
|
+
/** The root formatter config's `printWidth`, so emitted sources wrap where the repo wraps. */
|
|
190
190
|
readonly printWidth?: number;
|
|
191
191
|
/** Filename of the root eslint config, when there is one. Absent means the repo does not lint. */
|
|
192
192
|
readonly eslintConfig?: string;
|
|
193
|
+
/** Filename of the root Oxlint config, when present. */
|
|
194
|
+
readonly oxlintConfig?: string;
|
|
193
195
|
}
|
|
194
196
|
|
|
195
197
|
/**
|
|
@@ -339,7 +341,7 @@ export function findBaseTsconfig(
|
|
|
339
341
|
}
|
|
340
342
|
|
|
341
343
|
/**
|
|
342
|
-
* The repo's
|
|
344
|
+
* The repo's formatter `printWidth`, so the emitted sources wrap where the repo
|
|
343
345
|
* wraps instead of at the scaffold's own default.
|
|
344
346
|
*
|
|
345
347
|
* **Why this is worth detecting at all.** There is no width that is stable for
|
|
@@ -360,19 +362,21 @@ export function findBaseTsconfig(
|
|
|
360
362
|
* adopter's formatter rewraps on first commit. So a heuristic that is usually
|
|
361
363
|
* right strictly improves on no detection, and can break nothing.
|
|
362
364
|
*/
|
|
363
|
-
export function
|
|
365
|
+
export function findFormatterPrintWidth(rootDir: string, probe: WorkspaceProbe): number | undefined {
|
|
364
366
|
const manifest = probe.readJson(path.join(rootDir, 'package.json'));
|
|
365
367
|
if (isJsonObject(manifest) && isJsonObject(manifest.prettier) && typeof manifest.prettier.printWidth === 'number') {
|
|
366
368
|
return manifest.prettier.printWidth;
|
|
367
369
|
}
|
|
368
370
|
const present = probe.listFiles(rootDir);
|
|
369
|
-
for (const file of present.filter(name => name === '.prettierrc' || name === '.prettierrc.json')) {
|
|
371
|
+
for (const file of present.filter(name => name === '.oxfmtrc.json' || name === '.prettierrc' || name === '.prettierrc.json')) {
|
|
370
372
|
const parsed = probe.readJson(path.join(rootDir, file));
|
|
371
373
|
if (isJsonObject(parsed) && typeof parsed.printWidth === 'number') {
|
|
372
374
|
return parsed.printWidth;
|
|
373
375
|
}
|
|
374
376
|
}
|
|
375
|
-
for (const file of present.filter(name =>
|
|
377
|
+
for (const file of present.filter(name =>
|
|
378
|
+
/^(\.prettierrc\.[cm]?[jt]s|prettier\.config\.[cm]?[jt]s|oxfmt\.config\.[cm]?[jt]s)$/.test(name)
|
|
379
|
+
)) {
|
|
376
380
|
const width = /\bprintWidth\s*:\s*(\d+)/.exec(probe.readText(path.join(rootDir, file)) ?? '');
|
|
377
381
|
if (width) {
|
|
378
382
|
return Number(width[1]);
|
|
@@ -398,6 +402,11 @@ export function findEslintConfig(rootDir: string, probe: WorkspaceProbe): string
|
|
|
398
402
|
return probe.listFiles(rootDir).find(name => /^eslint\.config\.[cm]?[jt]s$/.test(name) || /^\.eslintrc(\..+)?$/.test(name));
|
|
399
403
|
}
|
|
400
404
|
|
|
405
|
+
/** Detect Oxlint without evaluating executable configuration files. */
|
|
406
|
+
export function findOxlintConfig(rootDir: string, probe: WorkspaceProbe): string | undefined {
|
|
407
|
+
return probe.listFiles(rootDir).find(name => /^(oxlint\.config\.[cm]?[jt]s|\.oxlintrc\.jsonc?)$/.test(name));
|
|
408
|
+
}
|
|
409
|
+
|
|
401
410
|
/** POSIX-style relative path, which is what both `extends` and `--prefix` want on every platform. */
|
|
402
411
|
function posixRelative(from: string, to: string): string {
|
|
403
412
|
return path.relative(from, to).split(path.sep).join('/');
|
|
@@ -436,7 +445,8 @@ export function detectWorkspace(targetDir: string, probe: WorkspaceProbe): Works
|
|
|
436
445
|
scope: inferPackageScope(rootDir, workspaces, probe),
|
|
437
446
|
baseTsconfig: base && posixRelative(absoluteTarget, path.join(rootDir, base.file)),
|
|
438
447
|
baseCompilerOptions: base?.compilerOptions,
|
|
439
|
-
printWidth:
|
|
440
|
-
eslintConfig: findEslintConfig(rootDir, probe)
|
|
448
|
+
printWidth: findFormatterPrintWidth(rootDir, probe),
|
|
449
|
+
eslintConfig: findEslintConfig(rootDir, probe),
|
|
450
|
+
oxlintConfig: findOxlintConfig(rootDir, probe)
|
|
441
451
|
};
|
|
442
452
|
}
|
package/src/commands/init.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import * as fs from 'node:fs';
|
|
11
11
|
import * as path from 'node:path';
|
|
12
|
-
import { buildInitTemplates, readFrameworkVersion
|
|
12
|
+
import { buildInitTemplates, readFrameworkVersion } from './init-templates.js';
|
|
13
13
|
import { createNodeWorkspaceProbe, detectWorkspace, type JsonValue, type WorkspaceProbe } from './init-workspace.js';
|
|
14
14
|
|
|
15
15
|
/**
|
|
@@ -128,10 +128,12 @@ export interface InitWorkspacePlacement {
|
|
|
128
128
|
readonly baseTsconfig?: string;
|
|
129
129
|
/** That tsconfig's `compilerOptions`, so the emitted one can omit what it would inherit unchanged. */
|
|
130
130
|
readonly baseCompilerOptions?: Readonly<Record<string, JsonValue>>;
|
|
131
|
-
/** The workspace's
|
|
131
|
+
/** The workspace's formatter `printWidth`, so emitted sources wrap where the repo wraps. */
|
|
132
132
|
readonly printWidth?: number;
|
|
133
133
|
/** Filename of the workspace's eslint config; its presence is what makes the scaffold emit a `lint` script. */
|
|
134
134
|
readonly eslintConfig?: string;
|
|
135
|
+
/** Filename of the workspace Oxlint config, when present. */
|
|
136
|
+
readonly oxlintConfig?: string;
|
|
135
137
|
}
|
|
136
138
|
|
|
137
139
|
/**
|
|
@@ -466,7 +468,8 @@ export function resolveInitPackaging(targetDir: string, options: InitPackagingOp
|
|
|
466
468
|
baseTsconfig: detection.baseTsconfig,
|
|
467
469
|
baseCompilerOptions: detection.baseCompilerOptions,
|
|
468
470
|
printWidth: detection.printWidth,
|
|
469
|
-
eslintConfig: detection.eslintConfig
|
|
471
|
+
eslintConfig: detection.eslintConfig,
|
|
472
|
+
oxlintConfig: detection.oxlintConfig
|
|
470
473
|
}
|
|
471
474
|
};
|
|
472
475
|
}
|
|
@@ -515,10 +518,12 @@ export function runInit(options: InitCommandOptions): void {
|
|
|
515
518
|
}
|
|
516
519
|
write(" .gitignore holds `syntaxes/` only — the workspace root's covers the rest.\n");
|
|
517
520
|
if (detection.printWidth !== undefined) {
|
|
518
|
-
write(` Sources wrapped at ${detection.printWidth} columns, from ${detection.rootDir}'s
|
|
521
|
+
write(` Sources wrapped at ${detection.printWidth} columns, from ${detection.rootDir}'s formatter config.\n`);
|
|
519
522
|
}
|
|
520
|
-
if (detection.eslintConfig !== undefined) {
|
|
521
|
-
write(
|
|
523
|
+
if (detection.oxlintConfig !== undefined || detection.eslintConfig !== undefined) {
|
|
524
|
+
write(
|
|
525
|
+
` A lint script was added — ${detection.oxlintConfig ?? detection.eslintConfig} says this repo lints. Adjust it if your invocation differs.\n`
|
|
526
|
+
);
|
|
522
527
|
}
|
|
523
528
|
// Printed rather than applied: `init` writes inside the target directory
|
|
524
529
|
// and nowhere else, so a scaffold can never surprise-edit a manifest that
|
|
@@ -536,13 +541,7 @@ export function runInit(options: InitCommandOptions): void {
|
|
|
536
541
|
write('\n');
|
|
537
542
|
write('Next steps:\n');
|
|
538
543
|
write(` cd ${options.targetDir}\n`);
|
|
539
|
-
|
|
540
|
-
// `UNPUBLISHED_FRAMEWORK_VERSION`.
|
|
541
|
-
write(
|
|
542
|
-
composition.frameworkVersion === UNPUBLISHED_FRAMEWORK_VERSION
|
|
543
|
-
? ' npm install # 404s until @hydranium/* is published — see README\n'
|
|
544
|
-
: ' npm install\n'
|
|
545
|
-
);
|
|
544
|
+
write(' npm install\n');
|
|
546
545
|
write(' npm run build # langium generate + tsc\n');
|
|
547
546
|
write(' npm test # the scaffolded DI-composition test\n');
|
|
548
547
|
// `npx`, not a bare invocation: `@hydranium/cli` is a devDependency of the
|
|
@@ -53,5 +53,5 @@ export function buildLintGrammarDriverArgs(options: LintGrammarCommandOptions):
|
|
|
53
53
|
* `createServices` module, keeping the binary language-agnostic.
|
|
54
54
|
*/
|
|
55
55
|
export function runLintGrammar(options: LintGrammarCommandOptions): Promise<void> {
|
|
56
|
-
return runDriverChild([
|
|
56
|
+
return runDriverChild([DRIVER, ...buildLintGrammarDriverArgs(options)], options);
|
|
57
57
|
}
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
|
|
10
10
|
/*
|
|
11
11
|
* Spawned child of the `measure-memory` subcommand. Launched by the parent with
|
|
12
|
-
* `node --expose-gc
|
|
13
|
-
* so the underlying harness gets post-GC readings.
|
|
12
|
+
* `node [heap ceiling] --expose-gc <this> --services <module> <workspace> ...`
|
|
13
|
+
* so the underlying harness gets post-GC readings. The ceiling is whatever
|
|
14
|
+
* `driverHeapArgs` decides, which is nothing at all inside a container. It dynamic-imports the head's
|
|
14
15
|
* services module and runs the framework's `measureModelMemory`, streaming the
|
|
15
16
|
* baseline / after-build / churn lines to stdout. Never imported — only spawned.
|
|
16
17
|
*/
|
|
@@ -139,5 +139,5 @@ export function buildDriverArgs(options: MeasureMemoryCommandOptions): string[]
|
|
|
139
139
|
* module and reports the baseline / after-build / churn lines on stdout.
|
|
140
140
|
*/
|
|
141
141
|
export function runMeasureMemory(options: MeasureMemoryCommandOptions): Promise<void> {
|
|
142
|
-
return runDriverChild(['--expose-gc',
|
|
142
|
+
return runDriverChild(['--expose-gc', DRIVER, ...buildDriverArgs(options)], options);
|
|
143
143
|
}
|
|
@@ -44,5 +44,5 @@ export function buildModelDocsDriverArgs(options: ModelDocsCommandOptions): stri
|
|
|
44
44
|
* module, keeping the binary language-agnostic.
|
|
45
45
|
*/
|
|
46
46
|
export function runModelDocs(options: ModelDocsCommandOptions): Promise<void> {
|
|
47
|
-
return runDriverChild([
|
|
47
|
+
return runDriverChild([DRIVER, ...buildModelDocsDriverArgs(options)], options);
|
|
48
48
|
}
|
package/src/commands/reflect.ts
CHANGED
|
@@ -47,5 +47,5 @@ export function buildReflectDriverArgs(options: ReflectCommandOptions): string[]
|
|
|
47
47
|
* the binary language-agnostic.
|
|
48
48
|
*/
|
|
49
49
|
export function runReflect(options: ReflectCommandOptions): Promise<void> {
|
|
50
|
-
return runDriverChild([
|
|
50
|
+
return runDriverChild([DRIVER, ...buildReflectDriverArgs(options)], options);
|
|
51
51
|
}
|
package/src/commands/save.ts
CHANGED
|
@@ -23,6 +23,11 @@ import { withDataServer } from '../spawn-data-server.js';
|
|
|
23
23
|
*
|
|
24
24
|
* `clientId` defaults to `'hydranium-cli'` — adopters wanting a richer
|
|
25
25
|
* identity (per-user, per-script) override.
|
|
26
|
+
*
|
|
27
|
+
* The command saves as a client session under `clientId`: it registers the
|
|
28
|
+
* session, creates the document from the content when there is no file,
|
|
29
|
+
* opens it otherwise, saves, and ends the session, which leaves nothing open
|
|
30
|
+
* behind it.
|
|
26
31
|
*/
|
|
27
32
|
export interface SaveCommandOptions {
|
|
28
33
|
readonly serverCommand: string;
|
|
@@ -46,8 +51,7 @@ export async function runSave(options: SaveCommandOptions): Promise<void> {
|
|
|
46
51
|
const model = await resolveContent(options.content, options.__readFileForTest);
|
|
47
52
|
|
|
48
53
|
if (options.__proxyForTest) {
|
|
49
|
-
|
|
50
|
-
write(`${JSON.stringify(doc)}\n`);
|
|
54
|
+
write(`${JSON.stringify(await saveOpened(options.__proxyForTest, options.uri, clientId, model))}\n`);
|
|
51
55
|
return;
|
|
52
56
|
}
|
|
53
57
|
|
|
@@ -59,12 +63,49 @@ export async function runSave(options: SaveCommandOptions): Promise<void> {
|
|
|
59
63
|
env: options.logLevel ? logLevelEnv(options.logLevel) : undefined
|
|
60
64
|
},
|
|
61
65
|
async server => {
|
|
62
|
-
|
|
63
|
-
write(`${JSON.stringify(doc)}\n`);
|
|
66
|
+
write(`${JSON.stringify(await saveOpened(server, options.uri, clientId, model))}\n`);
|
|
64
67
|
}
|
|
65
68
|
);
|
|
66
69
|
}
|
|
67
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Save `model` to `uri` as the session `clientId`, and end the session whether
|
|
73
|
+
* the save succeeded or not. An end that fails after a failed save is
|
|
74
|
+
* swallowed: the save's error is the one the user needs.
|
|
75
|
+
*/
|
|
76
|
+
async function saveOpened(
|
|
77
|
+
server: Pick<
|
|
78
|
+
DataServerProtocol<TransferElement>,
|
|
79
|
+
'createSession' | 'openModelDocument' | 'createModelDocument' | 'saveModelDocument' | 'closeSession'
|
|
80
|
+
>,
|
|
81
|
+
uri: string,
|
|
82
|
+
clientId: string,
|
|
83
|
+
model: string
|
|
84
|
+
): Promise<unknown> {
|
|
85
|
+
await server.createSession({ clientId, label: 'hydranium-cli' });
|
|
86
|
+
let saved: unknown;
|
|
87
|
+
try {
|
|
88
|
+
// The server creates a document only when it finds no file and no
|
|
89
|
+
// client has it open; anything else is opened. An open that fails
|
|
90
|
+
// reports why the create failed too, since that may be the real cause.
|
|
91
|
+
await server.createModelDocument({ uri, clientId, text: model }).catch((created: unknown) =>
|
|
92
|
+
server.openModelDocument({ uri, clientId }).catch((opened: unknown) => {
|
|
93
|
+
throw new Error(`Cannot save ${uri}: ${describe(opened)} (creating it failed first: ${describe(created)})`, { cause: created });
|
|
94
|
+
})
|
|
95
|
+
);
|
|
96
|
+
saved = await server.saveModelDocument({ uri, clientId, model, basedOn: 'anything' });
|
|
97
|
+
} catch (error: unknown) {
|
|
98
|
+
await server.closeSession({ clientId }).catch(() => undefined);
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
await server.closeSession({ clientId });
|
|
102
|
+
return saved;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function describe(error: unknown): string {
|
|
106
|
+
return error instanceof Error ? error.message : String(error);
|
|
107
|
+
}
|
|
108
|
+
|
|
68
109
|
/**
|
|
69
110
|
* Resolve the `--content` argument to its textual form. The `@<path>`
|
|
70
111
|
* prefix reads from a file (parses as UTF-8); anything else is taken
|
package/src/commands/validate.ts
CHANGED
|
@@ -54,5 +54,5 @@ export function buildValidateDriverArgs(options: ValidateCommandOptions): string
|
|
|
54
54
|
* The child's exit code propagates to this process so shells and CI see the gate.
|
|
55
55
|
*/
|
|
56
56
|
export function runValidate(options: ValidateCommandOptions): Promise<void> {
|
|
57
|
-
return runDriverChild([
|
|
57
|
+
return runDriverChild([DRIVER, ...buildValidateDriverArgs(options)], options);
|
|
58
58
|
}
|
package/src/commands/watch.ts
CHANGED
|
@@ -8,7 +8,12 @@
|
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
10
|
import type { LogThreshold, TransferElement } from '@hydranium/protocol';
|
|
11
|
-
import type {
|
|
11
|
+
import type {
|
|
12
|
+
DataClientProtocol,
|
|
13
|
+
DataServerProtocol,
|
|
14
|
+
TransferDocumentDeletedEvent,
|
|
15
|
+
TransferDocumentUpdatedEvent
|
|
16
|
+
} from '@hydranium/protocol/data';
|
|
12
17
|
import { logLevelEnv } from '../log-level.js';
|
|
13
18
|
import { spawnDataServer } from '../spawn-data-server.js';
|
|
14
19
|
|
|
@@ -132,6 +137,24 @@ export async function runWatch(options: WatchCommandOptions): Promise<void> {
|
|
|
132
137
|
onDocumentSaved(): void {
|
|
133
138
|
// Persistence is out of band for the per-URI update view.
|
|
134
139
|
},
|
|
140
|
+
onDocumentDirtyChanged(): void {
|
|
141
|
+
// Out of band for the per-URI update view, as persistence is.
|
|
142
|
+
},
|
|
143
|
+
onDocumentDeleted(event: TransferDocumentDeletedEvent): void {
|
|
144
|
+
// In band, unlike the two neighbours: this is the end of the stream
|
|
145
|
+
// the view exists to show, and a consumer that never hears it waits
|
|
146
|
+
// forever for an update that cannot come. The line carries no
|
|
147
|
+
// `document`, so a reader must switch on shape rather than assume one.
|
|
148
|
+
if (event.uri !== options.uri) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
write(`${JSON.stringify(event)}\n`);
|
|
152
|
+
},
|
|
153
|
+
onDocumentsBuilt(): void {
|
|
154
|
+
// Out of band for a per-URI view by construction: this notification
|
|
155
|
+
// carries only documents nobody watches, and this command watches the
|
|
156
|
+
// one URI it was given.
|
|
157
|
+
},
|
|
135
158
|
onProjectsChanged(): void {
|
|
136
159
|
// Project lifecycle is out of band for the per-URI update view.
|
|
137
160
|
}
|