@hydranium/cli 1.0.0-next.38 → 1.0.0-next.380
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 +102 -204
- package/bin/hydranium-cli.js +13 -0
- package/heap-analysis/heap/core/load.mjs +2 -2
- package/heap-analysis/heap/langium/langium.classifier.mjs +22 -11
- 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.d.ts.map +1 -1
- package/lib/commands/analyze-heap-args.js +1 -2
- 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 +4 -2
- 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-ast-builder.d.ts.map +1 -1
- package/lib/commands/generate-ast-builder.js +1 -10
- package/lib/commands/generate-ast-builder.js.map +1 -1
- 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-builder.d.ts +33 -0
- package/lib/commands/generate-transfer-builder.d.ts.map +1 -0
- package/lib/commands/generate-transfer-builder.js +86 -0
- package/lib/commands/generate-transfer-builder.js.map +1 -0
- package/lib/commands/generate-transfer-model-args.d.ts.map +1 -1
- package/lib/commands/generate-transfer-model-args.js +20 -5
- 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 +8 -3
- package/lib/commands/generate-transfer-model-config.js.map +1 -1
- package/lib/commands/generate-transfer-model.d.ts +40 -9
- package/lib/commands/generate-transfer-model.d.ts.map +1 -1
- package/lib/commands/generate-transfer-model.js +219 -67
- package/lib/commands/generate-transfer-model.js.map +1 -1
- package/lib/commands/generated-module-path.d.ts +16 -0
- package/lib/commands/generated-module-path.d.ts.map +1 -0
- package/lib/commands/generated-module-path.js +21 -0
- package/lib/commands/generated-module-path.js.map +1 -0
- 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 +71 -19
- package/lib/commands/headless-harness.js.map +1 -1
- package/lib/commands/init-args.d.ts +2 -0
- package/lib/commands/init-args.d.ts.map +1 -1
- package/lib/commands/init-args.js +74 -31
- package/lib/commands/init-args.js.map +1 -1
- package/lib/commands/init-hosts.d.ts +24 -0
- package/lib/commands/init-hosts.d.ts.map +1 -0
- package/lib/commands/init-hosts.js +37 -0
- package/lib/commands/init-hosts.js.map +1 -0
- 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 +26 -14
- package/lib/commands/init-templates.d.ts.map +1 -1
- package/lib/commands/init-templates.js +2989 -746
- package/lib/commands/init-templates.js.map +1 -1
- package/lib/commands/init-wizard.d.ts +3 -0
- package/lib/commands/init-wizard.d.ts.map +1 -1
- package/lib/commands/init-wizard.js +46 -31
- 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 +58 -22
- package/lib/commands/init.d.ts.map +1 -1
- package/lib/commands/init.js +206 -48
- 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 +6 -5
- package/lib/commands/query-args.js.map +1 -1
- package/lib/commands/query.d.ts +6 -5
- package/lib/commands/query.d.ts.map +1 -1
- package/lib/commands/query.js +5 -4
- 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 +90 -39
- package/src/cli.ts +7 -2
- package/src/commands/analyze-heap-args.ts +1 -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 +4 -2
- 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-ast-builder.ts +1 -11
- package/src/commands/generate-languages.ts +132 -0
- package/src/commands/generate-transfer-builder.ts +101 -0
- package/src/commands/generate-transfer-model-args.ts +20 -5
- package/src/commands/generate-transfer-model-config.ts +8 -3
- package/src/commands/generate-transfer-model.ts +291 -65
- package/src/commands/generated-module-path.ts +22 -0
- package/src/commands/harness-args.ts +99 -31
- package/src/commands/headless-harness.ts +89 -28
- package/src/commands/init-args.ts +76 -31
- package/src/commands/init-hosts.ts +51 -0
- package/src/commands/init-prompt.ts +5 -6
- package/src/commands/init-templates.ts +3161 -759
- package/src/commands/init-wizard.ts +55 -31
- package/src/commands/init-workspace.ts +31 -16
- package/src/commands/init.ts +266 -63
- 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 +6 -4
- package/src/commands/query.ts +9 -9
- 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/client/gitignore +3 -0
- package/templates/client/src/__GRAMMAR_ID__-diagram-module.ts +82 -0
- package/templates/editorconfig +15 -0
- package/templates/gitignore +1 -0
- package/templates/host-browser/esbuild.mjs +82 -0
- package/templates/host-browser/gitignore +4 -0
- package/templates/host-browser/hydranium-mark.svg +40 -0
- package/templates/host-browser/index.html +287 -0
- package/templates/host-browser/playwright.config.mts +24 -0
- package/templates/host-browser/src/data-status.ts +15 -0
- package/templates/host-browser/src/diagram-save.ts +60 -0
- package/templates/host-browser/src/diagram.ts +118 -0
- package/templates/host-browser/src/editor.ts +102 -0
- package/templates/host-browser/src/log.ts +14 -0
- package/templates/host-browser/src/monaco-editor-core-worker.d.ts +7 -0
- package/templates/host-browser/src/monaco-editor-worker.ts +10 -0
- package/templates/host-browser/src/posix-path-shim.ts +23 -0
- package/templates/host-browser/src/shown-file.ts +61 -0
- package/templates/host-browser/src/workspace.ts +5 -0
- package/templates/host-browser/tsconfig.e2e.json +16 -0
- package/templates/host-browser/tsconfig.page.json +16 -0
- package/templates/host-browser/tsconfig.worker.json +16 -0
- package/templates/host-theia-browser/esbuild.mjs +24 -0
- package/templates/host-theia-browser/gitignore +9 -0
- package/templates/host-theia-browser/playwright.config.mts +42 -0
- package/templates/host-theia-browser/start.mjs +50 -0
- package/templates/host-theia-browser/tsconfig.e2e.json +16 -0
- package/templates/host-theia-electron/esbuild.mjs +27 -0
- package/templates/host-theia-electron/gitignore +8 -0
- package/templates/host-theia-electron/playwright.config.mts +12 -0
- package/templates/host-theia-electron/start.mjs +47 -0
- package/templates/host-theia-electron/tsconfig.e2e.json +16 -0
- package/templates/host-vscode/esbuild.mjs +38 -0
- package/templates/host-vscode/gitignore +7 -0
- package/templates/host-vscode/playwright.config.mts +14 -0
- package/templates/host-vscode/src/__GRAMMAR_ID__-diagram-editor.ts +19 -0
- package/templates/host-vscode/src/show-model-command.ts +24 -0
- package/templates/host-vscode/src/webview/__GRAMMAR_ID__-diagram.ts +18 -0
- package/templates/host-vscode/src/webview/diagram.css +40 -0
- package/templates/host-vscode/start.mjs +22 -0
- package/templates/host-vscode/test/install-vscode.mts +21 -0
- package/templates/host-vscode/test/vscode-version.mts +10 -0
- package/templates/host-vscode/tsconfig.e2e.json +17 -0
- package/templates/host-vscode/tsconfig.json +19 -0
- package/templates/host-vscode/tsconfig.webview.json +17 -0
- package/templates/oxlintrc.json +4 -0
- package/templates/protocol/gitignore +3 -0
- package/templates/protocol/src/__GRAMMAR_ID__-diagram-types.ts +47 -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__/delete-operation-handler.ts +63 -0
- package/templates/server/src/glsp/__GRAMMAR_ID__/gmodel-factory.ts +144 -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/__GRAMMAR_ID__-validation.ts +30 -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 +388 -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/theia/gitignore +3 -0
- package/templates/theia/src/browser/__GRAMMAR_ID__-diagram-frontend-module.ts +65 -0
- package/templates/theia/src/browser/__PROJECT_ID__-connection-preload-module.ts +15 -0
- package/templates/theia/src/browser/__PROJECT_ID__-data-connection.ts +15 -0
- package/templates/theia/src/browser/__PROJECT_ID__-data-port.ts +11 -0
- package/templates/theia/src/browser/__PROJECT_ID__-glsp-client-contribution.ts +13 -0
- package/templates/theia/src/browser/__PROJECT_ID__-show-model-command.ts +51 -0
- package/templates/theia/src/common/__PROJECT_ID__-glsp-contribution.ts +3 -0
- package/templates/theia/src/node/__PROJECT_ID__-connection-backend-module.ts +12 -0
- package/templates/theia/src/node/__PROJECT_ID__-data-backend-module.ts +15 -0
- package/templates/theia/src/node/__PROJECT_ID__-glsp-backend-module.ts +16 -0
- package/templates/theia/style/diagram.css +20 -0
- package/templates/theia/tsconfig.json +24 -0
- package/templates/vscode-servers/gitignore +4 -0
- package/templates/vscode-servers/tsconfig.json +19 -0
- package/templates/workspace/nodes.__EXTENSION__ +5 -0
- package/templates/workspace/references.__EXTENSION__ +2 -0
package/src/commands/init.ts
CHANGED
|
@@ -7,17 +7,26 @@
|
|
|
7
7
|
* SPDX-License-Identifier: MIT
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
|
+
import { spawnSync } from 'node:child_process';
|
|
10
11
|
import * as fs from 'node:fs';
|
|
12
|
+
import { createRequire } from 'node:module';
|
|
11
13
|
import * as path from 'node:path';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
+
import { editingHeads, INIT_HOSTS, type InitHost, isInitHost, isTheiaHost } from './init-hosts.js';
|
|
15
|
+
import { buildInitTemplates, readFrameworkVersion, SCAFFOLD_NODE_MAJOR, workspaceRootPins } from './init-templates.js';
|
|
16
|
+
import { kebab } from './kebab.js';
|
|
17
|
+
import {
|
|
18
|
+
createNodeWorkspaceProbe,
|
|
19
|
+
detectWorkspace,
|
|
20
|
+
parseNpmVersion,
|
|
21
|
+
workspaceGlobCovers,
|
|
22
|
+
type JsonValue,
|
|
23
|
+
type WorkspaceDetection,
|
|
24
|
+
type WorkspaceProbe
|
|
25
|
+
} from './init-workspace.js';
|
|
14
26
|
|
|
15
27
|
/**
|
|
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.
|
|
28
|
+
* The protocol heads a scaffold can start, in composition order. Any non-empty
|
|
29
|
+
* set: without `lsp`, the socket heads bring the workspace up themselves.
|
|
21
30
|
*/
|
|
22
31
|
export const INIT_HEADS = ['lsp', 'data', 'glsp'] as const;
|
|
23
32
|
|
|
@@ -27,6 +36,48 @@ export type InitHead = (typeof INIT_HEADS)[number];
|
|
|
27
36
|
/** The heads a scaffold emits when `--heads` is not given. */
|
|
28
37
|
export const DEFAULT_INIT_HEADS: readonly InitHead[] = ['lsp', 'data'];
|
|
29
38
|
|
|
39
|
+
/** One package of a scaffolded project, in the directory of its name — see {@link initMembers}. */
|
|
40
|
+
export type InitMember = 'protocol' | 'server' | 'client' | 'vscode-servers' | 'theia' | `host-${InitHost}`;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The packages a scaffold with these heads and hosts emits, in the order a
|
|
44
|
+
* workspace lists them. `client` comes with the `glsp` head, which always
|
|
45
|
+
* brings a diagram, and each host is a package of its own. `vscode-servers`
|
|
46
|
+
* comes with the VS Code host, whose server launch it holds, and with a Theia
|
|
47
|
+
* application, which sideloads it; `theia` is the Theia extension the Theia
|
|
48
|
+
* applications run.
|
|
49
|
+
*/
|
|
50
|
+
export function initMembers(heads: readonly InitHead[], hosts: readonly InitHost[]): InitMember[] {
|
|
51
|
+
return [
|
|
52
|
+
'protocol',
|
|
53
|
+
'server',
|
|
54
|
+
...(heads.includes('glsp') ? (['client'] as const) : []),
|
|
55
|
+
...(hosts.includes('vscode') || hosts.some(isTheiaHost) ? (['vscode-servers'] as const) : []),
|
|
56
|
+
...(hosts.some(isTheiaHost) ? (['theia'] as const) : []),
|
|
57
|
+
...hosts.map(host => `host-${host}` as const)
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A member's path relative to the workspace root, and the `workspaces` entry already covering it. */
|
|
62
|
+
export interface InitMemberPlacement {
|
|
63
|
+
readonly path: string;
|
|
64
|
+
readonly coveredBy?: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Where each member lands in a detected workspace.
|
|
69
|
+
*
|
|
70
|
+
* Judged per member rather than for the project directory, which holds no
|
|
71
|
+
* package: a root listing its members one by one needs an entry for each,
|
|
72
|
+
* while a `packages/*` glob covers none of them one level deeper.
|
|
73
|
+
*/
|
|
74
|
+
export function memberPlacements(detection: WorkspaceDetection, members: readonly InitMember[]): readonly InitMemberPlacement[] {
|
|
75
|
+
return members.map(member => {
|
|
76
|
+
const memberPath = `${detection.targetPath}/${member}`;
|
|
77
|
+
return { path: memberPath, coveredBy: detection.workspaces.find(glob => workspaceGlobCovers(glob, memberPath)) };
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
30
81
|
/** True for a string naming one of {@link INIT_HEADS}. */
|
|
31
82
|
export function isInitHead(value: string): value is InitHead {
|
|
32
83
|
return (INIT_HEADS as readonly string[]).includes(value);
|
|
@@ -55,10 +106,9 @@ export interface InitGrammarSpec {
|
|
|
55
106
|
/**
|
|
56
107
|
* Scaffold a GLSP diagram for this grammar. Requires the `glsp` head.
|
|
57
108
|
*
|
|
58
|
-
* Per grammar rather than per project because
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* with no diagram sitting on the same server is the normal case.
|
|
109
|
+
* Per grammar rather than per project because the scaffolded diagram is typed
|
|
110
|
+
* over one grammar's model, and a grammar with no diagram sitting on the same
|
|
111
|
+
* server is the normal case.
|
|
62
112
|
*/
|
|
63
113
|
readonly diagram?: boolean;
|
|
64
114
|
}
|
|
@@ -128,10 +178,16 @@ export interface InitWorkspacePlacement {
|
|
|
128
178
|
readonly baseTsconfig?: string;
|
|
129
179
|
/** That tsconfig's `compilerOptions`, so the emitted one can omit what it would inherit unchanged. */
|
|
130
180
|
readonly baseCompilerOptions?: Readonly<Record<string, JsonValue>>;
|
|
131
|
-
/** The workspace's
|
|
181
|
+
/** The workspace's formatter `printWidth`, so emitted sources wrap where the repo wraps. */
|
|
132
182
|
readonly printWidth?: number;
|
|
133
183
|
/** Filename of the workspace's eslint config; its presence is what makes the scaffold emit a `lint` script. */
|
|
134
184
|
readonly eslintConfig?: string;
|
|
185
|
+
/** Filename of the workspace Oxlint config, when present. */
|
|
186
|
+
readonly oxlintConfig?: string;
|
|
187
|
+
/** The npm version the workspace root's `packageManager` declares; with {@link npmVersion} it decides the scaffold's `vitest` range. */
|
|
188
|
+
readonly packageManagerNpm?: string;
|
|
189
|
+
/** The npm running `init`, when known. npm does not enforce `packageManager`, so a declaration alone cannot lift `vitest`. */
|
|
190
|
+
readonly npmVersion?: string;
|
|
135
191
|
}
|
|
136
192
|
|
|
137
193
|
/**
|
|
@@ -185,8 +241,10 @@ export interface InitComposition {
|
|
|
185
241
|
* one data server per project, not one per language).
|
|
186
242
|
*/
|
|
187
243
|
readonly projectId: string;
|
|
188
|
-
/** The protocol heads to start,
|
|
244
|
+
/** The protocol heads to start, at least one. */
|
|
189
245
|
readonly heads: readonly InitHead[];
|
|
246
|
+
/** The host applications emitted around the server, each a package of its own. */
|
|
247
|
+
readonly hosts: readonly InitHost[];
|
|
190
248
|
/** The grammars, in the order given. The first is the configuration root. */
|
|
191
249
|
readonly grammars: readonly InitGrammarNames[];
|
|
192
250
|
/** How the emitted `package.json` presents itself, and where it sits. */
|
|
@@ -222,14 +280,15 @@ export interface InitCommandOptions {
|
|
|
222
280
|
readonly grammars?: readonly InitGrammarSpec[];
|
|
223
281
|
/** The protocol heads to start. Defaults to {@link DEFAULT_INIT_HEADS}. */
|
|
224
282
|
readonly heads?: readonly InitHead[];
|
|
283
|
+
/** The host applications to emit around the server. Defaults to none. */
|
|
284
|
+
readonly hosts?: readonly InitHost[];
|
|
225
285
|
/** Scaffold into a non-empty directory anyway. */
|
|
226
286
|
readonly force?: boolean;
|
|
227
287
|
/**
|
|
228
288
|
* Scaffold a member of the surrounding npm workspace: extend its base
|
|
229
|
-
* tsconfig
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
* project the caller did not ask for.
|
|
289
|
+
* tsconfig and address the package by `--prefix` in the scripts it prints
|
|
290
|
+
* back at itself. Errors when no ancestor declares `workspaces`, rather than
|
|
291
|
+
* silently degrading to a standalone project the caller did not ask for.
|
|
233
292
|
*/
|
|
234
293
|
readonly monorepo?: boolean;
|
|
235
294
|
/** npm scope for the package name, e.g. `@acme`. */
|
|
@@ -244,21 +303,26 @@ export interface InitCommandOptions {
|
|
|
244
303
|
readonly __writeFilesForTest?: (targetDir: string, files: readonly InitFile[]) => void;
|
|
245
304
|
}
|
|
246
305
|
|
|
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
306
|
/** True for a PascalCase identifier legal as a generated TypeScript symbol prefix. */
|
|
253
307
|
function isPascalIdentifier(value: string): boolean {
|
|
254
308
|
return /^[A-Za-z][A-Za-z0-9]*$/.test(value);
|
|
255
309
|
}
|
|
256
310
|
|
|
257
|
-
/**
|
|
311
|
+
/**
|
|
312
|
+
* True for a file extension, without its leading dot, that is safe in an
|
|
313
|
+
* emitted file name and string literal: letters, digits and dashes.
|
|
314
|
+
*/
|
|
315
|
+
export function isUsableExtension(extension: string): boolean {
|
|
316
|
+
return /^[A-Za-z0-9][A-Za-z0-9-]*$/.test(extension);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Strip a leading dot and reject an extension {@link isUsableExtension} refuses. */
|
|
258
320
|
function normalizeExtension(value: string, grammar: string): string {
|
|
259
321
|
const extension = value.replace(/^\./, '');
|
|
260
|
-
if (extension
|
|
261
|
-
throw new Error(
|
|
322
|
+
if (!isUsableExtension(extension)) {
|
|
323
|
+
throw new Error(
|
|
324
|
+
`Invalid extension '${value}' for grammar '${grammar}': use letters, digits and dashes, starting with a letter or digit.`
|
|
325
|
+
);
|
|
262
326
|
}
|
|
263
327
|
return extension;
|
|
264
328
|
}
|
|
@@ -282,6 +346,11 @@ function resolveGrammar(spec: InitGrammarSpec, projectId: string, qualify: boole
|
|
|
282
346
|
if (extensions.length === 0) {
|
|
283
347
|
throw new Error(`Invalid --extensions for grammar '${spec.name}': at least one extension is required.`);
|
|
284
348
|
}
|
|
349
|
+
if (spec.languageId !== undefined && !/^[a-z0-9][a-z0-9-]*$/.test(spec.languageId)) {
|
|
350
|
+
throw new Error(
|
|
351
|
+
`Invalid --language-id '${spec.languageId}' for grammar '${spec.name}': use lowercase letters, digits and dashes, e.g. 'my-lang'.`
|
|
352
|
+
);
|
|
353
|
+
}
|
|
285
354
|
return {
|
|
286
355
|
grammar: spec.name,
|
|
287
356
|
grammarId,
|
|
@@ -298,8 +367,9 @@ function resolveGrammar(spec: InitGrammarSpec, projectId: string, qualify: boole
|
|
|
298
367
|
*
|
|
299
368
|
* With the `glsp` head on and nothing marked, a lone grammar is unambiguous and
|
|
300
369
|
* is taken as the diagram's; with several it is a real choice, so it is required
|
|
301
|
-
* rather than guessed —
|
|
302
|
-
*
|
|
370
|
+
* rather than guessed — the scaffolded state, storage and GModel factory are
|
|
371
|
+
* typed over the chosen grammar's model, so a wrong guess emits a diagram of
|
|
372
|
+
* the wrong model.
|
|
303
373
|
*/
|
|
304
374
|
function resolveDiagrams(specs: readonly InitGrammarSpec[], heads: readonly InitHead[]): boolean[] {
|
|
305
375
|
const marked = specs.map(spec => spec.diagram === true);
|
|
@@ -325,23 +395,34 @@ function resolveDiagrams(specs: readonly InitGrammarSpec[], heads: readonly Init
|
|
|
325
395
|
);
|
|
326
396
|
}
|
|
327
397
|
|
|
328
|
-
/** Validate the head set: known names only
|
|
398
|
+
/** Validate the head set: known names only. */
|
|
329
399
|
function resolveHeads(heads: readonly InitHead[] | undefined): readonly InitHead[] {
|
|
330
400
|
const resolved = heads === undefined || heads.length === 0 ? DEFAULT_INIT_HEADS : heads;
|
|
331
401
|
const unknown = resolved.filter(head => !isInitHead(head));
|
|
332
402
|
if (unknown.length > 0) {
|
|
333
403
|
throw new Error(`Unknown head(s) '${unknown.join(', ')}': expected one of ${INIT_HEADS.join(', ')}.`);
|
|
334
404
|
}
|
|
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
405
|
// De-duplicated and put back in composition order, so the order the heads
|
|
341
406
|
// were named in does not change the emitted project.
|
|
342
407
|
return INIT_HEADS.filter(head => resolved.includes(head));
|
|
343
408
|
}
|
|
344
409
|
|
|
410
|
+
/** Validate the host set against the heads: known names only, and one of the heads each host edits through. */
|
|
411
|
+
function resolveHosts(hosts: readonly InitHost[], heads: readonly InitHead[]): readonly InitHost[] {
|
|
412
|
+
const unknown = hosts.filter(host => !isInitHost(host));
|
|
413
|
+
if (unknown.length > 0) {
|
|
414
|
+
throw new Error(`Unknown host(s) '${unknown.join(', ')}': expected one of ${INIT_HOSTS.join(', ')}.`);
|
|
415
|
+
}
|
|
416
|
+
for (const host of hosts) {
|
|
417
|
+
const editing = editingHeads(host);
|
|
418
|
+
if (!editing.some(head => heads.includes(head))) {
|
|
419
|
+
const needed = editing.length === 1 ? `the '${editing[0]}' head` : `one of the '${editing.join(', ')}' heads`;
|
|
420
|
+
throw new Error(`--hosts ${host} needs ${needed}, but the head set is '${heads.join(',')}'.`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return INIT_HOSTS.filter(host => hosts.includes(host));
|
|
424
|
+
}
|
|
425
|
+
|
|
345
426
|
/**
|
|
346
427
|
* Reject the collisions that would otherwise surface as a confusing generator
|
|
347
428
|
* or runtime failure rather than as a bad invocation.
|
|
@@ -368,15 +449,18 @@ function assertNoCollisions(grammars: readonly InitGrammarNames[]): void {
|
|
|
368
449
|
}
|
|
369
450
|
byLanguageId.set(grammar.languageId, grammar.grammar);
|
|
370
451
|
|
|
452
|
+
// Case-folded: a starter file is named by extension, and `nodes.Foo` and
|
|
453
|
+
// `nodes.foo` are one file on a case-insensitive file system.
|
|
371
454
|
for (const extension of grammar.extensions) {
|
|
372
|
-
const
|
|
455
|
+
const key = extension.toLowerCase();
|
|
456
|
+
const clashingExtension = byExtension.get(key);
|
|
373
457
|
if (clashingExtension !== undefined) {
|
|
374
458
|
throw new Error(
|
|
375
459
|
`File extension '.${extension}' is claimed by both '${clashingExtension}' and '${grammar.grammar}'. ` +
|
|
376
460
|
'Langium routes documents by extension, so each grammar needs its own.'
|
|
377
461
|
);
|
|
378
462
|
}
|
|
379
|
-
byExtension.set(
|
|
463
|
+
byExtension.set(key, grammar.grammar);
|
|
380
464
|
}
|
|
381
465
|
}
|
|
382
466
|
}
|
|
@@ -393,7 +477,8 @@ export function resolveInitComposition(
|
|
|
393
477
|
name: string,
|
|
394
478
|
grammars: readonly InitGrammarSpec[] = [],
|
|
395
479
|
heads?: readonly InitHead[],
|
|
396
|
-
packaging: InitPackaging = STANDALONE_PACKAGING
|
|
480
|
+
packaging: InitPackaging = STANDALONE_PACKAGING,
|
|
481
|
+
hosts: readonly InitHost[] = []
|
|
397
482
|
): InitComposition {
|
|
398
483
|
if (!isPascalIdentifier(name)) {
|
|
399
484
|
throw new Error(
|
|
@@ -405,11 +490,20 @@ export function resolveInitComposition(
|
|
|
405
490
|
throw new Error(`Invalid --scope '${packaging.scope}': an npm scope starts with '@', e.g. '@acme'.`);
|
|
406
491
|
}
|
|
407
492
|
const resolvedHeads = resolveHeads(heads);
|
|
493
|
+
const resolvedHosts = resolveHosts(hosts, resolvedHeads);
|
|
408
494
|
const specs = grammars.length > 0 ? grammars : [{ name }];
|
|
409
495
|
const diagrams = resolveDiagrams(specs, resolvedHeads);
|
|
410
496
|
const resolved = specs.map((spec, index) => resolveGrammar(spec, projectId, specs.length > 1, diagrams[index]));
|
|
411
497
|
assertNoCollisions(resolved);
|
|
412
|
-
return {
|
|
498
|
+
return {
|
|
499
|
+
name,
|
|
500
|
+
projectId,
|
|
501
|
+
heads: resolvedHeads,
|
|
502
|
+
hosts: resolvedHosts,
|
|
503
|
+
grammars: resolved,
|
|
504
|
+
packaging,
|
|
505
|
+
frameworkVersion: readFrameworkVersion()
|
|
506
|
+
};
|
|
413
507
|
}
|
|
414
508
|
|
|
415
509
|
/**
|
|
@@ -432,6 +526,8 @@ export interface InitPackagingOptions {
|
|
|
432
526
|
readonly scope?: string;
|
|
433
527
|
readonly public?: boolean;
|
|
434
528
|
readonly probe?: WorkspaceProbe;
|
|
529
|
+
/** The npm running `init`. Defaults to the one `npm_config_user_agent` names, which `npx` and `npm run` set. */
|
|
530
|
+
readonly npmVersion?: string;
|
|
435
531
|
}
|
|
436
532
|
|
|
437
533
|
/**
|
|
@@ -466,24 +562,61 @@ export function resolveInitPackaging(targetDir: string, options: InitPackagingOp
|
|
|
466
562
|
baseTsconfig: detection.baseTsconfig,
|
|
467
563
|
baseCompilerOptions: detection.baseCompilerOptions,
|
|
468
564
|
printWidth: detection.printWidth,
|
|
469
|
-
eslintConfig: detection.eslintConfig
|
|
565
|
+
eslintConfig: detection.eslintConfig,
|
|
566
|
+
oxlintConfig: detection.oxlintConfig,
|
|
567
|
+
packageManagerNpm: detection.packageManagerNpm,
|
|
568
|
+
npmVersion: options.npmVersion ?? parseNpmVersion(process.env.npm_config_user_agent)
|
|
470
569
|
}
|
|
471
570
|
};
|
|
472
571
|
}
|
|
473
572
|
|
|
573
|
+
/**
|
|
574
|
+
* Format a standalone scaffold as its own `format` script would, with the
|
|
575
|
+
* `oxfmt` its root pins and the config it emits. A template is written at one
|
|
576
|
+
* length of the names its tokens stand for, so only a formatter run lets every
|
|
577
|
+
* project name pass `format:check`. Only the written files, because `--force`
|
|
578
|
+
* scaffolds beside files that are not ours to restyle. A failure leaves the
|
|
579
|
+
* files as written, which build all the same, and says so.
|
|
580
|
+
*/
|
|
581
|
+
function formatScaffold(targetDir: string, files: readonly InitFile[], write: (line: string) => void): void {
|
|
582
|
+
let reason: string | undefined;
|
|
583
|
+
try {
|
|
584
|
+
// Not `findPackageJSON`: Node 22.13, which `engines` admits, lacks it.
|
|
585
|
+
const manifestPath = createRequire(import.meta.url).resolve('oxfmt/package.json');
|
|
586
|
+
const bin = (JSON.parse(fs.readFileSync(manifestPath, 'utf-8')) as { bin: Record<string, string> }).bin.oxfmt;
|
|
587
|
+
const args = [path.join(path.dirname(manifestPath), bin), ...files.map(file => file.path)];
|
|
588
|
+
const result = spawnSync(process.execPath, args, { cwd: targetDir, encoding: 'utf-8' });
|
|
589
|
+
if (result.status !== 0) {
|
|
590
|
+
reason = result.error?.message ?? `${result.stdout}${result.stderr}${result.signal === null ? '' : `killed by ${result.signal}`}`;
|
|
591
|
+
}
|
|
592
|
+
} catch (error: unknown) {
|
|
593
|
+
reason = error instanceof Error ? error.message : String(error);
|
|
594
|
+
}
|
|
595
|
+
if (reason !== undefined) {
|
|
596
|
+
write(`Warning: oxfmt did not format the scaffold, so run \`npm run format\` after \`npm install\`:\n${reason.trim()}\n`);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
474
600
|
/**
|
|
475
601
|
* Scaffold a new Hydranium language project end-to-end: one starter grammar per
|
|
476
602
|
* `--grammar`, `create<Name>Services` DI wiring, a launcher for each head in
|
|
477
603
|
* `--heads`, `langium-config.json`, `package.json`, and scripts. Does NOT run `npm install`
|
|
478
604
|
* or `langium generate` — those are printed as next steps, so the command stays
|
|
479
|
-
* offline and
|
|
480
|
-
* target directory unless
|
|
605
|
+
* offline and changes nothing beyond the files it writes, which a standalone
|
|
606
|
+
* scaffold's oxfmt then formats. Refuses a non-empty target directory unless
|
|
607
|
+
* `force`.
|
|
481
608
|
*/
|
|
482
609
|
export function runInit(options: InitCommandOptions): void {
|
|
483
610
|
const write = options.write ?? ((line: string) => process.stdout.write(line));
|
|
484
611
|
const targetDir = path.resolve(options.targetDir);
|
|
485
612
|
const detection = options.monorepo ? detectWorkspace(targetDir, options.probe ?? createNodeWorkspaceProbe()) : undefined;
|
|
486
|
-
const composition = resolveInitComposition(
|
|
613
|
+
const composition = resolveInitComposition(
|
|
614
|
+
options.name,
|
|
615
|
+
options.grammars,
|
|
616
|
+
options.heads,
|
|
617
|
+
resolveInitPackaging(targetDir, options),
|
|
618
|
+
options.hosts
|
|
619
|
+
);
|
|
487
620
|
|
|
488
621
|
if (!options.force && isNonEmptyDir(targetDir)) {
|
|
489
622
|
throw new Error(`Target directory '${targetDir}' is not empty. Pass --force to scaffold into it anyway.`);
|
|
@@ -498,56 +631,126 @@ export function runInit(options: InitCommandOptions): void {
|
|
|
498
631
|
fs.mkdirSync(path.dirname(absolute), { recursive: true });
|
|
499
632
|
fs.writeFileSync(absolute, file.content, 'utf-8');
|
|
500
633
|
}
|
|
634
|
+
if (composition.packaging.workspace === undefined) {
|
|
635
|
+
formatScaffold(targetDir, files, write);
|
|
636
|
+
}
|
|
501
637
|
}
|
|
502
638
|
|
|
503
639
|
const grammarList = composition.grammars.map(grammar => (grammar.diagram ? `${grammar.grammar} (diagram)` : grammar.grammar)).join(', ');
|
|
504
640
|
write(
|
|
505
641
|
`Scaffolded ${composition.name} (${files.length} files, heads: ${composition.heads.join(',')}, ` +
|
|
642
|
+
(composition.hosts.length > 0 ? `hosts: ${composition.hosts.join(',')}, ` : '') +
|
|
506
643
|
`${composition.grammars.length} grammar(s): ${grammarList}) into ${targetDir}\n`
|
|
507
644
|
);
|
|
508
645
|
if (detection !== undefined) {
|
|
509
646
|
write('\n');
|
|
510
647
|
write(`Workspace root: ${detection.rootDir}\n`);
|
|
511
648
|
if (detection.baseTsconfig !== undefined) {
|
|
512
|
-
write(
|
|
649
|
+
write(
|
|
650
|
+
` Each library package's tsconfig.json extends the root's ${path.posix.basename(detection.baseTsconfig)}, minus the options it inherits unchanged.\n`
|
|
651
|
+
);
|
|
652
|
+
if (composition.hosts.length > 0) {
|
|
653
|
+
// A host's projects compile its page, worker and tests apart, each under options of its own.
|
|
654
|
+
const members = initMembers(composition.heads, composition.hosts);
|
|
655
|
+
const extensions = [
|
|
656
|
+
...(members.includes('vscode-servers') ? ["the VS Code servers extension's"] : []),
|
|
657
|
+
...(members.includes('theia') ? ["the Theia extension's"] : [])
|
|
658
|
+
];
|
|
659
|
+
const also = extensions.length === 0 ? '' : `, as ${extensions.join(' and ')} ${extensions.length === 1 ? 'does' : 'do'}`;
|
|
660
|
+
write(` Each host's tsconfigs stand alone${also}.\n`);
|
|
661
|
+
}
|
|
513
662
|
} else {
|
|
514
|
-
write(
|
|
663
|
+
write(" No root tsconfig carries compilerOptions, so each package's stands alone.\n");
|
|
515
664
|
}
|
|
516
|
-
write(" .gitignore holds `syntaxes/` only — the workspace root's covers the rest.\n");
|
|
517
665
|
if (detection.printWidth !== undefined) {
|
|
518
|
-
write(` Sources wrapped at ${detection.printWidth} columns, from ${detection.rootDir}'s
|
|
666
|
+
write(` Sources wrapped at ${detection.printWidth} columns, from ${detection.rootDir}'s formatter config.\n`);
|
|
519
667
|
}
|
|
520
|
-
if (detection.eslintConfig !== undefined) {
|
|
521
|
-
write(
|
|
668
|
+
if (detection.oxlintConfig !== undefined || detection.eslintConfig !== undefined) {
|
|
669
|
+
write(
|
|
670
|
+
` Each package got a lint script — ${detection.oxlintConfig ?? detection.eslintConfig} says this repo lints. Adjust them if your invocation differs.\n`
|
|
671
|
+
);
|
|
522
672
|
}
|
|
523
673
|
// Printed rather than applied: `init` writes inside the target directory
|
|
524
674
|
// and nowhere else, so a scaffold can never surprise-edit a manifest that
|
|
525
675
|
// is under review, mid-rebase, or simply not the caller's to change.
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
write(
|
|
676
|
+
const placements = memberPlacements(detection, initMembers(composition.heads, composition.hosts));
|
|
677
|
+
const uncovered = placements.filter(placement => placement.coveredBy === undefined);
|
|
678
|
+
if (uncovered.length > 0) {
|
|
679
|
+
write(` Add ${uncovered.length === 1 ? 'this' : 'these'} to "workspaces" in ${path.join(detection.rootDir, 'package.json')}:\n`);
|
|
680
|
+
write(`${uncovered.map(placement => ` "${placement.path}"`).join(',\n')}\n`);
|
|
681
|
+
}
|
|
682
|
+
for (const placement of placements.filter(candidate => candidate.coveredBy !== undefined)) {
|
|
683
|
+
write(` ${placement.path} is already covered by the "${placement.coveredBy}" workspaces entry.\n`);
|
|
531
684
|
}
|
|
685
|
+
const pins = workspaceRootPins(composition).map(([name, version]) => ` "${name}": "${version}"`);
|
|
686
|
+
write(
|
|
687
|
+
` Add these to "devDependencies" in ${path.join(detection.rootDir, 'package.json')}, so another member's version of one ` +
|
|
688
|
+
`cannot displace the copy these packages share with the framework:\n${pins.join(',\n')}\n`
|
|
689
|
+
);
|
|
532
690
|
if (composition.packaging.scope === undefined && detection.scope !== undefined) {
|
|
533
691
|
write(` Sibling packages all use the "${detection.scope}" scope; pass --scope ${detection.scope} to match them.\n`);
|
|
534
692
|
}
|
|
535
693
|
}
|
|
536
694
|
write('\n');
|
|
537
695
|
write('Next steps:\n');
|
|
538
|
-
|
|
539
|
-
//
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
696
|
+
// A workspace member's project has no manifest of its own, so its steps run
|
|
697
|
+
// from the workspace root and name the server by `--prefix`.
|
|
698
|
+
const server = detection === undefined ? 'server' : `${detection.targetPath}/server`;
|
|
699
|
+
if (detection === undefined) {
|
|
700
|
+
write(` cd ${options.targetDir}\n`);
|
|
701
|
+
// oxfmt and oxlint skip whatever an enclosing repository ignores, this
|
|
702
|
+
// folder included: https://github.com/oxc-project/oxc/issues/25603
|
|
703
|
+
if (spawnSync('git', ['check-ignore', '-q', targetDir], { cwd: targetDir }).status === 0) {
|
|
704
|
+
write(' git init # an enclosing repository ignores this folder, so lint and format find no file until then\n');
|
|
705
|
+
}
|
|
706
|
+
// A comment, so a pasted block runs without nvm. NVM for Windows ignores
|
|
707
|
+
// `.nvmrc` and its `install` does not switch, hence both commands.
|
|
708
|
+
write(
|
|
709
|
+
` # on Node ${SCAFFOLD_NODE_MAJOR}, which .nvmrc names. With nvm: nvm install ${SCAFFOLD_NODE_MAJOR}, then nvm use ${SCAFFOLD_NODE_MAJOR}\n`
|
|
710
|
+
);
|
|
711
|
+
write(' npm install\n');
|
|
712
|
+
write(' npm run build # langium generate + tsc, every package\n');
|
|
713
|
+
write(' npm test # the server tests\n');
|
|
714
|
+
write(' npm run watch # rebuild the members on every change; watch:<host> adds a host\n');
|
|
715
|
+
} else {
|
|
716
|
+
write(` cd ${detection.rootDir}\n`);
|
|
717
|
+
write(' npm install\n');
|
|
718
|
+
write(` npm --prefix ${server} run build # langium generate + tsc, protocol included\n`);
|
|
719
|
+
// The server's build reaches protocol through its project reference and
|
|
720
|
+
// nothing else, so each further member is built on its own.
|
|
721
|
+
for (const member of initMembers(composition.heads, composition.hosts).filter(name => name !== 'protocol' && name !== 'server')) {
|
|
722
|
+
write(` npm --prefix ${detection.targetPath}/${member} run build\n`);
|
|
723
|
+
}
|
|
724
|
+
write(` npm --prefix ${server} test\n`);
|
|
725
|
+
}
|
|
548
726
|
// `npx`, not a bare invocation: `@hydranium/cli` is a devDependency of the
|
|
549
727
|
// scaffold, so the binary is on PATH inside an npm script and nowhere else.
|
|
550
|
-
write(
|
|
728
|
+
write(` npx hydranium-cli reflect --services ./${server}/lib/node/services.js\n`);
|
|
729
|
+
const hostDir = (host: InitHost): string => (detection === undefined ? `host-${host}` : `${detection.targetPath}/host-${host}`);
|
|
730
|
+
// A standalone root runs a host as `start:<host>`. A surrounding workspace's root is not ours to add to.
|
|
731
|
+
const start = (host: InitHost): string => (detection === undefined ? `npm run start:${host}` : `npm --prefix ${hostDir(host)} start`);
|
|
732
|
+
if (composition.hosts.includes('browser')) {
|
|
733
|
+
const browser = hostDir('browser');
|
|
734
|
+
write(` ${start('browser')} # serve the page at http://localhost:4937, Ctrl+C to stop\n`);
|
|
735
|
+
// The smoke test serves the page itself, on the same port.
|
|
736
|
+
write(` npm --prefix ${browser} run test:e2e:install && npm --prefix ${browser} run test:e2e # with start stopped\n`);
|
|
737
|
+
}
|
|
738
|
+
if (composition.hosts.includes('vscode')) {
|
|
739
|
+
const vscode = hostDir('vscode');
|
|
740
|
+
write(` ${start('vscode')} # VS Code with the extension, on the workspace (needs \`code\` on the PATH)\n`);
|
|
741
|
+
write(` npm --prefix ${vscode} run test:e2e:install && npm --prefix ${vscode} run test:e2e\n`);
|
|
742
|
+
}
|
|
743
|
+
if (composition.hosts.includes('theia-browser')) {
|
|
744
|
+
const theia = hostDir('theia-browser');
|
|
745
|
+
write(` ${start('theia-browser')} # Theia at http://localhost:3000 on the workspace, Ctrl+C to stop\n`);
|
|
746
|
+
// The smoke test starts its own backend, on a port of its own.
|
|
747
|
+
write(` npm --prefix ${theia} run test:e2e:install && npm --prefix ${theia} run test:e2e\n`);
|
|
748
|
+
}
|
|
749
|
+
if (composition.hosts.includes('theia-electron')) {
|
|
750
|
+
const electron = hostDir('theia-electron');
|
|
751
|
+
write(` ${start('theia-electron')} # the Theia desktop application on the workspace\n`);
|
|
752
|
+
write(` npm --prefix ${electron} run test:e2e\n`);
|
|
753
|
+
}
|
|
551
754
|
write('\n');
|
|
552
755
|
write('The emitted sources carry no license header — run your own header tool if\n');
|
|
553
756
|
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
|
}
|