Create a modular execution environment
What a host that executes Beyond public modules must do, shown by BEE Node and Deno: resolving bare identities through a development service or a release's resolution, a Node host that renders widgets on the server, the difference between packaged and composed artifacts, and what a host may and may not expect from updates.
- Availability: Experimental
- Evidence: Recorded run
- How-to guide
The host contract
An execution environment maps public module identities to artifacts, resolves their bare references, executes them and owns their lifecycle. It does not turn internal files into modules of its own, and it does not choose versions: it asks the delivery source, which answers with the compiled module contract of Beyond.
The integration path, in order: resolve a bare specifier through a session of a delivery source; load the artifact for the platform and the environment of the host; keep external references bare so that one framework runs once; optionally connect to the development service for updates; release what the host holds when it ends. Production execution needs no development service.
BEE Node: the loader
The @beyond-js/bee-node package registers Node module hooks that resolve and load public modules from a Beyond delivery source. The adapter names the contract of that source: packages for the development service of Packages, resolution for any base that answers a resolution document (a development service or a CDN release), and engine for the bootstrap server the suite still uses to compile Packages itself.
# A Node process that loads the widget modules of a workspace from its development service through the
# installed loader, registers them and renders each widget with its server controller.
# <installation> is the directory where the Beyond toolchain is installed; <endpoint> the running service.
BEE_URL=<endpoint> BEE_ADAPTER=packages \
node --import <installation>/node_modules/@beyond-js/bee-node/register.mjs render.mjs "Server"The loader asks the service for the module at the Node options of the session (target=node, format=esm, env=development) and imports the artifact as a native ES module. The runtime the artifacts import, and the frameworks the supplied adapters depend on, are resolved once from the installation, so a widget of the workspace and its adapter share one React, one Vue and one Svelte.
Hosts that read a resolution
Every origin of the compiled-module contract answers /resolution.json and /importmap.json: which address delivers each public specifier. A host that reads one of them needs nothing else from the origin, so the same host runs against a development service and against a released application.
- Node.js, with BEE Node's
resolutionadapter:BEE_ADAPTER=resolutionandBEE_URL=<base>, where the base is a development service or a CDN release base (/_r/<release number>). It readsresolution.json?target=node&format=esmonce, loads modules only from under the base, and withBEE_CACHE=<directory>keepsimmutableanswers on disk, so a restart against a release makes no request. - Deno, with no adapter:
deno run --allow-import=<host> --import-map=<base>/importmap.json?target=node&format=esm <program>. The map's addresses are relative to its URL, so Deno requests every module from the base.
A Node host that renders widgets
/**
* The server execution host of the boundaries case: a Node process that loads the widget modules of the
* workspace from the development service through the installed loader, registers them as the module
* registration does on import, and renders each widget with its server controller. It prints the rendered
* HTML of each widget as JSON. It runs twice to show that the output is deterministic.
*
* Usage: BEE_URL=<service> BEE_ADAPTER=packages node --import <loader> ssr.mjs <label>
*/
const [label] = process.argv.slice(2);
const { widgets } = await import('@beyond-js/widgets/render');
const rendered = {};
for (const [name, specifier] of [['hello-react', '@testbed/boundaries/react'], ['hello-vue', '@testbed/boundaries/vue'], ['hello-svelte', '@testbed/boundaries/svelte'], ['hello-html', '@testbed/boundaries/html']]) {
const { Controller } = await import(specifier);
const specs = widgets.get(name);
if (!specs) throw new Error(`Importing ${specifier} did not register ${name}`);
const controller = new Controller({ specs });
const result = await controller.render({ attributes: new Map([['label', label]]) });
rendered[name] = { html: result.html, errors: result.errors ?? [], styles: controller.styles };
}
console.log(JSON.stringify(rendered));Importing a widget module registers its element in the Widgets registry, as it does in a browser; the host reads the specification back, constructs the server controller with it and calls render. The result carries the HTML of the widget and, on the controller, the stylesheet addresses of its dependencies. Nothing here starts a web server: a host that serves pages composes these results into a document and gives the browser the same modules to hydrate.
Packaged and composed artifacts
| Artifact | Produced by | Execution | Updates |
|---|---|---|---|
| Packaged | The esbuild bundler, one public module per native ES module |
Imported directly; public references stay bare | The stylesheet of the module is replaced in place; the code is shown on reload |
| Composed | The ts bundler, on the development runtime |
The artifact imports the runtime and registers one creator per source file, its dependencies and its exports | Internal modules that changed are evaluated again in place; the others keep their state |
Both keep public bare references and both are delivered by the same addresses. A host that runs composed artifacts needs the development runtime, which the delivery source lists in the import map of a page and the installation provides on Node.
What a host may expect from updates
The development runtime applies to loaded modules the builds the service announces: it replaces the creators whose hash changed, evaluates them again, keeps the others, and triggers the change of the package so that a mounted widget refreshes. A source that no longer compiles publishes nothing and the last good module stays; a creator that throws fails its update without locking the module for the corrected one; two edits saved in quick succession end in the last one. After the service restarts, the runtime is told it is stale and applies nothing more: that is a restart boundary of the host, reported instead of guessed. Deleted internal modules are retained, and no state migration is promised.
The development connection needs fetch with streaming bodies, TextDecoder, AbortController, timers and dynamic import(). Node 22, Deno and browsers provide them.
The same update mechanism runs in every host; a host only supplies how it imports an update (native import(), or SystemJS's own import in a SystemJS page) and whether it has a document for stylesheets. Notifications come from the service's event stream by default, from any emitter that speaks the same protocol (Workspace is one), or from local.hmr.notify(event); the updates themselves are always requested from the origin.
Next
Extend the compiler rather than the host: Create a bundler or a processor.