Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/theia-compat.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ on:
- 'packages/client-theia/**'
- 'packages/data-client-theia/**'
- 'packages/glsp-client-theia/**'
# Loaded by glsp-client-theia's frontend, so a change here is a change
# to what Theia loads.
- 'packages/glsp-client/**'
- '.github/workflows/theia-compat.yml'
# A README in those packages changes nothing Theia loads.
- '!**/*.md'
Expand Down
9 changes: 5 additions & 4 deletions docs/adopting/packages.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,17 @@ reads `<head>-<role>-<platform>`, and a name with no head spans all heads.
| **Client integrations** | | |
| [`@hydranium/client-theia`](../../packages/client-theia/README.md) | your host is Theia | What every head needs in a Theia application: logging, diagnostics and connection handling. |
| [`@hydranium/data-client-theia`](../../packages/data-client-theia/README.md) | …and you run the data head | The data head's client side in Theia. |
| [`@hydranium/glsp-client-theia`](../../packages/glsp-client-theia/README.md) | …and you run the GLSP head | The GLSP head's client side in Theia, including the diagram widget. |
| [`@hydranium/glsp-client`](../../packages/glsp-client/README.md) | you run the GLSP head, whatever the host | The GLSP head's client side for any host: the diagram classes and the binder that mounts them. |
| [`@hydranium/glsp-client-theia`](../../packages/glsp-client-theia/README.md) | …and your host is Theia | The GLSP head's client side in Theia, including the diagram widget. |
| [`@hydranium/client-browser`](../../packages/client-browser/README.md) | your host is a web page | The page side of a host whose heads run in a web worker: the worker bootstrap, the LSP handshake and the data head's port. |
| **Tooling** | | |
| [`@hydranium/cli`](../../packages/cli/README.md) | always, as a development dependency | `hydranium-cli`: scaffold a project, inspect a grammar, validate models headlessly, generate the transfer model, and drive a data server. |
| [`@hydranium/conformance`](../../packages/conformance/README.md) | you want to check your server against the protocol | A conformance suite you run against your own server, in any test runner. |
| [`@hydranium/langium`](../../packages/langium/README.md) | you import Langium at all | The one route to Langium, so your install has exactly one copy of it. |

For VS Code there is no client package; copy the client code from the
order-flow example. In a web page, the text editor and the diagram are still
copied from it.
For VS Code there is no client package beyond the diagram's; copy the rest of
the client code from the order-flow example. In a web page, the text editor is
still copied from it.

## Entry points

Expand Down
19 changes: 12 additions & 7 deletions docs/contributing/conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,15 @@ fills, never a static `node:` import. Why the split exists:
### A host-integration package's root barrel follows its upstream

The neutral-root rule binds the four head-neutral packages. **A
`packages/*-theia` root barrel matches the root convention of the upstream it
integrates**, judged by what an adopter importing the bare specifier is
entitled to get: re-export the browser tier where the upstream's root is its
browser tier, and declare nothing (`export {}`) where the upstream has no root
surface. A module belongs on `common/` only when its whole dependency closure,
`packages/*-theia` root barrel, and `glsp-client`'s, matches the root
convention of the upstream it integrates**, judged by what an adopter importing
the bare specifier is entitled to get: re-export the browser tier where the
upstream's root is its browser tier, and declare nothing (`export {}`) where
the upstream has no root surface. `glsp-client` carries no platform in its
name because every GLSP host is a browser, and its root is browser-bound as
`@eclipse-glsp/client`'s is: it renders and loads stylesheets, so it needs a
bundler with a CSS loader, and `check:neutral` gates it for Node imports
only. A module belongs on `common/` only when its whole dependency closure,
type-only imports included, is neutral or Theia's common tier; create the tier
when a module needs it. A root re-exporting a browser tier loads only in a
frontend build, so its declaration comment must not call it neutral. **A
Expand Down Expand Up @@ -164,8 +168,9 @@ overload set. An empty interface is legitimate when the whole surface is
`protected`; its oxlint `no-empty-object-type` disable carries the reason. Type
parameters go where they are honest: per-URI members, not a cross-grammar slot.

**GLSP DI tokens** live in the `HydraniumTypes` registry, so a token's name
cannot collide with the type it injects. No language or per-language service
**GLSP DI tokens** live in a registry, `HydraniumTypes` on the server and
`HydraniumClientTypes` on the client, so a token's name cannot collide with the
type it injects. No language or per-language service
gets a token: a diagram's language is its document's, and
`modelState.<role>For(node)` keeps nodes from other documents on their own
grammar.
Expand Down
17 changes: 11 additions & 6 deletions docs/guides/host-in-a-browser.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ its worker launcher. Run all heads in one worker, so they share one workspace.
imports it for a seam a browser never calls. Alias nothing else. A real Node
import anywhere then fails your build, which is the check you want, rather
than failing at runtime in a worker with no console. If you mount a diagram,
add `loader: { '.ttf': 'dataurl' }` for GLSP's font.
add `loader: { '.ttf': 'dataurl' }` for GLSP's font. If you minify, keep
class names (`keepNames`): the log tags the framework's and sprotty's
classes by name.

