Skip to content

Packages and runtime dependencies

A language is not one package. It is split across three or four, because the same code has to run in two very different places — the workbench's web worker in the browser, and the plugin service on a server — and because only some of it may be loaded twice.

This page covers the split, the base packages the platform gives you, and the runtime dependency mechanism that ties them together.

The four packages of a language

PackageRuns inContains
language-<name>Browser and serviceGrammar, scoping, validation, type system, serializers, diagram server, action handlers
editor-<name>Browser onlyThe GLSP/Sprotty diagram editor: views, palette, styling, client handlers
protocol-<name>BothAction and model types shared between editor and language server
service-<name>Service onlyThe manifest, the served entry points, handlers, deployment

Only service-<name> is deployed. The others are bundled into what it serves:

service-<name>/static/
├── language.js     ← language-<name>  (+ protocol-<name>)
├── editor.js       ← editor-<name>    (+ protocol-<name>)
├── styles.css      ← editor-<name>
└── gedWorker.js    ← language-<name>  (diagram diffing, optional)

A language without a diagram editor needs only language-<name> and service-<name> — the script and config plugins are built that way.

Why the split

language-<name> runs in the browser for editing and on the server for computing file data, so it must not depend on anything Node-specific. editor-<name> only ever runs in the browser, so it may depend on DOM APIs. protocol-<name> exists so the two halves of a diagram editor agree on the actions they exchange without depending on each other.

Base packages

Six packages carry the machinery so your plugin does not have to.

@mdeo/plugin

The manifest interfaces — Plugin, LanguagePlugin, LanguageContributionPlugin, ServerContributionPlugin. No runtime dependencies at all. Both your service and the backend speak this vocabulary.

@mdeo/language-common

The foundation for everything language-side:

AreaWhat you get
Grammar DSLcreateRule, createInterface, createTerminal, createInfixRule, and the combinators
SerializationGrammarSerializer, GrammarDeserializer, GrammarDeserializationContext
Module assemblycreateModule, createGLSPModule, configureGLSPServer
Default tokensID, INT, FLOAT, STRING, WS, NEWLINE, HIDDEN_NEWLINE, ML_COMMENT, SL_COMMENT
Editor defaultsdefaultLanguageConfiguration, defaultMonarchTokenProvider, serializeMonarchTokensProvider
ProtocolAction, AST-serializer, external-reference and metadata interfaces
Plugin contextPluginContext, initializePluginContext
UtilitiesconvertIcon, graph edit distance, URI parsing

@mdeo/language-shared

Reusable implementations on top of that foundation. This is where most of the work you would otherwise repeat already lives:

AreaWhat you get
ParsingNewlineAwareTokenBuilder, IdValueConverter, extended parser, multimode lexer
ScopingLocal scope providers, file-scoping (generateImportRules and its scope provider), path completion
SerializationDefaultAstSerializer, SerializerFormatter, registerDefaultTokenSerializers
External referencesDefaultExternalReferenceCollector, addExternalReferenceCollectionPhase
Diagram serverBase GModel factory, operation handlers, layout engine, model submission, clipboard
ActionsActionHandlerRegistry, DefaultActionProvider
WorkspaceWorkspace edit service, context actions
Grammar helpersmanySep, LeadingTrailing

@mdeo/editor-common

The client-side foundation: createContainer, the editor PluginContext, and initializeEditorPluginContext.

@mdeo/editor-shared

The reusable editor: DEFAULT_MODULES bundles bounds and layout, move and resize, edge routing and reconnection, label editing, the toolbox, marquee and hand tools, node and edge creation, grid, selection, decorations, undo/redo shared with the text editor, reveal-source, copy/paste and the editor settings panel. Plus base model classes, views and styles.

@mdeo/service-common

The whole plugin service: startLanguageService implements every HTTP endpoint, JWT authentication, static serving, the Langium instance pool, dependency tracking and the execution WebSocket bridge. Also parseServiceConfigFromEnv, astHandler, and the service-side initializePluginContext.

And @mdeo/protocol-common carries the diagram actions and metadata types shared by every editor.

Runtime dependency management

The rest of this page is the part that surprises people.

The problem

The workbench loads several plugins' language.js modules into one Langium environment. If each bundle brought its own copy of Langium, there would be several AstNode implementations, several service registries and several instanceof universes in the same worker — and cross-language references, which are the whole point, would not resolve.

