@hydranium/cli 1.0.0-next.10
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/LICENSE +21 -0
- package/README.md +240 -0
- package/heap-analysis/analyze-heap.mjs +798 -0
- package/heap-analysis/heap/core/aggregate.mjs +152 -0
- package/heap-analysis/heap/core/bucket-label.mjs +23 -0
- package/heap-analysis/heap/core/cutpoints.mjs +51 -0
- package/heap-analysis/heap/core/diff.mjs +56 -0
- package/heap-analysis/heap/core/format.mjs +47 -0
- package/heap-analysis/heap/core/holders.mjs +80 -0
- package/heap-analysis/heap/core/load.mjs +46 -0
- package/heap-analysis/heap/core/report.mjs +68 -0
- package/heap-analysis/heap/core/strings.mjs +80 -0
- package/heap-analysis/heap/langium/concepts.mjs +230 -0
- package/heap-analysis/heap/langium/glsp.classifier.mjs +80 -0
- package/heap-analysis/heap/langium/langium.classifier.mjs +203 -0
- package/heap-analysis/heap/renderer/renderer.mjs +122 -0
- package/lib/cli-version.d.ts +21 -0
- package/lib/cli-version.d.ts.map +1 -0
- package/lib/cli-version.js +30 -0
- package/lib/cli-version.js.map +1 -0
- package/lib/cli.d.ts +11 -0
- package/lib/cli.d.ts.map +1 -0
- package/lib/cli.js +135 -0
- package/lib/cli.js.map +1 -0
- package/lib/commands/analyze-heap-args.d.ts +46 -0
- package/lib/commands/analyze-heap-args.d.ts.map +1 -0
- package/lib/commands/analyze-heap-args.js +174 -0
- package/lib/commands/analyze-heap-args.js.map +1 -0
- package/lib/commands/analyze-heap.d.ts +37 -0
- package/lib/commands/analyze-heap.d.ts.map +1 -0
- package/lib/commands/analyze-heap.js +63 -0
- package/lib/commands/analyze-heap.js.map +1 -0
- package/lib/commands/ast-ground-truth-args.d.ts +23 -0
- package/lib/commands/ast-ground-truth-args.d.ts.map +1 -0
- package/lib/commands/ast-ground-truth-args.js +47 -0
- package/lib/commands/ast-ground-truth-args.js.map +1 -0
- package/lib/commands/ast-ground-truth-driver.d.ts +10 -0
- package/lib/commands/ast-ground-truth-driver.d.ts.map +1 -0
- package/lib/commands/ast-ground-truth-driver.js +72 -0
- package/lib/commands/ast-ground-truth-driver.js.map +1 -0
- package/lib/commands/ast-ground-truth.d.ts +31 -0
- package/lib/commands/ast-ground-truth.d.ts.map +1 -0
- package/lib/commands/ast-ground-truth.js +36 -0
- package/lib/commands/ast-ground-truth.js.map +1 -0
- package/lib/commands/generate-transfer-model-args.d.ts +38 -0
- package/lib/commands/generate-transfer-model-args.d.ts.map +1 -0
- package/lib/commands/generate-transfer-model-args.js +183 -0
- package/lib/commands/generate-transfer-model-args.js.map +1 -0
- package/lib/commands/generate-transfer-model-config.d.ts +32 -0
- package/lib/commands/generate-transfer-model-config.d.ts.map +1 -0
- package/lib/commands/generate-transfer-model-config.js +122 -0
- package/lib/commands/generate-transfer-model-config.js.map +1 -0
- package/lib/commands/generate-transfer-model.d.ts +110 -0
- package/lib/commands/generate-transfer-model.d.ts.map +1 -0
- package/lib/commands/generate-transfer-model.js +587 -0
- package/lib/commands/generate-transfer-model.js.map +1 -0
- package/lib/commands/harness-args.d.ts +198 -0
- package/lib/commands/harness-args.d.ts.map +1 -0
- package/lib/commands/harness-args.js +339 -0
- package/lib/commands/harness-args.js.map +1 -0
- package/lib/commands/headless-harness.d.ts +97 -0
- package/lib/commands/headless-harness.d.ts.map +1 -0
- package/lib/commands/headless-harness.js +121 -0
- package/lib/commands/headless-harness.js.map +1 -0
- package/lib/commands/init-args.d.ts +81 -0
- package/lib/commands/init-args.d.ts.map +1 -0
- package/lib/commands/init-args.js +250 -0
- package/lib/commands/init-args.js.map +1 -0
- package/lib/commands/init-prompt.d.ts +66 -0
- package/lib/commands/init-prompt.d.ts.map +1 -0
- package/lib/commands/init-prompt.js +71 -0
- package/lib/commands/init-prompt.js.map +1 -0
- package/lib/commands/init-templates.d.ts +42 -0
- package/lib/commands/init-templates.d.ts.map +1 -0
- package/lib/commands/init-templates.js +1902 -0
- package/lib/commands/init-templates.js.map +1 -0
- package/lib/commands/init-wizard.d.ts +65 -0
- package/lib/commands/init-wizard.d.ts.map +1 -0
- package/lib/commands/init-wizard.js +302 -0
- package/lib/commands/init-wizard.js.map +1 -0
- package/lib/commands/init-workspace.d.ts +168 -0
- package/lib/commands/init-workspace.d.ts.map +1 -0
- package/lib/commands/init-workspace.js +372 -0
- package/lib/commands/init-workspace.js.map +1 -0
- package/lib/commands/init.d.ts +273 -0
- package/lib/commands/init.d.ts.map +1 -0
- package/lib/commands/init.js +303 -0
- package/lib/commands/init.js.map +1 -0
- package/lib/commands/lint-grammar-args.d.ts +23 -0
- package/lib/commands/lint-grammar-args.d.ts.map +1 -0
- package/lib/commands/lint-grammar-args.js +63 -0
- package/lib/commands/lint-grammar-args.js.map +1 -0
- package/lib/commands/lint-grammar-driver.d.ts +10 -0
- package/lib/commands/lint-grammar-driver.d.ts.map +1 -0
- package/lib/commands/lint-grammar-driver.js +72 -0
- package/lib/commands/lint-grammar-driver.js.map +1 -0
- package/lib/commands/lint-grammar-report.d.ts +24 -0
- package/lib/commands/lint-grammar-report.d.ts.map +1 -0
- package/lib/commands/lint-grammar-report.js +65 -0
- package/lib/commands/lint-grammar-report.js.map +1 -0
- package/lib/commands/lint-grammar.d.ts +33 -0
- package/lib/commands/lint-grammar.d.ts.map +1 -0
- package/lib/commands/lint-grammar.js +42 -0
- package/lib/commands/lint-grammar.js.map +1 -0
- package/lib/commands/measure-memory-args.d.ts +23 -0
- package/lib/commands/measure-memory-args.d.ts.map +1 -0
- package/lib/commands/measure-memory-args.js +106 -0
- package/lib/commands/measure-memory-args.js.map +1 -0
- package/lib/commands/measure-memory-driver.d.ts +10 -0
- package/lib/commands/measure-memory-driver.d.ts.map +1 -0
- package/lib/commands/measure-memory-driver.js +110 -0
- package/lib/commands/measure-memory-driver.js.map +1 -0
- package/lib/commands/measure-memory.d.ts +68 -0
- package/lib/commands/measure-memory.d.ts.map +1 -0
- package/lib/commands/measure-memory.js +100 -0
- package/lib/commands/measure-memory.js.map +1 -0
- package/lib/commands/model-docs-args.d.ts +23 -0
- package/lib/commands/model-docs-args.d.ts.map +1 -0
- package/lib/commands/model-docs-args.js +53 -0
- package/lib/commands/model-docs-args.js.map +1 -0
- package/lib/commands/model-docs-driver.d.ts +10 -0
- package/lib/commands/model-docs-driver.d.ts.map +1 -0
- package/lib/commands/model-docs-driver.js +61 -0
- package/lib/commands/model-docs-driver.js.map +1 -0
- package/lib/commands/model-docs-report.d.ts +18 -0
- package/lib/commands/model-docs-report.d.ts.map +1 -0
- package/lib/commands/model-docs-report.js +123 -0
- package/lib/commands/model-docs-report.js.map +1 -0
- package/lib/commands/model-docs.d.ts +30 -0
- package/lib/commands/model-docs.d.ts.map +1 -0
- package/lib/commands/model-docs.js +37 -0
- package/lib/commands/model-docs.js.map +1 -0
- package/lib/commands/projects-args.d.ts +25 -0
- package/lib/commands/projects-args.d.ts.map +1 -0
- package/lib/commands/projects-args.js +49 -0
- package/lib/commands/projects-args.js.map +1 -0
- package/lib/commands/projects.d.ts +45 -0
- package/lib/commands/projects.d.ts.map +1 -0
- package/lib/commands/projects.js +39 -0
- package/lib/commands/projects.js.map +1 -0
- package/lib/commands/query-args.d.ts +25 -0
- package/lib/commands/query-args.d.ts.map +1 -0
- package/lib/commands/query-args.js +46 -0
- package/lib/commands/query-args.js.map +1 -0
- package/lib/commands/query.d.ts +32 -0
- package/lib/commands/query.d.ts.map +1 -0
- package/lib/commands/query.js +29 -0
- package/lib/commands/query.js.map +1 -0
- package/lib/commands/reflect-args.d.ts +23 -0
- package/lib/commands/reflect-args.d.ts.map +1 -0
- package/lib/commands/reflect-args.js +59 -0
- package/lib/commands/reflect-args.js.map +1 -0
- package/lib/commands/reflect-driver.d.ts +10 -0
- package/lib/commands/reflect-driver.d.ts.map +1 -0
- package/lib/commands/reflect-driver.js +66 -0
- package/lib/commands/reflect-driver.js.map +1 -0
- package/lib/commands/reflect-report.d.ts +20 -0
- package/lib/commands/reflect-report.d.ts.map +1 -0
- package/lib/commands/reflect-report.js +127 -0
- package/lib/commands/reflect-report.js.map +1 -0
- package/lib/commands/reflect.d.ts +30 -0
- package/lib/commands/reflect.d.ts.map +1 -0
- package/lib/commands/reflect.js +38 -0
- package/lib/commands/reflect.js.map +1 -0
- package/lib/commands/save-args.d.ts +26 -0
- package/lib/commands/save-args.d.ts.map +1 -0
- package/lib/commands/save-args.js +59 -0
- package/lib/commands/save-args.js.map +1 -0
- package/lib/commands/save.d.ts +38 -0
- package/lib/commands/save.d.ts.map +1 -0
- package/lib/commands/save.js +48 -0
- package/lib/commands/save.js.map +1 -0
- package/lib/commands/validate-args.d.ts +23 -0
- package/lib/commands/validate-args.d.ts.map +1 -0
- package/lib/commands/validate-args.js +60 -0
- package/lib/commands/validate-args.js.map +1 -0
- package/lib/commands/validate-driver.d.ts +10 -0
- package/lib/commands/validate-driver.d.ts.map +1 -0
- package/lib/commands/validate-driver.js +72 -0
- package/lib/commands/validate-driver.js.map +1 -0
- package/lib/commands/validate-report.d.ts +30 -0
- package/lib/commands/validate-report.d.ts.map +1 -0
- package/lib/commands/validate-report.js +87 -0
- package/lib/commands/validate-report.js.map +1 -0
- package/lib/commands/validate.d.ts +34 -0
- package/lib/commands/validate.d.ts.map +1 -0
- package/lib/commands/validate.js +41 -0
- package/lib/commands/validate.js.map +1 -0
- package/lib/commands/watch-args.d.ts +26 -0
- package/lib/commands/watch-args.d.ts.map +1 -0
- package/lib/commands/watch-args.js +59 -0
- package/lib/commands/watch-args.js.map +1 -0
- package/lib/commands/watch.d.ts +75 -0
- package/lib/commands/watch.d.ts.map +1 -0
- package/lib/commands/watch.js +112 -0
- package/lib/commands/watch.js.map +1 -0
- package/lib/index.d.ts +16 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +16 -0
- package/lib/index.js.map +1 -0
- package/lib/log-level.d.ts +39 -0
- package/lib/log-level.d.ts.map +1 -0
- package/lib/log-level.js +47 -0
- package/lib/log-level.js.map +1 -0
- package/lib/package-name.d.ts +33 -0
- package/lib/package-name.d.ts.map +1 -0
- package/lib/package-name.js +37 -0
- package/lib/package-name.js.map +1 -0
- package/lib/spawn-data-server.d.ts +102 -0
- package/lib/spawn-data-server.d.ts.map +1 -0
- package/lib/spawn-data-server.js +154 -0
- package/lib/spawn-data-server.js.map +1 -0
- package/lib/testing/echo-server.d.ts +11 -0
- package/lib/testing/echo-server.d.ts.map +1 -0
- package/lib/testing/echo-server.js +79 -0
- package/lib/testing/echo-server.js.map +1 -0
- package/package.json +105 -0
- package/src/cli-version.ts +32 -0
- package/src/cli.ts +139 -0
- package/src/commands/analyze-heap-args.ts +184 -0
- package/src/commands/analyze-heap.ts +76 -0
- package/src/commands/ast-ground-truth-args.ts +63 -0
- package/src/commands/ast-ground-truth-driver.ts +81 -0
- package/src/commands/ast-ground-truth.ts +49 -0
- package/src/commands/generate-transfer-model-args.ts +203 -0
- package/src/commands/generate-transfer-model-config.ts +128 -0
- package/src/commands/generate-transfer-model.ts +716 -0
- package/src/commands/harness-args.ts +429 -0
- package/src/commands/headless-harness.ts +165 -0
- package/src/commands/init-args.ts +273 -0
- package/src/commands/init-prompt.ts +149 -0
- package/src/commands/init-templates.ts +2037 -0
- package/src/commands/init-wizard.ts +378 -0
- package/src/commands/init-workspace.ts +442 -0
- package/src/commands/init.ts +554 -0
- package/src/commands/lint-grammar-args.ts +79 -0
- package/src/commands/lint-grammar-driver.ts +83 -0
- package/src/commands/lint-grammar-report.ts +74 -0
- package/src/commands/lint-grammar.ts +57 -0
- package/src/commands/measure-memory-args.ts +123 -0
- package/src/commands/measure-memory-driver.ts +128 -0
- package/src/commands/measure-memory.ts +143 -0
- package/src/commands/model-docs-args.ts +69 -0
- package/src/commands/model-docs-driver.ts +68 -0
- package/src/commands/model-docs-report.ts +133 -0
- package/src/commands/model-docs.ts +48 -0
- package/src/commands/projects-args.ts +62 -0
- package/src/commands/projects.ts +76 -0
- package/src/commands/query-args.ts +61 -0
- package/src/commands/query.ts +58 -0
- package/src/commands/reflect-args.ts +75 -0
- package/src/commands/reflect-driver.ts +76 -0
- package/src/commands/reflect-report.ts +138 -0
- package/src/commands/reflect.ts +51 -0
- package/src/commands/save-args.ts +74 -0
- package/src/commands/save.ts +84 -0
- package/src/commands/validate-args.ts +76 -0
- package/src/commands/validate-driver.ts +84 -0
- package/src/commands/validate-report.ts +95 -0
- package/src/commands/validate.ts +58 -0
- package/src/commands/watch-args.ts +74 -0
- package/src/commands/watch.ts +185 -0
- package/src/index.ts +16 -0
- package/src/log-level.ts +49 -0
- package/src/package-name.ts +39 -0
- package/src/spawn-data-server.ts +247 -0
- package/src/testing/echo-server.ts +93 -0
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 CrossBreeze, EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
|
|
10
|
+
import * as fs from 'node:fs';
|
|
11
|
+
import * as path from 'node:path';
|
|
12
|
+
import { buildInitTemplates, readFrameworkVersion, UNPUBLISHED_FRAMEWORK_VERSION } from './init-templates.js';
|
|
13
|
+
import { createNodeWorkspaceProbe, detectWorkspace, type JsonValue, type WorkspaceProbe } from './init-workspace.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The protocol heads a scaffold can start, in composition order.
|
|
17
|
+
*
|
|
18
|
+
* `lsp` is mandatory rather than optional: it owns the workspace, the build
|
|
19
|
+
* pipeline and the shared service tier every other head reads through, so a
|
|
20
|
+
* project without it has nothing for `data` or `glsp` to serve.
|
|
21
|
+
*/
|
|
22
|
+
export const INIT_HEADS = ['lsp', 'data', 'glsp'] as const;
|
|
23
|
+
|
|
24
|
+
/** One protocol head — see {@link INIT_HEADS}. */
|
|
25
|
+
export type InitHead = (typeof INIT_HEADS)[number];
|
|
26
|
+
|
|
27
|
+
/** The heads a scaffold emits when `--heads` is not given. */
|
|
28
|
+
export const DEFAULT_INIT_HEADS: readonly InitHead[] = ['lsp', 'data'];
|
|
29
|
+
|
|
30
|
+
/** True for a string naming one of {@link INIT_HEADS}. */
|
|
31
|
+
export function isInitHead(value: string): value is InitHead {
|
|
32
|
+
return (INIT_HEADS as readonly string[]).includes(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* One grammar as the command line collected it, before any derivation.
|
|
37
|
+
*
|
|
38
|
+
* Everything but {@link name} is optional: the derivation rules produce a usable
|
|
39
|
+
* routing key and extension from the project and grammar names alone. An
|
|
40
|
+
* override is for the case derivation cannot reach — a file extension that is
|
|
41
|
+
* not the kebab grammar name.
|
|
42
|
+
*/
|
|
43
|
+
export interface InitGrammarSpec {
|
|
44
|
+
/** PascalCase grammar name — the `grammar X` declaration. */
|
|
45
|
+
readonly name: string;
|
|
46
|
+
/**
|
|
47
|
+
* File extensions, leading dot optional. Defaults to the kebab grammar id.
|
|
48
|
+
*
|
|
49
|
+
* A list rather than a single value because `langium-config.json`'s
|
|
50
|
+
* `fileExtensions` is a list: one language may answer for several suffixes.
|
|
51
|
+
*/
|
|
52
|
+
readonly extensions?: readonly string[];
|
|
53
|
+
/** Override the derived routing key. */
|
|
54
|
+
readonly languageId?: string;
|
|
55
|
+
/**
|
|
56
|
+
* Scaffold a GLSP diagram for this grammar. Requires the `glsp` head.
|
|
57
|
+
*
|
|
58
|
+
* Per grammar rather than per project because
|
|
59
|
+
* `AbstractHydraniumGlspDiagramModule.declareLanguage()` returns ONE grammar's
|
|
60
|
+
* `LanguageMetaData` — a diagram type has exactly one grammar, and a grammar
|
|
61
|
+
* with no diagram sitting on the same server is the normal case.
|
|
62
|
+
*/
|
|
63
|
+
readonly diagram?: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The resolved names of ONE grammar — the per-language tier.
|
|
68
|
+
*
|
|
69
|
+
* Everything here gains a sibling per additional grammar, which is what
|
|
70
|
+
* separates it from {@link InitComposition}'s project-level fields. The
|
|
71
|
+
* generated-symbol split is the reason the tiers cannot be collapsed:
|
|
72
|
+
* `langium-cli` emits `<grammar>GeneratedModule` and `<grammar>LanguageMetaData`
|
|
73
|
+
* per grammar, but `<projectName>AstReflection` and
|
|
74
|
+
* `<projectName>GeneratedSharedModule` once per project.
|
|
75
|
+
*/
|
|
76
|
+
export interface InitGrammarNames {
|
|
77
|
+
/** PascalCase grammar name — the `grammar X` declaration. */
|
|
78
|
+
readonly grammar: string;
|
|
79
|
+
/** Kebab grammar id — names the `.langium` file. */
|
|
80
|
+
readonly grammarId: string;
|
|
81
|
+
/**
|
|
82
|
+
* Kebab routing key — the `langium-config.json` entry id.
|
|
83
|
+
*
|
|
84
|
+
* Qualified as `<projectId>-<grammarId>` whenever the grammar is separately
|
|
85
|
+
* named or the project holds more than one, because a second grammar needs a
|
|
86
|
+
* second id and retro-fitting the suffix later renames a routing key every
|
|
87
|
+
* host has already bound.
|
|
88
|
+
*/
|
|
89
|
+
readonly languageId: string;
|
|
90
|
+
/** File extensions without leading dots. At least one. */
|
|
91
|
+
readonly extensions: readonly string[];
|
|
92
|
+
/**
|
|
93
|
+
* The grammar's entry rule, always `<Grammar>Model`.
|
|
94
|
+
*
|
|
95
|
+
* **Derived rather than fixed, because one `langium-cli` run over N grammars
|
|
96
|
+
* emits ONE combined `ast.ts` sharing one reflection.** Two grammars both
|
|
97
|
+
* declaring `entry Model:` would put the same interface name in that module
|
|
98
|
+
* twice.
|
|
99
|
+
*/
|
|
100
|
+
readonly entryRule: string;
|
|
101
|
+
/**
|
|
102
|
+
* The starter node rule, always `<Grammar>Node`.
|
|
103
|
+
*
|
|
104
|
+
* **`Node` and not `Element`:** the transfer-model generator emits
|
|
105
|
+
* `export interface <projectName>Element` as the base type every rule
|
|
106
|
+
* extends, so a rule named `<Grammar>Element` would collide with it in the
|
|
107
|
+
* single-grammar case — the same interface declared twice, the second
|
|
108
|
+
* extending itself.
|
|
109
|
+
*/
|
|
110
|
+
readonly nodeRule: string;
|
|
111
|
+
/** Whether a GLSP diagram is scaffolded for this grammar. */
|
|
112
|
+
readonly diagram: boolean;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Where a scaffold sits inside an existing npm workspace.
|
|
117
|
+
*
|
|
118
|
+
* Detected rather than asked, because both members are things a wrong answer
|
|
119
|
+
* makes actively broken rather than merely unidiomatic: a `--prefix` that names
|
|
120
|
+
* the wrong directory runs the wrong package's scripts, and an `extends` that
|
|
121
|
+
* points at a *solution* tsconfig (`files: []` plus `references`, which is what
|
|
122
|
+
* a monorepo root usually has) inherits no compiler options at all.
|
|
123
|
+
*/
|
|
124
|
+
export interface InitWorkspacePlacement {
|
|
125
|
+
/** POSIX path of the target relative to the workspace root, e.g. `packages/foo`. */
|
|
126
|
+
readonly targetPath: string;
|
|
127
|
+
/** Relative specifier of the tsconfig carrying `compilerOptions`, e.g. `../../tsconfig.base.json`. */
|
|
128
|
+
readonly baseTsconfig?: string;
|
|
129
|
+
/** That tsconfig's `compilerOptions`, so the emitted one can omit what it would inherit unchanged. */
|
|
130
|
+
readonly baseCompilerOptions?: Readonly<Record<string, JsonValue>>;
|
|
131
|
+
/** The workspace's prettier `printWidth`, so emitted sources wrap where the repo wraps. */
|
|
132
|
+
readonly printWidth?: number;
|
|
133
|
+
/** Filename of the workspace's eslint config; its presence is what makes the scaffold emit a `lint` script. */
|
|
134
|
+
readonly eslintConfig?: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* How the emitted `package.json` presents itself.
|
|
139
|
+
*
|
|
140
|
+
* Separate from {@link InitWorkspacePlacement} because the two answer to
|
|
141
|
+
* different authorities: the placement is *detected* from the surrounding tree,
|
|
142
|
+
* while the scope and the publishability are **intent** and are never inferred —
|
|
143
|
+
* a repo's root manifest is routinely named for the repo rather than for the
|
|
144
|
+
* scope it publishes under, so guessing would confidently produce the wrong
|
|
145
|
+
* package name. The wizard offers a sibling-derived default; the flags decide.
|
|
146
|
+
*
|
|
147
|
+
* This is the one place the two polarities meet: every option surface upstream
|
|
148
|
+
* carries `public`, because a flag names a DEVIATION from the default, while
|
|
149
|
+
* this type carries `private`, because that is the manifest key the template
|
|
150
|
+
* substitutes.
|
|
151
|
+
*/
|
|
152
|
+
export interface InitPackaging {
|
|
153
|
+
/** npm scope for the package name, e.g. `@acme`. Absent leaves it unscoped. */
|
|
154
|
+
readonly scope?: string;
|
|
155
|
+
/** Emit `"private": true` — a package npm refuses to publish. */
|
|
156
|
+
readonly private: boolean;
|
|
157
|
+
/** The workspace the package joins. Absent for a standalone project. */
|
|
158
|
+
readonly workspace?: InitWorkspacePlacement;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* A standalone, unscoped, unpublishable package — what `init` emits without the
|
|
163
|
+
* packaging flags.
|
|
164
|
+
*
|
|
165
|
+
* `private` defaults to TRUE because the manifest also declares `license:
|
|
166
|
+
* "UNLICENSED"`, and a package that grants no rights while being publishable to
|
|
167
|
+
* a public registry asserts two contradictory things about itself. npm pairs
|
|
168
|
+
* those two keys for exactly this reason. The scaffold cannot pick a licence for
|
|
169
|
+
* a stranger's project, so it withholds publication instead of granting rights
|
|
170
|
+
* nobody chose; `--public` is the opt-out for a project that has picked one.
|
|
171
|
+
*/
|
|
172
|
+
export const STANDALONE_PACKAGING: InitPackaging = { private: true };
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The full scaffold description: one project tier, N grammar tiers. Templates
|
|
176
|
+
* read this and are rendered per tier.
|
|
177
|
+
*/
|
|
178
|
+
export interface InitComposition {
|
|
179
|
+
/** PascalCase project name — the Langium `projectName`. */
|
|
180
|
+
readonly name: string;
|
|
181
|
+
/**
|
|
182
|
+
* Kebab project id, always derived from {@link name} and never from a
|
|
183
|
+
* language id. Names what a second grammar must not rename: the package and
|
|
184
|
+
* `bin` key, the DI module file, and the data-server port command (there is
|
|
185
|
+
* one data server per project, not one per language).
|
|
186
|
+
*/
|
|
187
|
+
readonly projectId: string;
|
|
188
|
+
/** The protocol heads to start, always including `lsp`. */
|
|
189
|
+
readonly heads: readonly InitHead[];
|
|
190
|
+
/** The grammars, in the order given. The first is the configuration root. */
|
|
191
|
+
readonly grammars: readonly InitGrammarNames[];
|
|
192
|
+
/** How the emitted `package.json` presents itself, and where it sits. */
|
|
193
|
+
readonly packaging: InitPackaging;
|
|
194
|
+
/**
|
|
195
|
+
* The framework version every emitted `@hydranium/*` pin is derived from —
|
|
196
|
+
* the scaffolding CLI's own, since the scope is versioned as one line.
|
|
197
|
+
*
|
|
198
|
+
* Carried on the composition rather than read where the templates render, so
|
|
199
|
+
* {@link planInitFiles} stays pure over its input. That is what lets a golden
|
|
200
|
+
* pin the emitted shape at a fixed token without pinning the number, which
|
|
201
|
+
* would otherwise turn every release into a red suite.
|
|
202
|
+
*/
|
|
203
|
+
readonly frameworkVersion: string;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** One file the scaffold writes: relative path + fully-substituted content. */
|
|
207
|
+
export interface InitFile {
|
|
208
|
+
path: string;
|
|
209
|
+
content: string;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Options for the {@link runInit} subcommand. */
|
|
213
|
+
export interface InitCommandOptions {
|
|
214
|
+
/** Target directory to scaffold into. */
|
|
215
|
+
readonly targetDir: string;
|
|
216
|
+
/** PascalCase project name (drives the services + shared generated module names). */
|
|
217
|
+
readonly name: string;
|
|
218
|
+
/**
|
|
219
|
+
* The grammars to scaffold, in order. Defaults to a single grammar named
|
|
220
|
+
* after the project, which is the single-language starter shape.
|
|
221
|
+
*/
|
|
222
|
+
readonly grammars?: readonly InitGrammarSpec[];
|
|
223
|
+
/** The protocol heads to start. Defaults to {@link DEFAULT_INIT_HEADS}. */
|
|
224
|
+
readonly heads?: readonly InitHead[];
|
|
225
|
+
/** Scaffold into a non-empty directory anyway. */
|
|
226
|
+
readonly force?: boolean;
|
|
227
|
+
/**
|
|
228
|
+
* Scaffold a member of the surrounding npm workspace: extend its base
|
|
229
|
+
* tsconfig, leave `.gitignore` to the root, and address the package by
|
|
230
|
+
* `--prefix` in the scripts it prints back at itself. Errors when no ancestor
|
|
231
|
+
* declares `workspaces`, rather than silently degrading to a standalone
|
|
232
|
+
* project the caller did not ask for.
|
|
233
|
+
*/
|
|
234
|
+
readonly monorepo?: boolean;
|
|
235
|
+
/** npm scope for the package name, e.g. `@acme`. */
|
|
236
|
+
readonly scope?: string;
|
|
237
|
+
/** Omit `"private": true`, leaving the package publishable. */
|
|
238
|
+
readonly public?: boolean;
|
|
239
|
+
/** Output sink. Default: `process.stdout.write`. Tests inject a capturing stub. */
|
|
240
|
+
readonly write?: (line: string) => void;
|
|
241
|
+
/** Filesystem port for workspace detection. Default: the real filesystem. */
|
|
242
|
+
readonly probe?: WorkspaceProbe;
|
|
243
|
+
/** Test-only: capture the planned files instead of writing to disk. */
|
|
244
|
+
readonly __writeFilesForTest?: (targetDir: string, files: readonly InitFile[]) => void;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Kebab-case a PascalCase/camelCase name. */
|
|
248
|
+
export function kebab(name: string): string {
|
|
249
|
+
return name.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** True for a PascalCase identifier legal as a generated TypeScript symbol prefix. */
|
|
253
|
+
function isPascalIdentifier(value: string): boolean {
|
|
254
|
+
return /^[A-Za-z][A-Za-z0-9]*$/.test(value);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Strip a leading dot and reject an extension that is empty or dotted through. */
|
|
258
|
+
function normalizeExtension(value: string, grammar: string): string {
|
|
259
|
+
const extension = value.replace(/^\./, '');
|
|
260
|
+
if (extension.length === 0) {
|
|
261
|
+
throw new Error(`Invalid extension '${value}' for grammar '${grammar}': an extension cannot be empty.`);
|
|
262
|
+
}
|
|
263
|
+
return extension;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Resolve one grammar's names.
|
|
268
|
+
*
|
|
269
|
+
* `qualify` comes from the project rather than the grammar: a lone grammar
|
|
270
|
+
* whose name matches the project keeps the bare project id, but as soon as
|
|
271
|
+
* there are several every id is qualified, so the set reads consistently
|
|
272
|
+
* instead of one member being special.
|
|
273
|
+
*/
|
|
274
|
+
function resolveGrammar(spec: InitGrammarSpec, projectId: string, qualify: boolean, diagram: boolean): InitGrammarNames {
|
|
275
|
+
if (!isPascalIdentifier(spec.name)) {
|
|
276
|
+
throw new Error(
|
|
277
|
+
`Invalid --grammar '${spec.name}': must be a PascalCase identifier (letters and digits, starting with a letter), e.g. 'Domain'.`
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
const grammarId = kebab(spec.name);
|
|
281
|
+
const extensions = (spec.extensions ?? [grammarId]).map(extension => normalizeExtension(extension, spec.name));
|
|
282
|
+
if (extensions.length === 0) {
|
|
283
|
+
throw new Error(`Invalid --extensions for grammar '${spec.name}': at least one extension is required.`);
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
grammar: spec.name,
|
|
287
|
+
grammarId,
|
|
288
|
+
languageId: spec.languageId ?? (qualify || grammarId !== projectId ? `${projectId}-${grammarId}` : projectId),
|
|
289
|
+
extensions,
|
|
290
|
+
entryRule: `${spec.name}Model`,
|
|
291
|
+
nodeRule: `${spec.name}Node`,
|
|
292
|
+
diagram
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Resolve which grammars get a diagram.
|
|
298
|
+
*
|
|
299
|
+
* With the `glsp` head on and nothing marked, a lone grammar is unambiguous and
|
|
300
|
+
* is taken as the diagram's; with several it is a real choice, so it is required
|
|
301
|
+
* rather than guessed — binding a diagram to the wrong grammar surfaces much
|
|
302
|
+
* later as references resolving against the wrong scope.
|
|
303
|
+
*/
|
|
304
|
+
function resolveDiagrams(specs: readonly InitGrammarSpec[], heads: readonly InitHead[]): boolean[] {
|
|
305
|
+
const marked = specs.map(spec => spec.diagram === true);
|
|
306
|
+
if (!heads.includes('glsp')) {
|
|
307
|
+
const named = specs.filter(spec => spec.diagram === true).map(spec => spec.name);
|
|
308
|
+
if (named.length > 0) {
|
|
309
|
+
throw new Error(
|
|
310
|
+
`--diagram was given for ${named.join(', ')} but the head set is '${heads.join(',')}'. ` +
|
|
311
|
+
"Add 'glsp' to --heads, since the emitted dependencies follow the head set."
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
return marked;
|
|
315
|
+
}
|
|
316
|
+
if (marked.some(Boolean)) {
|
|
317
|
+
return marked;
|
|
318
|
+
}
|
|
319
|
+
if (specs.length === 1) {
|
|
320
|
+
return [true];
|
|
321
|
+
}
|
|
322
|
+
throw new Error(
|
|
323
|
+
`--heads includes 'glsp' but no grammar carries --diagram. Mark the one the diagram edits: ` +
|
|
324
|
+
`${specs.map(spec => spec.name).join(', ')}.`
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Validate the head set: known names only, and `lsp` present. */
|
|
329
|
+
function resolveHeads(heads: readonly InitHead[] | undefined): readonly InitHead[] {
|
|
330
|
+
const resolved = heads === undefined || heads.length === 0 ? DEFAULT_INIT_HEADS : heads;
|
|
331
|
+
const unknown = resolved.filter(head => !isInitHead(head));
|
|
332
|
+
if (unknown.length > 0) {
|
|
333
|
+
throw new Error(`Unknown head(s) '${unknown.join(', ')}': expected one of ${INIT_HEADS.join(', ')}.`);
|
|
334
|
+
}
|
|
335
|
+
if (!resolved.includes('lsp')) {
|
|
336
|
+
throw new Error(
|
|
337
|
+
"--heads must include 'lsp': it owns the workspace, the build pipeline and the shared service tier the other heads read through."
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
// De-duplicated and put back in composition order, so the order the heads
|
|
341
|
+
// were named in does not change the emitted project.
|
|
342
|
+
return INIT_HEADS.filter(head => resolved.includes(head));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Reject the collisions that would otherwise surface as a confusing generator
|
|
347
|
+
* or runtime failure rather than as a bad invocation.
|
|
348
|
+
*
|
|
349
|
+
* The extension check is the one worth having: Langium routes documents to a
|
|
350
|
+
* language BY extension, so two grammars claiming `.foo` is not a name clash
|
|
351
|
+
* but a silent mis-route of every `.foo` file to whichever language registered
|
|
352
|
+
* first.
|
|
353
|
+
*/
|
|
354
|
+
function assertNoCollisions(grammars: readonly InitGrammarNames[]): void {
|
|
355
|
+
const byGrammarId = new Map<string, string>();
|
|
356
|
+
const byLanguageId = new Map<string, string>();
|
|
357
|
+
const byExtension = new Map<string, string>();
|
|
358
|
+
for (const grammar of grammars) {
|
|
359
|
+
const clashingName = byGrammarId.get(grammar.grammarId);
|
|
360
|
+
if (clashingName !== undefined) {
|
|
361
|
+
throw new Error(`Duplicate grammar '${grammar.grammar}': it resolves to the same file as '${clashingName}'.`);
|
|
362
|
+
}
|
|
363
|
+
byGrammarId.set(grammar.grammarId, grammar.grammar);
|
|
364
|
+
|
|
365
|
+
const clashingLanguage = byLanguageId.get(grammar.languageId);
|
|
366
|
+
if (clashingLanguage !== undefined) {
|
|
367
|
+
throw new Error(`Duplicate language id '${grammar.languageId}': claimed by both '${clashingLanguage}' and '${grammar.grammar}'.`);
|
|
368
|
+
}
|
|
369
|
+
byLanguageId.set(grammar.languageId, grammar.grammar);
|
|
370
|
+
|
|
371
|
+
for (const extension of grammar.extensions) {
|
|
372
|
+
const clashingExtension = byExtension.get(extension);
|
|
373
|
+
if (clashingExtension !== undefined) {
|
|
374
|
+
throw new Error(
|
|
375
|
+
`File extension '.${extension}' is claimed by both '${clashingExtension}' and '${grammar.grammar}'. ` +
|
|
376
|
+
'Langium routes documents by extension, so each grammar needs its own.'
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
byExtension.set(extension, grammar.grammar);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Derive the whole scaffold description from `--name` plus the collected
|
|
386
|
+
* grammars. Throws on a name that is not a valid PascalCase identifier — both
|
|
387
|
+
* project and grammar names become prefixes of generated TypeScript symbols.
|
|
388
|
+
*
|
|
389
|
+
* With no grammars given, one is synthesised from the project name, which is
|
|
390
|
+
* the single-language starter shape.
|
|
391
|
+
*/
|
|
392
|
+
export function resolveInitComposition(
|
|
393
|
+
name: string,
|
|
394
|
+
grammars: readonly InitGrammarSpec[] = [],
|
|
395
|
+
heads?: readonly InitHead[],
|
|
396
|
+
packaging: InitPackaging = STANDALONE_PACKAGING
|
|
397
|
+
): InitComposition {
|
|
398
|
+
if (!isPascalIdentifier(name)) {
|
|
399
|
+
throw new Error(
|
|
400
|
+
`Invalid --name '${name}': must be a PascalCase identifier (letters and digits, starting with a letter), e.g. 'Bookstore'.`
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
const projectId = kebab(name);
|
|
404
|
+
if (packaging.scope !== undefined && !/^@[a-z0-9][a-z0-9._-]*$/.test(packaging.scope)) {
|
|
405
|
+
throw new Error(`Invalid --scope '${packaging.scope}': an npm scope starts with '@', e.g. '@acme'.`);
|
|
406
|
+
}
|
|
407
|
+
const resolvedHeads = resolveHeads(heads);
|
|
408
|
+
const specs = grammars.length > 0 ? grammars : [{ name }];
|
|
409
|
+
const diagrams = resolveDiagrams(specs, resolvedHeads);
|
|
410
|
+
const resolved = specs.map((spec, index) => resolveGrammar(spec, projectId, specs.length > 1, diagrams[index]));
|
|
411
|
+
assertNoCollisions(resolved);
|
|
412
|
+
return { name, projectId, heads: resolvedHeads, grammars: resolved, packaging, frameworkVersion: readFrameworkVersion() };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Plan the files a scaffold emits — every template rendered for this
|
|
417
|
+
* composition. Pure over its input → unit-testable without touching disk;
|
|
418
|
+
* {@link runInit} is the thin IO wrapper.
|
|
419
|
+
*/
|
|
420
|
+
export function planInitFiles(composition: InitComposition): InitFile[] {
|
|
421
|
+
return buildInitTemplates(composition);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/** True when `dir` exists and contains at least one entry. */
|
|
425
|
+
export function isNonEmptyDir(dir: string): boolean {
|
|
426
|
+
return fs.existsSync(dir) && fs.statSync(dir).isDirectory() && fs.readdirSync(dir).length > 0;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** The packaging flags, plus the workspace they were given in. */
|
|
430
|
+
export interface InitPackagingOptions {
|
|
431
|
+
readonly monorepo?: boolean;
|
|
432
|
+
readonly scope?: string;
|
|
433
|
+
readonly public?: boolean;
|
|
434
|
+
readonly probe?: WorkspaceProbe;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Resolve the packaging tier for a target directory, running workspace
|
|
439
|
+
* detection when `--monorepo` asked for it.
|
|
440
|
+
*
|
|
441
|
+
* Exported because the `init`-provenance gate re-runs the reference example's
|
|
442
|
+
* recorded invocation and must reach the same composition `runInit` would: a
|
|
443
|
+
* second copy of these rules there would drift from these, and the gate exists
|
|
444
|
+
* precisely to catch drift.
|
|
445
|
+
*/
|
|
446
|
+
export function resolveInitPackaging(targetDir: string, options: InitPackagingOptions): InitPackaging {
|
|
447
|
+
if (options.monorepo !== true) {
|
|
448
|
+
return { scope: options.scope, private: options.public !== true };
|
|
449
|
+
}
|
|
450
|
+
const detection = detectWorkspace(targetDir, options.probe ?? createNodeWorkspaceProbe());
|
|
451
|
+
if (detection === undefined) {
|
|
452
|
+
throw new Error(
|
|
453
|
+
`--monorepo was given but no ancestor of '${path.resolve(targetDir)}' has a package.json declaring 'workspaces'. ` +
|
|
454
|
+
'Scaffold without it for a standalone project.'
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
return {
|
|
458
|
+
scope: options.scope,
|
|
459
|
+
private: options.public !== true,
|
|
460
|
+
// The scope and the publishability deliberately do NOT fall back to what
|
|
461
|
+
// detection found: the emitted package name stays a function of the argv
|
|
462
|
+
// alone, which is what lets the wizard's echoed command reproduce the
|
|
463
|
+
// scaffold anywhere. Detection only supplies the wizard's DEFAULT answer.
|
|
464
|
+
workspace: {
|
|
465
|
+
targetPath: detection.targetPath,
|
|
466
|
+
baseTsconfig: detection.baseTsconfig,
|
|
467
|
+
baseCompilerOptions: detection.baseCompilerOptions,
|
|
468
|
+
printWidth: detection.printWidth,
|
|
469
|
+
eslintConfig: detection.eslintConfig
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Scaffold a new Hydranium language project end-to-end: one starter grammar per
|
|
476
|
+
* `--grammar`, `create<Name>Services` DI wiring, a launcher for each head in
|
|
477
|
+
* `--heads`, `langium-config.json`, `package.json`, and scripts. Does NOT run `npm install`
|
|
478
|
+
* or `langium generate` — those are printed as next steps, so the command stays
|
|
479
|
+
* offline and side-effect-free beyond the files it writes. Refuses a non-empty
|
|
480
|
+
* target directory unless `force`.
|
|
481
|
+
*/
|
|
482
|
+
export function runInit(options: InitCommandOptions): void {
|
|
483
|
+
const write = options.write ?? ((line: string) => process.stdout.write(line));
|
|
484
|
+
const targetDir = path.resolve(options.targetDir);
|
|
485
|
+
const detection = options.monorepo ? detectWorkspace(targetDir, options.probe ?? createNodeWorkspaceProbe()) : undefined;
|
|
486
|
+
const composition = resolveInitComposition(options.name, options.grammars, options.heads, resolveInitPackaging(targetDir, options));
|
|
487
|
+
|
|
488
|
+
if (!options.force && isNonEmptyDir(targetDir)) {
|
|
489
|
+
throw new Error(`Target directory '${targetDir}' is not empty. Pass --force to scaffold into it anyway.`);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const files = planInitFiles(composition);
|
|
493
|
+
if (options.__writeFilesForTest) {
|
|
494
|
+
options.__writeFilesForTest(targetDir, files);
|
|
495
|
+
} else {
|
|
496
|
+
for (const file of files) {
|
|
497
|
+
const absolute = path.join(targetDir, file.path);
|
|
498
|
+
fs.mkdirSync(path.dirname(absolute), { recursive: true });
|
|
499
|
+
fs.writeFileSync(absolute, file.content, 'utf-8');
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const grammarList = composition.grammars.map(grammar => (grammar.diagram ? `${grammar.grammar} (diagram)` : grammar.grammar)).join(', ');
|
|
504
|
+
write(
|
|
505
|
+
`Scaffolded ${composition.name} (${files.length} files, heads: ${composition.heads.join(',')}, ` +
|
|
506
|
+
`${composition.grammars.length} grammar(s): ${grammarList}) into ${targetDir}\n`
|
|
507
|
+
);
|
|
508
|
+
if (detection !== undefined) {
|
|
509
|
+
write('\n');
|
|
510
|
+
write(`Workspace root: ${detection.rootDir}\n`);
|
|
511
|
+
if (detection.baseTsconfig !== undefined) {
|
|
512
|
+
write(` tsconfig.json extends ${detection.baseTsconfig}, minus the options it inherits unchanged.\n`);
|
|
513
|
+
} else {
|
|
514
|
+
write(' No root tsconfig carries compilerOptions, so the emitted one stands alone.\n');
|
|
515
|
+
}
|
|
516
|
+
write(" .gitignore holds `syntaxes/` only — the workspace root's covers the rest.\n");
|
|
517
|
+
if (detection.printWidth !== undefined) {
|
|
518
|
+
write(` Sources wrapped at ${detection.printWidth} columns, from ${detection.rootDir}'s prettier config.\n`);
|
|
519
|
+
}
|
|
520
|
+
if (detection.eslintConfig !== undefined) {
|
|
521
|
+
write(` A lint script was added — ${detection.eslintConfig} says this repo lints. Adjust it if your invocation differs.\n`);
|
|
522
|
+
}
|
|
523
|
+
// Printed rather than applied: `init` writes inside the target directory
|
|
524
|
+
// and nowhere else, so a scaffold can never surprise-edit a manifest that
|
|
525
|
+
// is under review, mid-rebase, or simply not the caller's to change.
|
|
526
|
+
if (detection.coveredBy === undefined) {
|
|
527
|
+
write(` Add this to "workspaces" in ${path.join(detection.rootDir, 'package.json')}:\n`);
|
|
528
|
+
write(` "${detection.targetPath}"\n`);
|
|
529
|
+
} else {
|
|
530
|
+
write(` Already covered by the "${detection.coveredBy}" workspaces entry — no root manifest change needed.\n`);
|
|
531
|
+
}
|
|
532
|
+
if (composition.packaging.scope === undefined && detection.scope !== undefined) {
|
|
533
|
+
write(` Sibling packages all use the "${detection.scope}" scope; pass --scope ${detection.scope} to match them.\n`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
write('\n');
|
|
537
|
+
write('Next steps:\n');
|
|
538
|
+
write(` cd ${options.targetDir}\n`);
|
|
539
|
+
// The warning is conditional on the pins this scaffold actually carries — see
|
|
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
|
+
);
|
|
546
|
+
write(' npm run build # langium generate + tsc\n');
|
|
547
|
+
write(' npm test # the scaffolded DI-composition test\n');
|
|
548
|
+
// `npx`, not a bare invocation: `@hydranium/cli` is a devDependency of the
|
|
549
|
+
// scaffold, so the binary is on PATH inside an npm script and nowhere else.
|
|
550
|
+
write(' npx hydranium-cli reflect --services ./lib/services.js\n');
|
|
551
|
+
write('\n');
|
|
552
|
+
write('The emitted sources carry no license header — run your own header tool if\n');
|
|
553
|
+
write('your repo gates them. See the README section on that.\n');
|
|
554
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 CrossBreeze, EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
exitWithUsage,
|
|
12
|
+
helpRequested,
|
|
13
|
+
LOG_LEVEL_FLAG,
|
|
14
|
+
logLevelHelpLine,
|
|
15
|
+
logLevelOption,
|
|
16
|
+
parseHarnessArgs,
|
|
17
|
+
printHelp,
|
|
18
|
+
SERVICES_FLAG,
|
|
19
|
+
type UsageError
|
|
20
|
+
} from './harness-args.js';
|
|
21
|
+
import { runLintGrammar, type LintGrammarCommandOptions } from './lint-grammar.js';
|
|
22
|
+
|
|
23
|
+
const VALUE_FLAGS = [LOG_LEVEL_FLAG] as const;
|
|
24
|
+
const BOOL_FLAGS = ['--strict', '--json'] as const;
|
|
25
|
+
const REPEATABLE_VALUE_FLAGS = ['--name-property'] as const;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Every flag `lint-grammar` accepts, derived from the sets the parser is handed
|
|
29
|
+
* so the list cannot claim a flag the parser would reject. `--services` is
|
|
30
|
+
* consumed by the shared harness parser itself, so it is in neither set.
|
|
31
|
+
*/
|
|
32
|
+
export const LINT_GRAMMAR_FLAGS: readonly string[] = [SERVICES_FLAG, ...REPEATABLE_VALUE_FLAGS, ...VALUE_FLAGS, ...BOOL_FLAGS];
|
|
33
|
+
|
|
34
|
+
/** The subset that takes a value, so `--help` in a value position reads as data. */
|
|
35
|
+
export const LINT_GRAMMAR_VALUE_FLAGS: readonly string[] = [SERVICES_FLAG, ...REPEATABLE_VALUE_FLAGS, ...VALUE_FLAGS];
|
|
36
|
+
|
|
37
|
+
/** The `--help` text, as data, held to {@link LINT_GRAMMAR_FLAGS} by a test. */
|
|
38
|
+
export const LINT_GRAMMAR_HELP: readonly string[] = [
|
|
39
|
+
'Usage: hydranium-cli lint-grammar --services <module> [--name-property <p>]... [--strict] [--json] [--log-level <lvl>]',
|
|
40
|
+
'',
|
|
41
|
+
"Check a head's grammar against the framework's conventions — a CI gate: the",
|
|
42
|
+
'process exits non-zero when a violation is found. Read-only: no workspace is',
|
|
43
|
+
'built (the grammar is static once the head registers its language), so',
|
|
44
|
+
'`lint-grammar` takes no `<workspace>`. Checks that every concrete cross-reference',
|
|
45
|
+
'target carries a name property (else references cannot resolve) and that each',
|
|
46
|
+
'language declares an entry rule. `<module>` is an ESM module exporting a',
|
|
47
|
+
'zero-arg `createServices(): { shared }` thunk. Needs @hydranium/core.',
|
|
48
|
+
'',
|
|
49
|
+
'Options:',
|
|
50
|
+
' --services <module> ESM module exporting `createServices(): { shared }` (required).',
|
|
51
|
+
' --name-property <p> Property that satisfies the nameability convention',
|
|
52
|
+
' (repeatable). Default: name.',
|
|
53
|
+
' --strict Also fail (non-zero exit) on warnings, not only errors.',
|
|
54
|
+
' --json Emit the raw JSON result instead of the human report.',
|
|
55
|
+
logLevelHelpLine(23)
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
export function parseLintGrammarArgs(args: string[], onError: UsageError = exitWithUsage): LintGrammarCommandOptions {
|
|
59
|
+
const { servicesModule, options, values } = parseHarnessArgs(args, 'lint-grammar', VALUE_FLAGS, BOOL_FLAGS, {
|
|
60
|
+
requireWorkspace: false,
|
|
61
|
+
repeatableValueFlags: REPEATABLE_VALUE_FLAGS,
|
|
62
|
+
onError
|
|
63
|
+
});
|
|
64
|
+
return {
|
|
65
|
+
servicesModule,
|
|
66
|
+
nameProperties: values['--name-property'],
|
|
67
|
+
strict: options['--strict'] === 'true',
|
|
68
|
+
json: options['--json'] === 'true',
|
|
69
|
+
logLevel: logLevelOption(options[LOG_LEVEL_FLAG])
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function runLintGrammarCommand(args: string[]): Promise<void> {
|
|
74
|
+
if (helpRequested(args, LINT_GRAMMAR_VALUE_FLAGS)) {
|
|
75
|
+
printHelp(LINT_GRAMMAR_HELP);
|
|
76
|
+
return Promise.resolve();
|
|
77
|
+
}
|
|
78
|
+
return runLintGrammar(parseLintGrammarArgs(args));
|
|
79
|
+
}
|