2. **Give every head its own `MessagePort`.** In the page, build the `Worker`
and hand it to `connectWorkerHeads` from `@hydranium/client-browser`, which
Expand Down Expand Up @@ -134,11 +136,14 @@ const fileSystem = await persistentFileSystem({
before the connection listens, so you also see the first build, and keep
only the newest lines. See `src/page/log-panel.ts`.

8. **Mount the diagram.** Connect GLSP's `BaseJsonrpcGLSPClient` over the GLSP
head's connection from `connectWorkerHeads`, wrapped with
`sendByMethodName` from `@hydranium/protocol`. Upstream's
`GLSPWebWorkerProvider` starts a worker of its own, so it is not the piece
to use. Then supply what a shell would:
8. **Mount the diagram.** Connect `HydraniumGlspClient` from
`@hydranium/glsp-client` over the GLSP head's connection from
`connectWorkerHeads`, wrapped with `sendByMethodName` from
`@hydranium/protocol`. Upstream's `GLSPWebWorkerProvider` starts a worker of
its own, so it is not the piece to use. Add a container module after
`STANDALONE_MODULE_CONFIG` that calls `createGlspClientModule` with your
logger, so a failed load and sprotty's own errors reach it instead of
vanishing. Then supply what a shell would:
- size the `<svg>` to its mount, as in `#mount > svg { width: 100%; height:
100% }`, or the diagram draws only what fits in 300×150;
- give the mount a height and `position: relative`, so the tool palette
Expand Down
3 changes: 2 additions & 1 deletion docs/guides/host-in-theia.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ them.
- `@hydranium/client-theia`: what every head shares, such as the output-channel
logger, the editor's save handling, and the backend's socket bridge;
- `@hydranium/data-client-theia`: the data head's connection;
- `@hydranium/glsp-client-theia`: the diagram.
- `@hydranium/glsp-client-theia`: the diagram, over `@hydranium/glsp-client`,
whose classes it binds for you.

Each head gets a frontend module and a backend module. The backend forwards
the frontend's channel to the head's socket on the server, so the server runs
Expand Down
2 changes: 1 addition & 1 deletion docs/img/architecture.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion examples/order-flow/browser/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ npm --prefix examples/order-flow/browser run test:e2e
```

It asserts all three heads against the oracles' document, diagnostic, node and
edge counts, and that the console stays silent. It asserts that a drag and a
edge counts, and that the console carries nothing but the GLSP client's
model-initialized timing. It asserts that a drag and a
palette create reach the `.layout` document, read back through the data head
rather than from the rendered diagram, since sprotty draws a dragged node at
the drop point whether or not the write landed. Further Playwright projects
Expand Down
1 change: 1 addition & 0 deletions examples/order-flow/browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@hydranium/data-server": "^1.0.0-next",
"@hydranium/example-order-flow-client": "0.0.0",
"@hydranium/example-order-flow-server": "0.0.0",
"@hydranium/glsp-client": "^1.0.0-next",
"@hydranium/glsp-server": "^1.0.0-next",
"@hydranium/langium": "^1.0.0-next",
"@hydranium/protocol": "^1.0.0-next",
Expand Down
40 changes: 26 additions & 14 deletions examples/order-flow/browser/src/page/process-diagram.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@
* # What replaces the shell
*
* A Theia or VS Code integration contributes a `GLSPClient` and a widget. Here
* `STANDALONE_MODULE_CONFIG` supplies GLSP's own plain-webapp modules and the
* client is built by hand over the head's connection — one line, because
* `BaseJsonrpcGLSPClient` takes any `ConnectionProvider` and the connection
* `@hydranium/client-browser` hands out over the GLSP port is one, wrapped
* with `sendByMethodName`.
* `STANDALONE_MODULE_CONFIG` supplies GLSP's own plain-webapp modules, the
* framework's `createGlspClientModule` binds its classes over the page's
* logger, and the client is built by hand over the head's connection — one
* line, because `HydraniumGlspClient` takes any `ConnectionProvider` and the
* connection `@hydranium/client-browser` hands out over the GLSP port is one,
* wrapped with `sendByMethodName`.
*
* Upstream's `GLSPWebWorkerProvider` looks like the piece for this and is not:
* it constructs the worker itself, which would be a second worker with a second
Expand All @@ -45,11 +46,11 @@ import {
TYPES,
createDiagramOptionsModule
} from '@eclipse-glsp/client';
import { BaseJsonrpcGLSPClient } from '@eclipse-glsp/protocol';
import { createGlspClientModule, HydraniumGlspClient } from '@hydranium/glsp-client';
import { initializeOrderFlowProcessDiagramContainer } from '@hydranium/example-order-flow-client/lib/diagram/order-flow-process-diagram-module';
import { PROCESS_DIAGRAM_TYPE } from '@hydranium/example-order-flow-client/lib/diagram/order-flow-process-diagram-types';
import { Container, ContainerModule } from 'inversify';
import { sendByMethodName, SystemClock } from '@hydranium/protocol';
import { type Logger, sendByMethodName, SystemClock } from '@hydranium/protocol';
import type { MessageConnection } from 'vscode-jsonrpc/browser';
import { enableTouchDragging } from './touch-input.js';
// LAST, so esbuild emits these rules after `@eclipse-glsp/client`'s and they win
Expand Down Expand Up @@ -104,17 +105,22 @@ const noContextMenuModule = new ContainerModule(bind => {
* Resolves with a one-line report of what ended up on the page — the same shape
* as the data head's line, so the three heads read as three answers about one
* store rather than as three unrelated widgets.
*
* The logger carries the framework's client classes under a tag of the page's
* own, so a load failure the page itself reports does not read as a line the
* worker sent.
*/
export async function mountProcessDiagram(
connection: MessageConnection,
sourceUri: string,
onReport: (report: string) => void
onReport: (report: string) => void,
logger: Logger
): Promise<string> {
// No `connection.listen()` here: `BaseJsonrpcGLSPClient.start` calls it, and
// a second call throws. GLSP's client sends message types built by its own
// copy of `vscode-jsonrpc`, which this connection's copy rejects unless they
// go by method name.
const glspClient = new BaseJsonrpcGLSPClient({ id: PROCESS_DIAGRAM_ELEMENT_ID, connectionProvider: sendByMethodName(connection) });
const glspClient = new HydraniumGlspClient({ id: PROCESS_DIAGRAM_ELEMENT_ID, connectionProvider: sendByMethodName(connection) });

const diagramOptions: IDiagramOptions = {
clientId: PROCESS_DIAGRAM_ELEMENT_ID,
Expand All @@ -123,15 +129,21 @@ export async function mountProcessDiagram(
sourceUri
};

// The diagram definition is appended LAST by
// `initializeOrderFlowProcessDiagramContainer`, after the host's modules —
// sprotty's model and view registries throw on a duplicate key, so whichever
// module registers an element type first wins by making the second throw.
// The framework's classes replace tokens GLSP's default modules bind, so
// this module goes after `STANDALONE_MODULE_CONFIG`. The diagram definition
// is appended LAST by `initializeOrderFlowProcessDiagramContainer`, after
// the host's modules — sprotty's model and view registries throw on a
// duplicate key, so whichever module registers an element type first wins
// by making the second throw.
const hydraniumModule = new ContainerModule((bind, unbind, isBound, rebind) => {
createGlspClientModule({ bind, unbind, isBound, rebind }, { logger });
});
const container = initializeOrderFlowProcessDiagramContainer(
new Container(),
createDiagramOptionsModule(diagramOptions),
STANDALONE_MODULE_CONFIG,
noContextMenuModule
noContextMenuModule,
hydraniumModule
);

await container.get(DiagramLoader).load();
Expand Down
2 changes: 1 addition & 1 deletion examples/order-flow/browser/src/page/workbench.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1001,7 +1001,7 @@ export async function main(locale: Locale | undefined): Promise<void> {
// The callback keeps the line current as elements are created and deleted;
// the resolved value is the first reading. A report written once would go
// stale on the first palette gesture, beside a layout report that does not.
setGlspReport(await mountProcessDiagram(channels.heads.glsp, GLSP_HEAD_DOCUMENT, setGlspReport));
setGlspReport(await mountProcessDiagram(channels.heads.glsp, GLSP_HEAD_DOCUMENT, setGlspReport, pageLogger.with('GLSP client')));
// AFTER the mount, and the ordering is the whole of it: a listener put on
// this element before the diagram takes it over never fires — measured,
// the same registration moved above this line stops working.
Expand Down
25 changes: 16 additions & 9 deletions examples/order-flow/browser/test/e2e/order-flow-browser.spec.mts
Original file line number Diff line number Diff line change
Expand Up @@ -318,15 +318,22 @@ test.describe('order-flow in a web worker', () => {
// inside it both carry the class.
await expect(page.locator(`${GRAPH} g.sprotty-edge`)).toHaveCount(EXPECTED_EDGE_COUNT);

// SILENT, not merely error-free, and that stricter form is affordable
// because it is currently true: the page binds the one host service GLSP
// would otherwise warn about on every container build. The console is this
// host's only log — the worker posts its failures on the global channel and
// the language server's lines arrive over the LSP one — so a warning that
// means nothing is worse here than elsewhere: it trains the reader to skim
// the one place real failures appear. A new dependency that warns should
// have to be looked at rather than absorbed.
expect(consoleOutput).toEqual([]);
// EXACTLY the framework's two milestone lines, not merely error-free, and
// that stricter form is affordable because it is currently true: the page
// binds the one host service GLSP would otherwise warn about on every
// container build. The console is this host's only log — the worker posts
// its failures on the global channel and the language server's lines
// arrive over the LSP one — so a warning that means nothing is worse here
// than elsewhere: it trains the reader to skim the one place real failures
// appear. A new dependency that warns should have to be looked at rather
// than absorbed. The two lines are the GLSP client's model-initialized
// timing, written through the page's logger under the client's own tag,
// apart from the head's; they are asserted rather than filtered, so a
// framework line that stops arriving is seen too.
expect(consoleOutput).toEqual([
expect.stringMatching(/^info: \[Info {2}- [\d:.]+\] \[GLSP client\] Model initialized \[#\d+ start\]$/),
expect.stringMatching(/^info: \[Info {2}- [\d:.]+\] \[GLSP client\] Model initialized \[#\d+ done, [\d.]+m?s\]$/)
]);
});

test('the narrow layout leaves no control behind on a workbench viewport', async ({ page }) => {
Expand Down
6 changes: 4 additions & 2 deletions examples/order-flow/client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ order-flow-client diagram definition + the properties model
| both halves of the post-message hop)
| adapter
Theia -> @hydranium/glsp-client-theia / data-client-theia / client-theia
VS Code -> @eclipse-glsp/vscode-integration, plus a hand-rolled data hop
Browser -> MessagePorts to heads in a web worker, no host framework
VS Code -> @eclipse-glsp/vscode-integration + @hydranium/glsp-client,
plus a hand-rolled data hop
Browser -> MessagePorts to heads in a web worker + @hydranium/glsp-client,
no host framework
|
order-flow-theia / order-flow-vscode / order-flow-browser thin shells
```
Expand Down
1 change: 1 addition & 0 deletions examples/order-flow/theia/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@hydranium/client-theia": "^1.0.0-next",
"@hydranium/data-client-theia": "^1.0.0-next",
"@hydranium/example-order-flow-client": "0.0.0",
"@hydranium/glsp-client": "^1.0.0-next",
"@hydranium/glsp-client-theia": "^1.0.0-next",
"@hydranium/protocol": "^1.0.0-next",
"@theia/core": "^1.72.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* SPDX-License-Identifier: MIT
********************************************************************************/

import { type ContainerConfiguration, ConsoleLogger, LogLevel, TYPES } from '@eclipse-glsp/client';
import { type ContainerConfiguration } from '@eclipse-glsp/client';
import { initializeOrderFlowProcessDiagramContainer } from '@hydranium/example-order-flow-client/lib/diagram/order-flow-process-diagram-module';
import { AbstractHydraniumGlspDiagramConfiguration, createGlspClientTheiaModule } from '@hydranium/glsp-client-theia/browser';
import { type Container, ContainerModule } from '@theia/core/shared/inversify';
Expand Down Expand Up @@ -55,13 +55,7 @@ export class OrderFlowProcessDiagramConfiguration extends AbstractHydraniumGlspD
* The log THRESHOLD is not set here — it is a process-global applied once from
* the frontend module via `logLevelPreference`, whereas this module is
* instantiated per diagram container.
*
* `TYPES.ILogger` is rebound by hand because the framework offers no helper for
* it; sprotty's default logger is otherwise silent about view-registration
* failures, which are exactly the errors a diagram integration produces.
*/
const orderFlowProcessTheiaModule = new ContainerModule((bind, unbind, isBound, rebind) => {
rebind(TYPES.ILogger).to(ConsoleLogger).inSingletonScope();
rebind(TYPES.LogLevel).toConstantValue(LogLevel.warn);
createGlspClientTheiaModule({ bind, unbind, isBound, rebind }, { channelLogger: { channelName: ORDER_FLOW_OUTPUT_CHANNEL } });
});
1 change: 1 addition & 0 deletions examples/order-flow/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"@hydranium/example-order-flow-client": "0.0.0",
"@hydranium/example-order-flow-server": "0.0.0",
"@hydranium/example-order-flow-vscode-servers": "0.0.0",
"@hydranium/glsp-client": "^1.0.0-next",
"@hydranium/protocol": "^1.0.0-next",
"inversify": "^6.1.3",
"vscode-jsonrpc": "9.0.0",
Expand Down
Loading
Loading