So heavy libraries must exist exactly once, provided by the host rather than bundled by each plugin.

The mechanism

A host — the workbench, or a plugin service — builds a PluginContext holding the real modules and installs it on globalThis before any language code is imported:

ts
// service side: @mdeo/service-common does this for you
import { initializePluginContext } from "@mdeo/service-common";
initializePluginContext();

// only now may language packages be imported
const { metamodelPluginProvider } = await import("@mdeo/language-metamodel");

Language code then reaches a managed dependency through sharedImport, and imports it statically only as a type:

ts
import type { ELK, ElkNode } from "elkjs";           // types: erased at build time
import { sharedImport } from "@mdeo/language-shared"; // values: from the host

const elkjs = sharedImport("elkjs");

Import order is load-bearing

initializePluginContext() must run before the first import of a language package, and language packages must therefore be imported with await import(...) rather than a static import. A static import is hoisted above the initialisation call and fails with "Plugin context is not initialized."

There are two contexts

Server-side and client-side dependencies are managed separately, because they are needed in different places.

Language contextEditor context
GlobalglobalThis.pluginContextglobalThis.editorPluginContext
Type declared in@mdeo/language-common@mdeo/editor-common
Installed byinitializePluginContext from @mdeo/language-common (workbench) or @mdeo/service-common (services)initializeEditorPluginContext from @mdeo/editor-common
AccessorsharedImport from @mdeo/language-sharedsharedImport from @mdeo/editor-shared
Used bylanguage-*, service-*editor-*

Common versus shared

The naming is not decorative. It says which side of the mechanism a package sits on:

*-common*-shared
RoleDeclares the contractImplements against it
OwnsThe PluginContext type and the initialiserThe sharedImport accessor
Depends on managed librariesAs devDependencies — types onlyAs devDependencies — types only
Who calls into itHosts, at startupYour language and editor code

So the rule of thumb when writing a language:

  • import types from the managed library itself (import type { AstNode } from "langium");
  • import values with sharedImport("langium") from @mdeo/language-shared;
  • never add a managed library to your package's dependencies — put it in devDependencies, so it is available for type checking and cannot be bundled.

@mdeo/service-common is the exception that proves the rule: it depends on the managed libraries for real, because it is the host that supplies them.

Managed dependencies

Language context — 15 entries, keyed by their import specifier:

KeyPurpose
langiumCore Langium
langium/lspLangium's LSP layer
langium/grammarGrammar AST helpers, used to build modules
typirThe type system framework
typir-langiumIts Langium binding
prettierFormatting, used by the serializers
@eclipse-glsp/serverDiagram server
@eclipse-glsp/server/browser.jsIts browser entry point
@eclipse-glsp/protocolGLSP actions and operations
@eclipse-glsp/graphThe GLSP graphical model
inversifyDependency injection, used by GLSP
vscode-jsonrpcJSON-RPC primitives
vscode-languageserver-typesLSP data types
vscode-languageserver-protocolLSP protocol types
elkjsAutomatic diagram layout

Editor context — 7 entries:

KeyPurpose
@eclipse-glsp/clientThe GLSP client
@eclipse-glsp/sprottySprotty rendering
@eclipse-glsp/protocolGLSP actions and operations
inversifyDependency injection
minisearchSearch in the toolbox and palettes
lucideIcons
snabbdomThe virtual DOM the views render into

Anything not on these lists is an ordinary dependency of your package: declare it in dependencies and bundle it normally.

Putting it together

For a new language todo:

app/packages/
├── language-todo/     deps: @mdeo/language-common, @mdeo/language-shared, @mdeo/plugin
│                      devDeps: langium, typir, … (types only)
├── editor-todo/       deps: @mdeo/editor-shared          (only if it has a diagram editor)
│                      devDeps: @eclipse-glsp/client, inversify, snabbdom, …
├── protocol-todo/     deps: —                            (only if it has a diagram editor)
└── service-todo/      deps: @mdeo/service-common, @mdeo/language-todo, @mdeo/plugin, lucide
                       devDeps: vite, tsx, @types/node

Register each new package in app/tsconfig.build.json so the project-reference build picks it up.

See Add a plugin for the files that go inside, and Anatomy of a plugin for how a service is wired.

Released under the terms of the repository licence.