ng-weld
v0.2.1
Published
Weld Angular Module Federation remotes to a host shell: one typed contract for declaring embeddable components, emitted as a descriptor JSON the host consumes. Angular micro-frontend embed manifest.
Downloads
77
Maintainers
Readme
ng-weld
Weld Angular Module Federation remotes to a host shell. One typed contract for declaring embeddable components, emitted as a descriptor JSON the host consumes — so no team has to read another team's source to wire it up.
When several teams each ship an Angular Module Federation remote, every team invents its own way to expose a component or page for embedding: different window config keys, different custom-element tags, different bootstrap boilerplate. A host shell then has to learn each remote's quirks by hand.
ng-weld gives every remote one typed way to declare what it exposes, and produces one predictable embed-descriptor.json that any host can read — no more reading each remote's source to wire it up.
remote's embed.config.mjs ──defineEmbed()──▶ validated contract
│
serializeDescriptor()
│
▼
embed-descriptor.json ──▶ host shellInstall
npm install ng-weldQuickstart for a remote (producer)
A remote ends up with two files — one config, one element file — plus a one-line change to an
existing file and a one-line change to package.json. The steps below are in the order you'll
actually do them: write the element before you declare it, so the config never describes
something that doesn't exist yet.
Prerequisite: a native-federation Angular remote with a web-component expose key (e.g.
'./WebComponent') in federation.config.js. Note the file that key points at — call it your
entry file; you'll re-export through it in step 4.
1. Install
pnpm add -D ng-weld2. Write the element
For a partial element — one feature component exposed on its own, with no routing —
definePartialElement() (from ng-weld/runtime) removes the boilerplate:
// partial-elements.ts
import { provideHttpClient } from '@angular/common/http';
import { definePartialElement } from 'ng-weld/runtime';
import { ReportsTableComponent } from './features/reports-table.component';
export const defineRemoteReportsTableElement = definePartialElement({
elementTag: 'remote-reports-table-element',
component: ReportsTableComponent,
providers: [provideHttpClient()],
});That's the whole element. The helper handles the idempotency guard, createApplication(),
createCustomElement(), and customElements.define() — and returns the idempotent async function
the host calls as defineExport.
Why partial elements matter: each one boots its own Angular application, so it shares no
Router and no global state with any other element. That's what lets a host place several widgets
from the same remote on one page. A whole-page element that owns routing can only be mounted
once per page — two of them fight over the browser URL. For that reason definePartialElement()
rejects routing providers with a clear error rather than letting the collision reach runtime.
If an element genuinely needs its own router, it's a whole-page element: write it by hand (see
createApplication + a silent LocationStrategy — no ng-weld helper for this yet).
ensureRemoteStyles() is also available for injecting the remote's stylesheet when it's embedded.
Import path matters.
ng-weld(main) has zero dependencies and is safe to import from a Node build script — that's whyembed.config.mjs(step 4) uses it.ng-weld/runtimeneeds Angular as a peer dependency and is browser-only. Never importng-weld/runtimefrom your config.
3. Re-export it through your entry file
A host Connection stores a single exposedModule, so every element you offer must be reachable
through that one key — otherwise the host would need a separate connection per file. Add this
to the entry file from the Prerequisite:
// remote-entry.element.ts ← the file './WebComponent' points at
export * from './partial-elements';4. Declare the contract — embed.config.mjs (repo root)
Author it as pure data (no Angular imports) so the emit runs in plain node with no risk of a
browser-only import crashing the build:
// embed.config.mjs
import { defineEmbed } from 'ng-weld';
export default defineEmbed({
provider: 'ANGULAR_COMPONENT',
exposedModule: './WebComponent',
windowConfigKey: '__remoteReportsConfig',
navigateEventName: 'remoteReports:navigate',
name: 'Reports',
defaultWindowConfig: { basePath: '/reports-widget', mfeName: 'remoteReports' },
elements: [
{
elementTag: 'remote-reports-table-element', // must match step 2, exactly
description: 'Reports table, embedded standalone.',
},
// A remote exposing more elements just adds more entries here.
],
});defineEmbed() validates as you write — hyphen in elementTag, __ prefix on windowConfigKey,
known provider, unique tags — throwing EmbedConfigError on any violation. It does not
check that elementTag matches something you actually exported in step 2 — see the drift warning
below.
⚠️ Step 2 and step 4 must agree, and nothing checks that for you.
ng-weld emit(step 6) reads only this config — it never inspects your TypeScript.defineExportis derived fromelementTag(remote-reports-table-element→defineRemoteReportsTableElement); if your step-2 export doesn't use that exact name, or you delete/rename it later without updating this array, the descriptor will keep advertising an element that doesn't exist. The host only discovers this at runtime, with "Remote did not export function …". Re-read steps 2 and 4 side by side before you ship.
5. Wire the emit into your build
ng-weld ships the emitter as a CLI, so there's no script to copy into your repo. Run it before
ng build:
{
"scripts": {
"start": "ng-weld emit && ng serve",
"build": "ng-weld emit && ng build",
"build:prod": "ng-weld emit && ng build --configuration production",
"emit:descriptor": "ng-weld emit"
}
}ng-weld emit reads embed.config.mjs and writes public/embed-descriptor.json. Emitting into
public/ is what makes the descriptor available in both ng serve (dev) and ng build
(prod — Angular copies public/ into the output, next to remoteEntry.json).
ng-weld emit # embed.config.mjs → public/embed-descriptor.json
ng-weld emit --config src/embed.mjs # custom config path
ng-weld emit --out dist/app/browser # custom output directory
ng-weld emit --silent # no success loggingThe config is re-validated on emit, so a malformed contract fails your build instead of shipping a broken descriptor. (This re-validates the shape — it still can't catch the step 2/4 drift above, since it has no way to inspect your compiled code.)
6. Emit & verify
pnpm emit:descriptor # or: pnpm buildOpen it in a browser — it should sit right next to remoteEntry.json:
http://localhost:4204/embed-descriptor.jsonThe emitted JSON:
{
"schemaVersion": 1,
"provider": "ANGULAR_COMPONENT",
"exposedModule": "./WebComponent",
"windowConfigKey": "__remoteReportsConfig",
"navigateEventName": "remoteReports:navigate",
"defaultWindowConfig": { "basePath": "/reports-widget", "mfeName": "remoteReports" },
"name": "Reports",
"elements": [
{
"elementTag": "remote-reports-table-element",
"defineExport": "defineRemoteReportsTableElement",
"description": "Reports table, embedded standalone."
}
]
}Check that elementTag and defineExport here match your step-2 code exactly — this is the
fastest place to catch the drift from step 4 before it reaches a host.
7. Build
pnpm build8. Done
The host shell now fetches your descriptor from the Remote Entry URL and auto-fills its Connection
- DataSource forms. The remote team does nothing else.
CORS: the descriptor is fetched cross-origin, so your remote must send CORS headers — the same requirement
remoteEntry.jsonalready meets.
Consuming the descriptor (host side)
The same package validates the descriptor on the host. Instead of hand-parsing
the fetched JSON, call parseDescriptor() — it checks schemaVersion, validates the
shape, and hands back a typed EmbedDescriptor, so a malformed or newer-than-supported
descriptor fails loudly instead of rendering a broken widget:
import { parseDescriptor } from 'ng-weld';
const res = await fetch(`${remoteEntryUrl.replace('remoteEntry.json', 'embed-descriptor.json')}`);
const descriptor = parseDescriptor(await res.text());
// descriptor.exposedModule, .windowConfigKey, .navigateEventName, .defaultWindowConfig
// → fill the host Connection form
// descriptor.elements[] (each .elementTag + .defineExport + .acceptsRoutes)
// → one candidate DataSource per elementAPI
ng-weld — no dependencies, safe in Node
| Export | Side | Description |
| --- | --- | --- |
| defineEmbed(config) | producer | Validate and return a remote's embed contract. Throws EmbedConfigError. |
| toDescriptor(config) | producer | Project a config into the plain-object descriptor. |
| serializeDescriptor(config) | producer | Pretty-printed JSON string, ready to write to disk. |
| parseDescriptor(json) | consumer | Validate + type a fetched descriptor. Throws DescriptorParseError. |
| deriveDefineExport(tag) | — | The deterministic define<Pascal> name for an element tag. |
| SCHEMA_VERSION | — | Current descriptor schema version. |
ng-weld/runtime — browser only, Angular peer dependency
| Export | Description |
| --- | --- |
| definePartialElement(opts) | Define a routing-free element with its own isolated application. Returns the idempotent defineExport function. |
| ensureRemoteStyles(opts?) | Inject the remote's stylesheet once per document (handles hashed filenames and @layer stripping). |
CLI
| Command | Description |
| --- | --- |
| ng-weld emit | Read embed.config.mjs, write public/embed-descriptor.json. Supports --config, --out, --silent. |
Full TypeScript types (EmbedConfig, EmbedDescriptor, …) are shipped.
What ng-weld does and does not guarantee
- ✅ The descriptor shape — every remote emits the same envelope, and
parseDescriptorenforces it on the host. - ✅ The
defineExportname — derived deterministically from the tag. - ✅ Correct partial-element behavior — when you build one with
definePartialElement(), the idempotency guard and application isolation are handled for you, and routing providers are rejected outright. - ⚠️ Not that the declared elements actually exist.
ng-weld emitreads onlyembed.config.mjs— it never inspects your TypeScript. If your config declares an element you never exported, the descriptor advertises it anyway and the host fails at runtime with "Remote did not export function …". Keep the config and your element file in sync. Closing this gap — generating the element code from the same config — is the remaining roadmap item.
Security note
The defaultWindowConfig and any per-connection config end up on
window[windowConfigKey], readable by any co-tenant of the host shell. Put
only configuration there — never API keys, tokens, or user identifiers.
Roadmap
- v0.1 — typed
defineEmbed()contract + descriptor emit + host-sideparseDescriptor(). - v0.2 (current) —
ng-weld emitCLI (no more copied emit script) andng-weld/runtime'sdefinePartialElement()for multi-element remotes. - next — generate the element code from
embed.config.mjsso the config and the implementation cannot drift, and extend the same treatment to whole-page (routed) elements.
License
MIT © irsyadali1
