@hydranium/cli 1.0.0-next.31 → 1.0.0-next.312
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 +79 -205
- package/bin/hydranium-cli.js +13 -0
- package/heap-analysis/heap/core/load.mjs +2 -2
- package/lib/cli-version.js.map +1 -1
- package/lib/cli.js +7 -2
- package/lib/cli.js.map +1 -1
- 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.d.ts +3 -2
- package/lib/commands/ast-ground-truth-args.d.ts.map +1 -1
- package/lib/commands/ast-ground-truth-args.js +11 -9
- package/lib/commands/ast-ground-truth-args.js.map +1 -1
- package/lib/commands/ast-ground-truth-driver.js +2 -1
- package/lib/commands/ast-ground-truth-driver.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/client-session.d.ts +17 -0
- package/lib/commands/client-session.d.ts.map +1 -0
- package/lib/commands/client-session.js +27 -0
- package/lib/commands/client-session.js.map +1 -0
- package/lib/commands/create-args.d.ts +17 -0
- package/lib/commands/create-args.d.ts.map +1 -0
- package/lib/commands/create-args.js +53 -0
- package/lib/commands/create-args.js.map +1 -0
- package/lib/commands/create.d.ts +38 -0
- package/lib/commands/create.d.ts.map +1 -0
- package/lib/commands/create.js +43 -0
- package/lib/commands/create.js.map +1 -0
- package/lib/commands/delete-args.d.ts +17 -0
- package/lib/commands/delete-args.d.ts.map +1 -0
- package/lib/commands/delete-args.js +49 -0
- package/lib/commands/delete-args.js.map +1 -0
- package/lib/commands/delete.d.ts +30 -0
- package/lib/commands/delete.d.ts.map +1 -0
- package/lib/commands/delete.js +32 -0
- package/lib/commands/delete.js.map +1 -0
- package/lib/commands/generate-languages.d.ts +35 -0
- package/lib/commands/generate-languages.d.ts.map +1 -0
- package/lib/commands/generate-languages.js +105 -0
- package/lib/commands/generate-languages.js.map +1 -0
- package/lib/commands/generate-transfer-model-args.d.ts.map +1 -1
- package/lib/commands/generate-transfer-model-args.js +7 -0
- 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 +6 -3
- package/lib/commands/generate-transfer-model-config.js.map +1 -1
- package/lib/commands/generate-transfer-model.d.ts +15 -1
- package/lib/commands/generate-transfer-model.d.ts.map +1 -1
- package/lib/commands/generate-transfer-model.js +46 -8
- package/lib/commands/generate-transfer-model.js.map +1 -1
- package/lib/commands/harness-args.d.ts +44 -42
- package/lib/commands/harness-args.d.ts.map +1 -1
- package/lib/commands/harness-args.js +109 -27
- package/lib/commands/harness-args.js.map +1 -1
- package/lib/commands/headless-harness.d.ts +37 -20
- package/lib/commands/headless-harness.d.ts.map +1 -1
- package/lib/commands/headless-harness.js +68 -17
- package/lib/commands/headless-harness.js.map +1 -1
- package/lib/commands/init-args.d.ts.map +1 -1
- package/lib/commands/init-args.js +32 -27
- package/lib/commands/init-args.js.map +1 -1
- package/lib/commands/init-prompt.d.ts +3 -3
- package/lib/commands/init-prompt.d.ts.map +1 -1
- package/lib/commands/init-prompt.js +4 -5
- package/lib/commands/init-prompt.js.map +1 -1
- package/lib/commands/init-templates.d.ts +18 -14
- package/lib/commands/init-templates.d.ts.map +1 -1
- package/lib/commands/init-templates.js +732 -677
- package/lib/commands/init-templates.js.map +1 -1
- package/lib/commands/init-wizard.d.ts.map +1 -1
- package/lib/commands/init-wizard.js +17 -27
- package/lib/commands/init-wizard.js.map +1 -1
- package/lib/commands/init-workspace.d.ts +13 -11
- package/lib/commands/init-workspace.d.ts.map +1 -1
- package/lib/commands/init-workspace.js +21 -7
- package/lib/commands/init-workspace.js.map +1 -1
- package/lib/commands/init.d.ts +45 -19
- package/lib/commands/init.d.ts.map +1 -1
- package/lib/commands/init.js +80 -43
- package/lib/commands/init.js.map +1 -1
- package/lib/commands/kebab.d.ts +15 -0
- package/lib/commands/kebab.d.ts.map +1 -0
- package/lib/commands/kebab.js +17 -0
- package/lib/commands/kebab.js.map +1 -0
- package/lib/commands/lint-grammar-args.d.ts +3 -2
- package/lib/commands/lint-grammar-args.d.ts.map +1 -1
- package/lib/commands/lint-grammar-args.js +13 -10
- package/lib/commands/lint-grammar-args.js.map +1 -1
- package/lib/commands/lint-grammar-driver.js +2 -1
- package/lib/commands/lint-grammar-driver.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.d.ts +3 -2
- package/lib/commands/measure-memory-args.d.ts.map +1 -1
- package/lib/commands/measure-memory-args.js +22 -11
- package/lib/commands/measure-memory-args.js.map +1 -1
- package/lib/commands/measure-memory-driver.js +5 -3
- 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.d.ts +4 -3
- package/lib/commands/model-docs-args.d.ts.map +1 -1
- package/lib/commands/model-docs-args.js +12 -10
- package/lib/commands/model-docs-args.js.map +1 -1
- package/lib/commands/model-docs-driver.js +2 -1
- package/lib/commands/model-docs-driver.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.d.ts.map +1 -1
- package/lib/commands/projects-args.js +5 -5
- package/lib/commands/projects-args.js.map +1 -1
- package/lib/commands/projects.d.ts +3 -4
- package/lib/commands/projects.d.ts.map +1 -1
- package/lib/commands/projects.js +2 -2
- package/lib/commands/projects.js.map +1 -1
- package/lib/commands/query-args.d.ts.map +1 -1
- package/lib/commands/query-args.js +5 -5
- package/lib/commands/query-args.js.map +1 -1
- package/lib/commands/query.d.ts +3 -4
- package/lib/commands/query.d.ts.map +1 -1
- package/lib/commands/query.js +2 -2
- package/lib/commands/query.js.map +1 -1
- package/lib/commands/reflect-args.d.ts +3 -2
- package/lib/commands/reflect-args.d.ts.map +1 -1
- package/lib/commands/reflect-args.js +13 -10
- package/lib/commands/reflect-args.js.map +1 -1
- package/lib/commands/reflect-driver.js +2 -1
- package/lib/commands/reflect-driver.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.d.ts.map +1 -1
- package/lib/commands/save-args.js +7 -4
- package/lib/commands/save-args.js.map +1 -1
- package/lib/commands/save.d.ts +8 -4
- package/lib/commands/save.d.ts.map +1 -1
- package/lib/commands/save.js +12 -23
- package/lib/commands/save.js.map +1 -1
- package/lib/commands/validate-args.d.ts +3 -2
- package/lib/commands/validate-args.d.ts.map +1 -1
- package/lib/commands/validate-args.js +13 -10
- package/lib/commands/validate-args.js.map +1 -1
- package/lib/commands/validate-driver.js +2 -1
- package/lib/commands/validate-driver.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.d.ts.map +1 -1
- package/lib/commands/watch-args.js +6 -4
- package/lib/commands/watch-args.js.map +1 -1
- package/lib/commands/watch.d.ts +3 -4
- package/lib/commands/watch.d.ts.map +1 -1
- package/lib/commands/watch.js +20 -2
- 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 +2 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -0
- package/lib/index.js.map +1 -1
- package/lib/log-level.d.ts +21 -11
- package/lib/log-level.d.ts.map +1 -1
- package/lib/log-level.js +25 -14
- package/lib/log-level.js.map +1 -1
- package/lib/spawn-data-server.d.ts +9 -4
- package/lib/spawn-data-server.d.ts.map +1 -1
- package/lib/spawn-data-server.js +5 -4
- package/lib/spawn-data-server.js.map +1 -1
- package/lib/stderr-connection-logger.d.ts +12 -0
- package/lib/stderr-connection-logger.d.ts.map +1 -0
- package/lib/stderr-connection-logger.js +16 -0
- package/lib/stderr-connection-logger.js.map +1 -0
- package/lib/testing/echo-server.js +9 -1
- package/lib/testing/echo-server.js.map +1 -1
- package/package.json +52 -39
- package/src/cli.ts +7 -2
- package/src/commands/analyze-heap.ts +4 -1
- package/src/commands/ast-ground-truth-args.ts +15 -12
- package/src/commands/ast-ground-truth-driver.ts +2 -1
- package/src/commands/ast-ground-truth.ts +1 -1
- package/src/commands/client-session.ts +33 -0
- package/src/commands/create-args.ts +68 -0
- package/src/commands/create.ts +83 -0
- package/src/commands/delete-args.ts +64 -0
- package/src/commands/delete.ts +61 -0
- package/src/commands/generate-languages.ts +132 -0
- package/src/commands/generate-transfer-model-args.ts +7 -0
- package/src/commands/generate-transfer-model-config.ts +6 -3
- package/src/commands/generate-transfer-model.ts +56 -8
- package/src/commands/harness-args.ts +99 -31
- package/src/commands/headless-harness.ts +86 -26
- package/src/commands/init-args.ts +32 -27
- package/src/commands/init-prompt.ts +5 -6
- package/src/commands/init-templates.ts +790 -696
- package/src/commands/init-wizard.ts +17 -27
- package/src/commands/init-workspace.ts +31 -16
- package/src/commands/init.ts +126 -58
- package/src/commands/kebab.ts +17 -0
- package/src/commands/lint-grammar-args.ts +17 -13
- package/src/commands/lint-grammar-driver.ts +2 -1
- package/src/commands/lint-grammar.ts +1 -1
- package/src/commands/measure-memory-args.ts +27 -14
- package/src/commands/measure-memory-driver.ts +5 -3
- package/src/commands/measure-memory.ts +1 -1
- package/src/commands/model-docs-args.ts +16 -13
- package/src/commands/model-docs-driver.ts +2 -1
- package/src/commands/model-docs.ts +1 -1
- package/src/commands/projects-args.ts +5 -4
- package/src/commands/projects.ts +4 -6
- package/src/commands/query-args.ts +5 -4
- package/src/commands/query.ts +4 -6
- package/src/commands/reflect-args.ts +17 -13
- package/src/commands/reflect-driver.ts +2 -1
- package/src/commands/reflect.ts +1 -1
- package/src/commands/save-args.ts +7 -3
- package/src/commands/save.ts +24 -27
- package/src/commands/validate-args.ts +17 -13
- package/src/commands/validate-driver.ts +2 -1
- package/src/commands/validate.ts +1 -1
- package/src/commands/watch-args.ts +6 -3
- package/src/commands/watch.ts +28 -7
- package/src/driver-heap.ts +65 -0
- package/src/index.ts +2 -0
- package/src/log-level.ts +42 -14
- package/src/spawn-data-server.ts +17 -5
- package/src/stderr-connection-logger.ts +18 -0
- package/src/testing/echo-server.ts +11 -2
- package/templates/gitignore +1 -0
- package/templates/protocol/gitignore +3 -0
- package/templates/protocol/src/__GRAMMAR_ID__-diagram-types.ts +41 -0
- package/templates/server/gitignore +6 -0
- package/templates/server/src/glsp/__GRAMMAR_ID__/create-node-operation-handler.ts +62 -0
- package/templates/server/src/glsp/__GRAMMAR_ID__/gmodel-factory.ts +60 -0
- package/templates/server/src/glsp/__GRAMMAR_ID__/state.ts +18 -0
- package/templates/server/src/glsp/__GRAMMAR_ID__/storage.ts +16 -0
- package/templates/server/src/glsp/__GRAMMAR_ID__/submission-handler.ts +24 -0
- package/templates/server/src/grammar/common.langium +12 -0
- package/templates/server/src/language-server/__GRAMMAR_ID__-serializer.ts +53 -0
- package/templates/server/src/language-server/ast.ts +23 -0
- package/templates/server/src/node/services.ts +16 -0
- package/templates/server/test/__GRAMMAR_ID__-diagram.test.ts +112 -0
- package/templates/server/test/tsconfig.json +4 -0
- package/templates/server/tsconfig.test.json +10 -0
- package/templates/server/vitest.config.ts +14 -0
- package/templates/workspace/nodes.__EXTENSION__ +5 -0
- package/templates/workspace/references.__EXTENSION__ +2 -0
|
@@ -55,8 +55,9 @@
|
|
|
55
55
|
*/
|
|
56
56
|
|
|
57
57
|
import * as path from 'node:path';
|
|
58
|
-
import { DEFAULT_INIT_HEADS, INIT_HEADS, type InitHead, isNonEmptyDir,
|
|
59
|
-
import {
|
|
58
|
+
import { DEFAULT_INIT_HEADS, INIT_HEADS, type InitHead, isNonEmptyDir, isUsableExtension, memberPlacements } from './init.js';
|
|
59
|
+
import { kebab } from './kebab.js';
|
|
60
|
+
import { HEAD_CHOICES, InitWizardCancelled, type PromptPort } from './init-prompt.js';
|
|
60
61
|
import { type WorkspaceDetection } from './init-workspace.js';
|
|
61
62
|
|
|
62
63
|
/** One grammar as the wizard collected it. */
|
|
@@ -158,13 +159,13 @@ export function formatCommand(argv: readonly string[]): string {
|
|
|
158
159
|
/** Validate a file extension, and reject one another grammar already claims. */
|
|
159
160
|
function extensionProblem(value: string, taken: ReadonlySet<string>): string | undefined {
|
|
160
161
|
const extension = value.replace(/^\./, '');
|
|
161
|
-
if (
|
|
162
|
+
if (!isUsableExtension(extension)) {
|
|
162
163
|
return `'${value}' is not a usable file extension.`;
|
|
163
164
|
}
|
|
164
165
|
// Checked here rather than left to the scaffolder so the answer is corrected
|
|
165
166
|
// while the grammar that owns it is still on screen. Langium routes documents
|
|
166
167
|
// to a language BY extension, so a duplicate is a silent mis-route.
|
|
167
|
-
return taken.has(extension) ? `'.${extension}' is already claimed by another grammar.` : undefined;
|
|
168
|
+
return taken.has(extension.toLowerCase()) ? `'.${extension}' is already claimed by another grammar.` : undefined;
|
|
168
169
|
}
|
|
169
170
|
|
|
170
171
|
/**
|
|
@@ -198,31 +199,19 @@ async function askGrammar(
|
|
|
198
199
|
return { name, extension: extension.replace(/^\./, ''), diagram: false };
|
|
199
200
|
}
|
|
200
201
|
|
|
201
|
-
/**
|
|
202
|
-
* Ask for the protocol head set.
|
|
203
|
-
*
|
|
204
|
-
* `lsp` is not offered, it is stated: it owns the workspace, the build pipeline
|
|
205
|
-
* and the shared tier the other heads read through, so a project without it has
|
|
206
|
-
* nothing for them to serve. Presenting it as a checkbox would offer a choice
|
|
207
|
-
* the scaffolder then refuses.
|
|
208
|
-
*/
|
|
202
|
+
/** Ask for the protocol head set, in composition order whatever order they were picked in. */
|
|
209
203
|
async function askHeads(prompt: PromptPort): Promise<InitHead[]> {
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
choices: OPTIONAL_HEAD_CHOICES,
|
|
213
|
-
initialValues: DEFAULT_INIT_HEADS.filter((head): head is Exclude<InitHead, 'lsp'> => head !== 'lsp')
|
|
214
|
-
});
|
|
215
|
-
const chosen = new Set<InitHead>(['lsp', ...optional]);
|
|
216
|
-
return INIT_HEADS.filter(head => chosen.has(head));
|
|
204
|
+
const chosen = await prompt.multiselect({ message: 'Protocol heads', choices: HEAD_CHOICES, initialValues: DEFAULT_INIT_HEADS });
|
|
205
|
+
return INIT_HEADS.filter(head => chosen.includes(head));
|
|
217
206
|
}
|
|
218
207
|
|
|
219
208
|
/**
|
|
220
209
|
* Mark the grammar a scaffolded diagram edits.
|
|
221
210
|
*
|
|
222
|
-
* Only a real question with `glsp` on AND several grammars:
|
|
223
|
-
*
|
|
224
|
-
* that answer already — so asking would offer a choice
|
|
225
|
-
* silently overrule.
|
|
211
|
+
* Only a real question with `glsp` on AND several grammars: the scaffolded
|
|
212
|
+
* diagram is typed over one grammar's model, and with a single grammar the
|
|
213
|
+
* scaffolder derives that answer already — so asking would offer a choice
|
|
214
|
+
* whose "no" it would silently overrule.
|
|
226
215
|
*/
|
|
227
216
|
async function askDiagramGrammar(
|
|
228
217
|
prompt: PromptPort,
|
|
@@ -268,10 +257,11 @@ async function askWorkspaceMembership(
|
|
|
268
257
|
if (detection === undefined) {
|
|
269
258
|
return { monorepo: false };
|
|
270
259
|
}
|
|
260
|
+
const uncovered = memberPlacements(detection).filter(placement => placement.coveredBy === undefined);
|
|
271
261
|
const covered =
|
|
272
|
-
|
|
273
|
-
?
|
|
274
|
-
: `
|
|
262
|
+
uncovered.length === 0
|
|
263
|
+
? 'Its packages are already covered by the workspaces entries.'
|
|
264
|
+
: `The root manifest has no entry covering ${uncovered.map(placement => placement.path).join(', ')}, so ${uncovered.length === 1 ? 'one is' : 'they are'} printed for you to add.`;
|
|
275
265
|
// The placement is named, not just the root. The root alone reads as "this
|
|
276
266
|
// ran somewhere I did not ask for" whenever the command was issued from a
|
|
277
267
|
// different directory than the target, which is the normal case.
|
|
@@ -364,7 +354,7 @@ export async function runInitWizard(
|
|
|
364
354
|
const grammar = await askGrammar(prompt, grammars.length, grammars.length === 0 ? name : undefined, takenNames, takenExtensions);
|
|
365
355
|
grammars.push(grammar);
|
|
366
356
|
takenNames.add(grammar.name);
|
|
367
|
-
takenExtensions.add(grammar.extension);
|
|
357
|
+
takenExtensions.add(grammar.extension.toLowerCase());
|
|
368
358
|
} while (await prompt.confirm({ message: 'Add another grammar?', initialValue: false }));
|
|
369
359
|
|
|
370
360
|
const withDiagram = await askDiagramGrammar(prompt, heads, grammars);
|
|
@@ -169,14 +169,6 @@ export interface WorkspaceDetection {
|
|
|
169
169
|
readonly workspaces: readonly string[];
|
|
170
170
|
/** POSIX path of the target relative to {@link rootDir}, e.g. `packages/foo`. */
|
|
171
171
|
readonly targetPath: string;
|
|
172
|
-
/**
|
|
173
|
-
* The `workspaces` glob already covering {@link targetPath}, if any.
|
|
174
|
-
*
|
|
175
|
-
* Absent means the root manifest needs a new entry — which is why this is
|
|
176
|
-
* computed per target rather than assumed: a `packages/*` glob covers a new
|
|
177
|
-
* package for free, while a repo that lists its examples one by one does not.
|
|
178
|
-
*/
|
|
179
|
-
readonly coveredBy?: string;
|
|
180
172
|
/** The npm scope the sibling packages agree on (`@acme`), when they agree. */
|
|
181
173
|
readonly scope?: string;
|
|
182
174
|
/**
|
|
@@ -186,10 +178,14 @@ export interface WorkspaceDetection {
|
|
|
186
178
|
readonly baseTsconfig?: string;
|
|
187
179
|
/** That tsconfig's `compilerOptions`, so the emitted one can drop what it inherits. */
|
|
188
180
|
readonly baseCompilerOptions?: Readonly<Record<string, JsonValue>>;
|
|
189
|
-
/** The root
|
|
181
|
+
/** The root formatter config's `printWidth`, so emitted sources wrap where the repo wraps. */
|
|
190
182
|
readonly printWidth?: number;
|
|
191
183
|
/** Filename of the root eslint config, when there is one. Absent means the repo does not lint. */
|
|
192
184
|
readonly eslintConfig?: string;
|
|
185
|
+
/** Filename of the root Oxlint config, when present. */
|
|
186
|
+
readonly oxlintConfig?: string;
|
|
187
|
+
/** The npm version the root's `packageManager` field declares, when it names npm. */
|
|
188
|
+
readonly packageManagerNpm?: string;
|
|
193
189
|
}
|
|
194
190
|
|
|
195
191
|
/**
|
|
@@ -339,7 +335,7 @@ export function findBaseTsconfig(
|
|
|
339
335
|
}
|
|
340
336
|
|
|
341
337
|
/**
|
|
342
|
-
* The repo's
|
|
338
|
+
* The repo's formatter `printWidth`, so the emitted sources wrap where the repo
|
|
343
339
|
* wraps instead of at the scaffold's own default.
|
|
344
340
|
*
|
|
345
341
|
* **Why this is worth detecting at all.** There is no width that is stable for
|
|
@@ -360,19 +356,21 @@ export function findBaseTsconfig(
|
|
|
360
356
|
* adopter's formatter rewraps on first commit. So a heuristic that is usually
|
|
361
357
|
* right strictly improves on no detection, and can break nothing.
|
|
362
358
|
*/
|
|
363
|
-
export function
|
|
359
|
+
export function findFormatterPrintWidth(rootDir: string, probe: WorkspaceProbe): number | undefined {
|
|
364
360
|
const manifest = probe.readJson(path.join(rootDir, 'package.json'));
|
|
365
361
|
if (isJsonObject(manifest) && isJsonObject(manifest.prettier) && typeof manifest.prettier.printWidth === 'number') {
|
|
366
362
|
return manifest.prettier.printWidth;
|
|
367
363
|
}
|
|
368
364
|
const present = probe.listFiles(rootDir);
|
|
369
|
-
for (const file of present.filter(name => name === '.prettierrc' || name === '.prettierrc.json')) {
|
|
365
|
+
for (const file of present.filter(name => name === '.oxfmtrc.json' || name === '.prettierrc' || name === '.prettierrc.json')) {
|
|
370
366
|
const parsed = probe.readJson(path.join(rootDir, file));
|
|
371
367
|
if (isJsonObject(parsed) && typeof parsed.printWidth === 'number') {
|
|
372
368
|
return parsed.printWidth;
|
|
373
369
|
}
|
|
374
370
|
}
|
|
375
|
-
for (const file of present.filter(name =>
|
|
371
|
+
for (const file of present.filter(name =>
|
|
372
|
+
/^(\.prettierrc\.[cm]?[jt]s|prettier\.config\.[cm]?[jt]s|oxfmt\.config\.[cm]?[jt]s)$/.test(name)
|
|
373
|
+
)) {
|
|
376
374
|
const width = /\bprintWidth\s*:\s*(\d+)/.exec(probe.readText(path.join(rootDir, file)) ?? '');
|
|
377
375
|
if (width) {
|
|
378
376
|
return Number(width[1]);
|
|
@@ -398,6 +396,22 @@ export function findEslintConfig(rootDir: string, probe: WorkspaceProbe): string
|
|
|
398
396
|
return probe.listFiles(rootDir).find(name => /^eslint\.config\.[cm]?[jt]s$/.test(name) || /^\.eslintrc(\..+)?$/.test(name));
|
|
399
397
|
}
|
|
400
398
|
|
|
399
|
+
/** Detect Oxlint without evaluating executable configuration files. */
|
|
400
|
+
export function findOxlintConfig(rootDir: string, probe: WorkspaceProbe): string | undefined {
|
|
401
|
+
return probe.listFiles(rootDir).find(name => /^(oxlint\.config\.[cm]?[jt]s|\.oxlintrc\.jsonc?)$/.test(name));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/** The npm version the root's `packageManager` field declares (`npm@11.15.0` gives `11.15.0`), when it names npm. */
|
|
405
|
+
export function findPackageManagerNpm(rootDir: string, probe: WorkspaceProbe): string | undefined {
|
|
406
|
+
const manifest = probe.readJson(path.join(rootDir, 'package.json'));
|
|
407
|
+
return parseNpmVersion(isJsonObject(manifest) && typeof manifest.packageManager === 'string' ? manifest.packageManager : undefined);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** The npm version, prerelease included, of a `packageManager` field (`npm@…`) or a user agent (`npm/…`); undefined for another tool. */
|
|
411
|
+
export function parseNpmVersion(spec: string | undefined): string | undefined {
|
|
412
|
+
return /^npm[@/](\d+\.\d+\.\d+(?:-[\w.]+)?)/.exec(spec ?? '')?.[1];
|
|
413
|
+
}
|
|
414
|
+
|
|
401
415
|
/** POSIX-style relative path, which is what both `extends` and `--prefix` want on every platform. */
|
|
402
416
|
function posixRelative(from: string, to: string): string {
|
|
403
417
|
return path.relative(from, to).split(path.sep).join('/');
|
|
@@ -432,11 +446,12 @@ export function detectWorkspace(targetDir: string, probe: WorkspaceProbe): Works
|
|
|
432
446
|
rootDir,
|
|
433
447
|
workspaces,
|
|
434
448
|
targetPath,
|
|
435
|
-
coveredBy: workspaces.find(glob => workspaceGlobCovers(glob, targetPath)),
|
|
436
449
|
scope: inferPackageScope(rootDir, workspaces, probe),
|
|
437
450
|
baseTsconfig: base && posixRelative(absoluteTarget, path.join(rootDir, base.file)),
|
|
438
451
|
baseCompilerOptions: base?.compilerOptions,
|
|
439
|
-
printWidth:
|
|
440
|
-
eslintConfig: findEslintConfig(rootDir, probe)
|
|
452
|
+
printWidth: findFormatterPrintWidth(rootDir, probe),
|
|
453
|
+
eslintConfig: findEslintConfig(rootDir, probe),
|
|
454
|
+
oxlintConfig: findOxlintConfig(rootDir, probe),
|
|
455
|
+
packageManagerNpm: findPackageManagerNpm(rootDir, probe)
|
|
441
456
|
};
|
|
442
457
|
}
|
package/src/commands/init.ts
CHANGED
|
@@ -9,15 +9,21 @@
|
|
|
9
9
|
|
|
10
10
|
import * as fs from 'node:fs';
|
|
11
11
|
import * as path from 'node:path';
|
|
12
|
-
import { buildInitTemplates, readFrameworkVersion,
|
|
13
|
-
import {
|
|
12
|
+
import { buildInitTemplates, readFrameworkVersion, workspaceRootPins } from './init-templates.js';
|
|
13
|
+
import { kebab } from './kebab.js';
|
|
14
|
+
import {
|
|
15
|
+
createNodeWorkspaceProbe,
|
|
16
|
+
detectWorkspace,
|
|
17
|
+
parseNpmVersion,
|
|
18
|
+
workspaceGlobCovers,
|
|
19
|
+
type JsonValue,
|
|
20
|
+
type WorkspaceDetection,
|
|
21
|
+
type WorkspaceProbe
|
|
22
|
+
} from './init-workspace.js';
|
|
14
23
|
|
|
15
24
|
/**
|
|
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.
|
|
25
|
+
* The protocol heads a scaffold can start, in composition order. Any non-empty
|
|
26
|
+
* set: without `lsp`, the socket heads bring the workspace up themselves.
|
|
21
27
|
*/
|
|
22
28
|
export const INIT_HEADS = ['lsp', 'data', 'glsp'] as const;
|
|
23
29
|
|
|
@@ -27,6 +33,35 @@ export type InitHead = (typeof INIT_HEADS)[number];
|
|
|
27
33
|
/** The heads a scaffold emits when `--heads` is not given. */
|
|
28
34
|
export const DEFAULT_INIT_HEADS: readonly InitHead[] = ['lsp', 'data'];
|
|
29
35
|
|
|
36
|
+
/**
|
|
37
|
+
* The library packages of a scaffolded project, each in the directory of its
|
|
38
|
+
* name, in the order a workspace lists them.
|
|
39
|
+
*/
|
|
40
|
+
export const INIT_MEMBERS = ['protocol', 'server'] as const;
|
|
41
|
+
|
|
42
|
+
/** One library member — see {@link INIT_MEMBERS}. */
|
|
43
|
+
export type InitMember = (typeof INIT_MEMBERS)[number];
|
|
44
|
+
|
|
45
|
+
/** A member's path relative to the workspace root, and the `workspaces` entry already covering it. */
|
|
46
|
+
export interface InitMemberPlacement {
|
|
47
|
+
readonly path: string;
|
|
48
|
+
readonly coveredBy?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Where each member lands in a detected workspace.
|
|
53
|
+
*
|
|
54
|
+
* Judged per member rather than for the project directory, which holds no
|
|
55
|
+
* package: a root listing its members one by one needs an entry for each,
|
|
56
|
+
* while a `packages/*` glob covers none of them one level deeper.
|
|
57
|
+
*/
|
|
58
|
+
export function memberPlacements(detection: WorkspaceDetection): readonly InitMemberPlacement[] {
|
|
59
|
+
return INIT_MEMBERS.map(member => {
|
|
60
|
+
const memberPath = `${detection.targetPath}/${member}`;
|
|
61
|
+
return { path: memberPath, coveredBy: detection.workspaces.find(glob => workspaceGlobCovers(glob, memberPath)) };
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
30
65
|
/** True for a string naming one of {@link INIT_HEADS}. */
|
|
31
66
|
export function isInitHead(value: string): value is InitHead {
|
|
32
67
|
return (INIT_HEADS as readonly string[]).includes(value);
|
|
@@ -55,10 +90,9 @@ export interface InitGrammarSpec {
|
|
|
55
90
|
/**
|
|
56
91
|
* Scaffold a GLSP diagram for this grammar. Requires the `glsp` head.
|
|
57
92
|
*
|
|
58
|
-
* Per grammar rather than per project because
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* with no diagram sitting on the same server is the normal case.
|
|
93
|
+
* Per grammar rather than per project because the scaffolded diagram is typed
|
|
94
|
+
* over one grammar's model, and a grammar with no diagram sitting on the same
|
|
95
|
+
* server is the normal case.
|
|
62
96
|
*/
|
|
63
97
|
readonly diagram?: boolean;
|
|
64
98
|
}
|
|
@@ -128,10 +162,16 @@ export interface InitWorkspacePlacement {
|
|
|
128
162
|
readonly baseTsconfig?: string;
|
|
129
163
|
/** That tsconfig's `compilerOptions`, so the emitted one can omit what it would inherit unchanged. */
|
|
130
164
|
readonly baseCompilerOptions?: Readonly<Record<string, JsonValue>>;
|
|
131
|
-
/** The workspace's
|
|
165
|
+
/** The workspace's formatter `printWidth`, so emitted sources wrap where the repo wraps. */
|
|
132
166
|
readonly printWidth?: number;
|
|
133
167
|
/** Filename of the workspace's eslint config; its presence is what makes the scaffold emit a `lint` script. */
|
|
134
168
|
readonly eslintConfig?: string;
|
|
169
|
+
/** Filename of the workspace Oxlint config, when present. */
|
|
170
|
+
readonly oxlintConfig?: string;
|
|
171
|
+
/** The npm version the workspace root's `packageManager` declares; with {@link npmVersion} it decides the scaffold's `vitest` range. */
|
|
172
|
+
readonly packageManagerNpm?: string;
|
|
173
|
+
/** The npm running `init`, when known. npm does not enforce `packageManager`, so a declaration alone cannot lift `vitest`. */
|
|
174
|
+
readonly npmVersion?: string;
|
|
135
175
|
}
|
|
136
176
|
|
|
137
177
|
/**
|
|
@@ -185,7 +225,7 @@ export interface InitComposition {
|
|
|
185
225
|
* one data server per project, not one per language).
|
|
186
226
|
*/
|
|
187
227
|
readonly projectId: string;
|
|
188
|
-
/** The protocol heads to start,
|
|
228
|
+
/** The protocol heads to start, at least one. */
|
|
189
229
|
readonly heads: readonly InitHead[];
|
|
190
230
|
/** The grammars, in the order given. The first is the configuration root. */
|
|
191
231
|
readonly grammars: readonly InitGrammarNames[];
|
|
@@ -226,10 +266,9 @@ export interface InitCommandOptions {
|
|
|
226
266
|
readonly force?: boolean;
|
|
227
267
|
/**
|
|
228
268
|
* Scaffold a member of the surrounding npm workspace: extend its base
|
|
229
|
-
* tsconfig
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
* project the caller did not ask for.
|
|
269
|
+
* tsconfig and address the package by `--prefix` in the scripts it prints
|
|
270
|
+
* back at itself. Errors when no ancestor declares `workspaces`, rather than
|
|
271
|
+
* silently degrading to a standalone project the caller did not ask for.
|
|
233
272
|
*/
|
|
234
273
|
readonly monorepo?: boolean;
|
|
235
274
|
/** npm scope for the package name, e.g. `@acme`. */
|
|
@@ -244,21 +283,26 @@ export interface InitCommandOptions {
|
|
|
244
283
|
readonly __writeFilesForTest?: (targetDir: string, files: readonly InitFile[]) => void;
|
|
245
284
|
}
|
|
246
285
|
|
|
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
286
|
/** True for a PascalCase identifier legal as a generated TypeScript symbol prefix. */
|
|
253
287
|
function isPascalIdentifier(value: string): boolean {
|
|
254
288
|
return /^[A-Za-z][A-Za-z0-9]*$/.test(value);
|
|
255
289
|
}
|
|
256
290
|
|
|
257
|
-
/**
|
|
291
|
+
/**
|
|
292
|
+
* True for a file extension, without its leading dot, that is safe in an
|
|
293
|
+
* emitted file name and string literal: letters, digits and dashes.
|
|
294
|
+
*/
|
|
295
|
+
export function isUsableExtension(extension: string): boolean {
|
|
296
|
+
return /^[A-Za-z0-9][A-Za-z0-9-]*$/.test(extension);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Strip a leading dot and reject an extension {@link isUsableExtension} refuses. */
|
|
258
300
|
function normalizeExtension(value: string, grammar: string): string {
|
|
259
301
|
const extension = value.replace(/^\./, '');
|
|
260
|
-
if (extension
|
|
261
|
-
throw new Error(
|
|
302
|
+
if (!isUsableExtension(extension)) {
|
|
303
|
+
throw new Error(
|
|
304
|
+
`Invalid extension '${value}' for grammar '${grammar}': use letters, digits and dashes, starting with a letter or digit.`
|
|
305
|
+
);
|
|
262
306
|
}
|
|
263
307
|
return extension;
|
|
264
308
|
}
|
|
@@ -282,6 +326,11 @@ function resolveGrammar(spec: InitGrammarSpec, projectId: string, qualify: boole
|
|
|
282
326
|
if (extensions.length === 0) {
|
|
283
327
|
throw new Error(`Invalid --extensions for grammar '${spec.name}': at least one extension is required.`);
|
|
284
328
|
}
|
|
329
|
+
if (spec.languageId !== undefined && !/^[a-z0-9][a-z0-9-]*$/.test(spec.languageId)) {
|
|
330
|
+
throw new Error(
|
|
331
|
+
`Invalid --language-id '${spec.languageId}' for grammar '${spec.name}': use lowercase letters, digits and dashes, e.g. 'my-lang'.`
|
|
332
|
+
);
|
|
333
|
+
}
|
|
285
334
|
return {
|
|
286
335
|
grammar: spec.name,
|
|
287
336
|
grammarId,
|
|
@@ -298,8 +347,9 @@ function resolveGrammar(spec: InitGrammarSpec, projectId: string, qualify: boole
|
|
|
298
347
|
*
|
|
299
348
|
* With the `glsp` head on and nothing marked, a lone grammar is unambiguous and
|
|
300
349
|
* is taken as the diagram's; with several it is a real choice, so it is required
|
|
301
|
-
* rather than guessed —
|
|
302
|
-
*
|
|
350
|
+
* rather than guessed — the scaffolded state, storage and GModel factory are
|
|
351
|
+
* typed over the chosen grammar's model, so a wrong guess emits a diagram of
|
|
352
|
+
* the wrong model.
|
|
303
353
|
*/
|
|
304
354
|
function resolveDiagrams(specs: readonly InitGrammarSpec[], heads: readonly InitHead[]): boolean[] {
|
|
305
355
|
const marked = specs.map(spec => spec.diagram === true);
|
|
@@ -325,18 +375,13 @@ function resolveDiagrams(specs: readonly InitGrammarSpec[], heads: readonly Init
|
|
|
325
375
|
);
|
|
326
376
|
}
|
|
327
377
|
|
|
328
|
-
/** Validate the head set: known names only
|
|
378
|
+
/** Validate the head set: known names only. */
|
|
329
379
|
function resolveHeads(heads: readonly InitHead[] | undefined): readonly InitHead[] {
|
|
330
380
|
const resolved = heads === undefined || heads.length === 0 ? DEFAULT_INIT_HEADS : heads;
|
|
331
381
|
const unknown = resolved.filter(head => !isInitHead(head));
|
|
332
382
|
if (unknown.length > 0) {
|
|
333
383
|
throw new Error(`Unknown head(s) '${unknown.join(', ')}': expected one of ${INIT_HEADS.join(', ')}.`);
|
|
334
384
|
}
|
|
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
385
|
// De-duplicated and put back in composition order, so the order the heads
|
|
341
386
|
// were named in does not change the emitted project.
|
|
342
387
|
return INIT_HEADS.filter(head => resolved.includes(head));
|
|
@@ -368,15 +413,18 @@ function assertNoCollisions(grammars: readonly InitGrammarNames[]): void {
|
|
|
368
413
|
}
|
|
369
414
|
byLanguageId.set(grammar.languageId, grammar.grammar);
|
|
370
415
|
|
|
416
|
+
// Case-folded: a starter file is named by extension, and `nodes.Foo` and
|
|
417
|
+
// `nodes.foo` are one file on a case-insensitive file system.
|
|
371
418
|
for (const extension of grammar.extensions) {
|
|
372
|
-
const
|
|
419
|
+
const key = extension.toLowerCase();
|
|
420
|
+
const clashingExtension = byExtension.get(key);
|
|
373
421
|
if (clashingExtension !== undefined) {
|
|
374
422
|
throw new Error(
|
|
375
423
|
`File extension '.${extension}' is claimed by both '${clashingExtension}' and '${grammar.grammar}'. ` +
|
|
376
424
|
'Langium routes documents by extension, so each grammar needs its own.'
|
|
377
425
|
);
|
|
378
426
|
}
|
|
379
|
-
byExtension.set(
|
|
427
|
+
byExtension.set(key, grammar.grammar);
|
|
380
428
|
}
|
|
381
429
|
}
|
|
382
430
|
}
|
|
@@ -432,6 +480,8 @@ export interface InitPackagingOptions {
|
|
|
432
480
|
readonly scope?: string;
|
|
433
481
|
readonly public?: boolean;
|
|
434
482
|
readonly probe?: WorkspaceProbe;
|
|
483
|
+
/** The npm running `init`. Defaults to the one `npm_config_user_agent` names, which `npx` and `npm run` set. */
|
|
484
|
+
readonly npmVersion?: string;
|
|
435
485
|
}
|
|
436
486
|
|
|
437
487
|
/**
|
|
@@ -466,7 +516,10 @@ export function resolveInitPackaging(targetDir: string, options: InitPackagingOp
|
|
|
466
516
|
baseTsconfig: detection.baseTsconfig,
|
|
467
517
|
baseCompilerOptions: detection.baseCompilerOptions,
|
|
468
518
|
printWidth: detection.printWidth,
|
|
469
|
-
eslintConfig: detection.eslintConfig
|
|
519
|
+
eslintConfig: detection.eslintConfig,
|
|
520
|
+
oxlintConfig: detection.oxlintConfig,
|
|
521
|
+
packageManagerNpm: detection.packageManagerNpm,
|
|
522
|
+
npmVersion: options.npmVersion ?? parseNpmVersion(process.env.npm_config_user_agent)
|
|
470
523
|
}
|
|
471
524
|
};
|
|
472
525
|
}
|
|
@@ -509,45 +562,60 @@ export function runInit(options: InitCommandOptions): void {
|
|
|
509
562
|
write('\n');
|
|
510
563
|
write(`Workspace root: ${detection.rootDir}\n`);
|
|
511
564
|
if (detection.baseTsconfig !== undefined) {
|
|
512
|
-
write(
|
|
565
|
+
write(
|
|
566
|
+
` Each package's tsconfig.json extends the root's ${path.posix.basename(detection.baseTsconfig)}, minus the options it inherits unchanged.\n`
|
|
567
|
+
);
|
|
513
568
|
} else {
|
|
514
|
-
write(
|
|
569
|
+
write(" No root tsconfig carries compilerOptions, so each package's stands alone.\n");
|
|
515
570
|
}
|
|
516
|
-
write(" .gitignore holds `syntaxes/` only — the workspace root's covers the rest.\n");
|
|
517
571
|
if (detection.printWidth !== undefined) {
|
|
518
|
-
write(` Sources wrapped at ${detection.printWidth} columns, from ${detection.rootDir}'s
|
|
572
|
+
write(` Sources wrapped at ${detection.printWidth} columns, from ${detection.rootDir}'s formatter config.\n`);
|
|
519
573
|
}
|
|
520
|
-
if (detection.eslintConfig !== undefined) {
|
|
521
|
-
write(
|
|
574
|
+
if (detection.oxlintConfig !== undefined || detection.eslintConfig !== undefined) {
|
|
575
|
+
write(
|
|
576
|
+
` Each package got a lint script — ${detection.oxlintConfig ?? detection.eslintConfig} says this repo lints. Adjust them if your invocation differs.\n`
|
|
577
|
+
);
|
|
522
578
|
}
|
|
523
579
|
// Printed rather than applied: `init` writes inside the target directory
|
|
524
580
|
// and nowhere else, so a scaffold can never surprise-edit a manifest that
|
|
525
581
|
// is under review, mid-rebase, or simply not the caller's to change.
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
write(
|
|
582
|
+
const placements = memberPlacements(detection);
|
|
583
|
+
const uncovered = placements.filter(placement => placement.coveredBy === undefined);
|
|
584
|
+
if (uncovered.length > 0) {
|
|
585
|
+
write(` Add ${uncovered.length === 1 ? 'this' : 'these'} to "workspaces" in ${path.join(detection.rootDir, 'package.json')}:\n`);
|
|
586
|
+
write(`${uncovered.map(placement => ` "${placement.path}"`).join(',\n')}\n`);
|
|
587
|
+
}
|
|
588
|
+
for (const placement of placements.filter(candidate => candidate.coveredBy !== undefined)) {
|
|
589
|
+
write(` ${placement.path} is already covered by the "${placement.coveredBy}" workspaces entry.\n`);
|
|
531
590
|
}
|
|
591
|
+
const pins = workspaceRootPins(composition.heads).map(([name, version]) => ` "${name}": "${version}"`);
|
|
592
|
+
write(
|
|
593
|
+
` Add these to "devDependencies" in ${path.join(detection.rootDir, 'package.json')}, so another member's version of one ` +
|
|
594
|
+
`cannot displace the copy these packages share with the framework:\n${pins.join(',\n')}\n`
|
|
595
|
+
);
|
|
532
596
|
if (composition.packaging.scope === undefined && detection.scope !== undefined) {
|
|
533
597
|
write(` Sibling packages all use the "${detection.scope}" scope; pass --scope ${detection.scope} to match them.\n`);
|
|
534
598
|
}
|
|
535
599
|
}
|
|
536
600
|
write('\n');
|
|
537
601
|
write('Next steps:\n');
|
|
538
|
-
|
|
539
|
-
//
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
602
|
+
// A workspace member's project has no manifest of its own, so its steps run
|
|
603
|
+
// from the workspace root and name the server by `--prefix`.
|
|
604
|
+
const server = detection === undefined ? 'server' : `${detection.targetPath}/server`;
|
|
605
|
+
if (detection === undefined) {
|
|
606
|
+
write(` cd ${options.targetDir}\n`);
|
|
607
|
+
write(' npm install\n');
|
|
608
|
+
write(' npm run build # langium generate + tsc, every package\n');
|
|
609
|
+
write(' npm test # the server tests\n');
|
|
610
|
+
} else {
|
|
611
|
+
write(` cd ${detection.rootDir}\n`);
|
|
612
|
+
write(' npm install\n');
|
|
613
|
+
write(` npm --prefix ${server} run build # langium generate + tsc, protocol included\n`);
|
|
614
|
+
write(` npm --prefix ${server} test\n`);
|
|
615
|
+
}
|
|
548
616
|
// `npx`, not a bare invocation: `@hydranium/cli` is a devDependency of the
|
|
549
617
|
// scaffold, so the binary is on PATH inside an npm script and nowhere else.
|
|
550
|
-
write(
|
|
618
|
+
write(` npx hydranium-cli reflect --services ./${server}/lib/node/services.js\n`);
|
|
551
619
|
write('\n');
|
|
552
620
|
write('The emitted sources carry no license header — run your own header tool if\n');
|
|
553
621
|
write('your repo gates them. See the README section on that.\n');
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 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
|
+
/**
|
|
11
|
+
* Kebab-case a PascalCase/camelCase name. Shared by `init`, which derives a
|
|
12
|
+
* grammar's id from it, and the languages emitter, whose constant names have
|
|
13
|
+
* to split a grammar name the same way.
|
|
14
|
+
*/
|
|
15
|
+
export function kebab(name: string): string {
|
|
16
|
+
return name.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
|
|
17
|
+
}
|
|
@@ -9,34 +9,36 @@
|
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
11
|
exitWithUsage,
|
|
12
|
+
HARNESS_VALUE_FLAGS,
|
|
12
13
|
helpRequested,
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
importHelpLines,
|
|
15
|
+
LOG_VALUE_FLAGS,
|
|
16
|
+
logHelpLines,
|
|
17
|
+
logOptions,
|
|
16
18
|
parseHarnessArgs,
|
|
17
19
|
printHelp,
|
|
18
|
-
SERVICES_FLAG,
|
|
19
20
|
type UsageError
|
|
20
21
|
} from './harness-args.js';
|
|
21
22
|
import { runLintGrammar, type LintGrammarCommandOptions } from './lint-grammar.js';
|
|
22
23
|
|
|
23
|
-
const VALUE_FLAGS = [
|
|
24
|
+
const VALUE_FLAGS = [...LOG_VALUE_FLAGS] as const;
|
|
24
25
|
const BOOL_FLAGS = ['--strict', '--json'] as const;
|
|
25
26
|
const REPEATABLE_VALUE_FLAGS = ['--name-property'] as const;
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
29
|
* 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.
|
|
30
|
-
* consumed by the shared harness parser itself,
|
|
30
|
+
* so the list cannot claim a flag the parser would reject.
|
|
31
|
+
* {@link HARNESS_VALUE_FLAGS} are consumed by the shared harness parser itself,
|
|
32
|
+
* so they are in neither set.
|
|
31
33
|
*/
|
|
32
|
-
export const LINT_GRAMMAR_FLAGS: readonly string[] = [
|
|
34
|
+
export const LINT_GRAMMAR_FLAGS: readonly string[] = [...HARNESS_VALUE_FLAGS, ...REPEATABLE_VALUE_FLAGS, ...VALUE_FLAGS, ...BOOL_FLAGS];
|
|
33
35
|
|
|
34
36
|
/** The subset that takes a value, so `--help` in a value position reads as data. */
|
|
35
|
-
export const LINT_GRAMMAR_VALUE_FLAGS: readonly string[] = [
|
|
37
|
+
export const LINT_GRAMMAR_VALUE_FLAGS: readonly string[] = [...HARNESS_VALUE_FLAGS, ...REPEATABLE_VALUE_FLAGS, ...VALUE_FLAGS];
|
|
36
38
|
|
|
37
39
|
/** The `--help` text, as data, held to {@link LINT_GRAMMAR_FLAGS} by a test. */
|
|
38
40
|
export const LINT_GRAMMAR_HELP: readonly string[] = [
|
|
39
|
-
'Usage: hydranium-cli lint-grammar --services <module> [--name-property <p>]... [--strict] [--json] [--log-level <lvl>]',
|
|
41
|
+
'Usage: hydranium-cli lint-grammar --services <module> [--import <specifier>]... [--name-property <p>]... [--strict] [--json] [--log-level <lvl>] [--log-file <file>] [--log-file-level <lvl>]',
|
|
40
42
|
'',
|
|
41
43
|
"Check a head's grammar against the framework's conventions — a CI gate: the",
|
|
42
44
|
'process exits non-zero when a violation is found. Read-only: no workspace is',
|
|
@@ -48,25 +50,27 @@ export const LINT_GRAMMAR_HELP: readonly string[] = [
|
|
|
48
50
|
'',
|
|
49
51
|
'Options:',
|
|
50
52
|
' --services <module> ESM module exporting `createServices(): { shared }` (required).',
|
|
53
|
+
...importHelpLines(23),
|
|
51
54
|
' --name-property <p> Property that satisfies the nameability convention',
|
|
52
55
|
' (repeatable). Default: name.',
|
|
53
56
|
' --strict Also fail (non-zero exit) on warnings, not only errors.',
|
|
54
57
|
' --json Emit the raw JSON result instead of the human report.',
|
|
55
|
-
|
|
58
|
+
...logHelpLines(23)
|
|
56
59
|
];
|
|
57
60
|
|
|
58
61
|
export function parseLintGrammarArgs(args: string[], onError: UsageError = exitWithUsage): LintGrammarCommandOptions {
|
|
59
|
-
const { servicesModule, options, values } = parseHarnessArgs(args, 'lint-grammar', VALUE_FLAGS, BOOL_FLAGS, {
|
|
62
|
+
const { servicesModule, imports, options, values } = parseHarnessArgs(args, 'lint-grammar', VALUE_FLAGS, BOOL_FLAGS, {
|
|
60
63
|
requireWorkspace: false,
|
|
61
64
|
repeatableValueFlags: REPEATABLE_VALUE_FLAGS,
|
|
62
65
|
onError
|
|
63
66
|
});
|
|
64
67
|
return {
|
|
65
68
|
servicesModule,
|
|
69
|
+
imports,
|
|
66
70
|
nameProperties: values['--name-property'],
|
|
67
71
|
strict: options['--strict'] === 'true',
|
|
68
72
|
json: options['--json'] === 'true',
|
|
69
|
-
|
|
73
|
+
...logOptions(options)
|
|
70
74
|
};
|
|
71
75
|
}
|
|
72
76
|
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* only spawned.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
import { describeError } from '@hydranium/protocol';
|
|
19
20
|
import { loadHeadlessContext } from './headless-harness.js';
|
|
20
21
|
import { formatLintReport, lintExitCode } from './lint-grammar-report.js';
|
|
21
22
|
|
|
@@ -78,6 +79,6 @@ async function main(argv: string[]): Promise<number> {
|
|
|
78
79
|
main(process.argv.slice(2))
|
|
79
80
|
.then(code => process.exit(code))
|
|
80
81
|
.catch((error: unknown) => {
|
|
81
|
-
console.error(
|
|
82
|
+
console.error(describeError(error));
|
|
82
83
|
process.exit(1);
|
|
83
84
|
});
|
|
@@ -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
|
}
|