@techhalo/halo-plugin
v1.0.3
Published
CLI for developing, building, and publishing Module Federation-based Halo plugins
Maintainers
Readme
Halo Plugin CLI (Module Federation only)
CLI for developing, authenticating, building, and publishing Halo plugins as Module Federation (MF) remotes.
Features
- Scaffold a ready-to-build Module Federation remote (Angular)
- Build MF artifacts that emit a remoteEntry.js
- Publish via Catalogue API: create first, then upload remoteEntry.js (no S3 creds required)
- Manifest-driven extension points (default:
plugin:app) - Auth: register, login, and logout with JWT stored locally (no API token env var needed)
- Angular Elements Zone.js Fix: Automatic handling of Zone.js dependency for web component plugins
- Ships a default cover image (
assets/cover.png) and CLI support for uploading custom covers
Requirements
- npm or yarn
Install
npm install -g @techhalo/halo-pluginQuick start (login required)
# 1) Authenticate (required; stores JWT under ~/.halo/config.json)
halo-plugin auth:register \
--full-name "Jane Dev" --email [email protected] --password secret123 --company Acme
halo-plugin auth:login --email [email protected] --password secret123
# 2) Set up code signing (one-time setup after registration)
halo-plugin keygen # Generate signing keys
halo-plugin keys upload # Upload public key to catalogue
# 3) Scaffold an MF remote plugin
halo-plugin generate user-profile
cd plugins/user-profile
# Optional: replace the default cover image before uploading to the catalogue
# cp /path/to/custom-cover.png assets/cover.png
# 4) Install plugin deps
npm install
# 5) Build production artifacts (must emit remoteEntry.js)
halo-plugin build --prod
# 6) Publish via Catalogue API (requires prior login and key setup)
# Provide --api-url or set HALO_API_URL; JWT from auth:login is used automatically
export HALO_API_URL="http://localhost:3000"
halo-plugin publishThis CLI automatically includes a fix for Angular Elements plugins that require Zone.js. The fix ensures Zone.js is loaded before custom elements are defined, preventing errors like:
NG908: Zone.js is required but missing
halo-biometric-provider not defined after UMD loadFor Plugin Developers
New plugins automatically include the fix. No additional changes needed.
For Host Applications
Use the provided Web Component Loader for safe plugin loading:
<script src="halo-wc-loader.js"></script>
<script>
HaloLoader.loadPlugin({
elementTag: 'halo-my-plugin',
pluginUrl: './my-plugin.iife.js'
});
</script>See Zone.js Fix Documentation for detailed information.
Code Signing & Trust
Halo CLI implements enterprise-grade code signing to ensure plugin integrity and authenticity.
Initial Setup (One-time after registration)
After registering your developer account, set up code signing:
# 1. Generate signing keys (RSA-4096 or ECDSA-P384)
halo-plugin keygen
# 2. Upload your public key to the catalogue
halo-plugin keys uploadcover:upload (authenticated)
Upload or replace the cover image used by Catalogue listings.
plugin cover:upload <pluginId> [dir]Options
-f, --file <path>Use a specific PNG file (defaults to[dir]/assets/cover.png)-u, --api-url <url>Catalogue API base URL (alt:HALO_API_URL)
Behaviour
- Validates that the image exists, is a PNG file, and is not a directory
- Reads the plugin ID argument and posts to
/api/plugins/files/cover - Falls back to
[dir]/assets/cover.pngso you can run it from the plugin root
Your private key is stored locally in ~/.halo/keys/ and never leaves your machine. The public key is uploaded to the Halo catalogue to verify your plugin signatures.
Key Management Commands
# List all keys and their status
halo-plugin keys list
# Upload public key to catalogue
halo-plugin keys upload
# Rotate keys (generate new, archive old)
halo-plugin keys rotate
# Revoke a compromised key
halo-plugin keys revoke <fingerprint>
# Export public key to file
halo-plugin keys export <destination>
# Import existing key pair
halo-plugin keys import <privateKey> <publicKey>How It Works
- Key Generation: Generate RSA-4096 or ECDSA-P384 key pairs locally
- Public Key Upload: Upload public key to catalogue (one-time)
- Plugin Signing: During build/publish, plugins are signed with your private key
- Signature Verification: Catalogue verifies signatures using your public key
Security Benefits
- Authenticity: Proves plugins come from verified developers
- Integrity: Detects any tampering with plugin files
- Non-repudiation: Developers cannot deny publishing a plugin
- Trust Chain: Users can verify plugin publishers
Key Rotation
Rotate keys periodically or when compromised:
halo-plugin keys rotateThis archives your current keys and generates new ones. Re-publish all plugins with the new key.
Commands
create
Create a new Module Federation remote plugin project.
halo-plugin create <name> [options]Options
-r, --remote-url <url>Remote base URL to reference in manifest (optional)--cover-image <path>Copy a custom PNG intoassets/cover.png
What you get
- Module Federation config:
module-federation.config.js,webpack.config.js - Angular bootstrap split:
src/bootstrap.ts,src/main.ts - Remote module:
src/app/plugin.module.tsexposed as./PluginModule - Manifest:
plugin-manifest.jsonwith:type: 'angular-mf'entry:{ remoteName, remoteEntry, exposedModule }navigation:{ routes: [{ path, label }] }
- Assets folder with
assets/cover.pngplaceholder ready for Catalogue upload
generate
Scaffold a new Module Federation remote plugin project under plugins/<name>.
halo-plugin generate <name> [options]Options
-d, --description <desc>Description text--cover-image <path>Copy a custom PNG intoassets/cover.png--outDir <dir>Directory to create the project in (default: plugins)--template <lit|ng-elements|angular-mf>Template to use (default: ng-elements)
What you get
- Module Federation remote scaffold (same as
create) - Manifest:
plugin-manifest.json(v1 schema: id, name, version, type, entry, permissions, navigation, extensionPoints, checksum/signature) - Assets folder with
assets/cover.pngplaceholder ready for Catalogue upload
build
Build with Angular CLI and verify MF artifacts (remoteEntry.js), then package ZIP for checksum/signature.
halo-plugin build [dir] [options]Options
-p, --prodProduction build (recommended)-k, --private-key <path>Sign manifest with provided private key
Outputs
- Angular build in
dist/<plugin-name>/and must includeremoteEntry.js - Versioned package:
<plugin-name>-v<version>.zipin plugin root - Manifest updated with checksum/signature derived from the ZIP
publish (authenticated)
Create the plugin in the catalogue, then upload remoteEntry.js to the plugin files endpoint.
halo-plugin publish [dir] [options]Options
-u, --api-url <url>Catalogue API base URL (alt:HALO_API_URL)--publicMark plugin as public (default)--privateMark plugin as private
Behaviour
- Version Conflict Detection: Checks if a plugin with the same name and version already exists
- If found, warns the user that publishing will override the existing version
- Provides interactive options to resolve the conflict:
- Continue with override (same version)
- Update to patch version (e.g., 1.2.3 → 1.2.4)
- Update to minor version (e.g., 1.2.3 → 1.3.0)
- Update to major version (e.g., 1.2.3 → 2.0.0)
- Enter custom version
- Cancel publishing
- Automatically updates
plugin-manifest.jsonwith the new version if changed - Suggests rebuilding the plugin when version is updated
- Verifies
plugin-manifest.jsonand<name>-v<version>.zip - Computes SHA-256 signature of the ZIP (for traceability) and updates manifest fields as needed
- Calls Catalogue API
POST /api/pluginswith name, version, description, publisher and a manifest payload including:
manifestVersion(same asversion)entryScriptUrl = remoteEntry.jschecksumandsignature(SHA-256 of the ZIP)
- Uploads
remoteEntry.jstoPOST /api/plugins/{pluginId}/files/uploadwith:
- multipart
file(content typeapplication/javascript) type = angular-bundle,version, optionaldescription
- Zips and uploads remaining build artifacts (excluding
remoteEntry.js) toPOST /api/plugins/{pluginId}/files/artifacts - Prints created plugin id and Remote Entry URL
Authentication
- Run
halo-plugin auth:loginonce to save your JWT locally; all catalogue API calls use it automatically. - Use
halo-plugin auth:logoutto clear the saved token.
Login required
- All CLI commands except
auth:*require you to be logged in; otherwise the CLI will exit with an instruction to login.
Environment variables
HALO_API_URL— Catalogue API base URL (or pass--api-url)
No S3 credentials are needed — uploads are proxied by the Catalogue API.
Plugin anatomy
Key files generated by generate:
plugin-manifest.json— id, name, version, type, entry, permissions, navigation, extensionPoints, checksum/signaturemodule-federation.config.js— MF config with name, filename, exposes{ './PluginModule': './src/app/plugin.module.ts', './Component': './src/app/<name>.component.ts' }webpack.config.js— applies withModuleFederationPlugin and sets shared dependencies; supports dev ESM viaMF_DEV_ESM=true- Angular workspace files —
angular.json(with@angular-builders/custom-webpack),tsconfig.json,src/index.html,src/bootstrap.ts,src/main.ts,src/styles.css
Plugin Permissions
Generated plugins automatically include ASP.NET Zero compatible permissions structure:
{
"permissions": {
"Permissions": [
{
"Name": "Pages.Plugins",
"DisplayName": "Plugins",
"Description": "Root permission for all plugins",
"MultiTenancySide": "Both",
"Children": [
{
"Name": "Pages.Plugins.YourPlugin",
"DisplayName": "YourPlugin",
"Description": "YourPlugin plugin permissions",
"MultiTenancySide": "Both",
"Children": [
{
"Name": "Pages.Plugins.YourPlugin.View",
"DisplayName": "View YourPlugin",
"Description": "Permission to view YourPlugin plugin",
"MultiTenancySide": "Both",
"Children": []
},
{
"Name": "Pages.Plugins.YourPlugin.Create",
"DisplayName": "Create in YourPlugin",
"Description": "Permission to create items in YourPlugin plugin",
"MultiTenancySide": "Both",
"Children": []
},
{
"Name": "Pages.Plugins.YourPlugin.Edit",
"DisplayName": "Edit in YourPlugin",
"Description": "Permission to edit items in YourPlugin plugin",
"MultiTenancySide": "Both",
"Children": []
},
{
"Name": "Pages.Plugins.YourPlugin.Delete",
"DisplayName": "Delete in YourPlugin",
"Description": "Permission to delete items in YourPlugin plugin",
"MultiTenancySide": "Both",
"Children": []
},
{
"Name": "Pages.Plugins.YourPlugin.Settings",
"DisplayName": "YourPlugin Settings",
"Description": "Permission to manage YourPlugin plugin settings",
"MultiTenancySide": "Both",
"Children": []
}
]
}
]
}
]
}
}Default Permissions Generated:
Pages.Plugins.{PluginName}.View- Access the pluginPages.Plugins.{PluginName}.Create- Create items within the pluginPages.Plugins.{PluginName}.Edit- Edit items within the pluginPages.Plugins.{PluginName}.Delete- Delete items within the pluginPages.Plugins.{PluginName}.Settings- Manage plugin settings
Navigation Integration: Plugin navigation routes automatically reference the View permission:
{
"navigation": [
{
"name": "your-plugin",
"permissionName": "Pages.Plugins.YourPlugin.View",
"requiresAuthentication": true
}
]
}Component Integration: Generated components use permissions in menu registration:
const menuItems: PluginMenuItem[] = [
{
id: 'your-plugin-dashboard',
label: 'Your Plugin Dashboard',
route: '/plugins/your-plugin',
permission: 'Pages.Plugins.YourPlugin.View'
},
{
id: 'your-plugin-settings',
label: 'Settings',
route: '/plugins/your-plugin/settings',
permission: 'Pages.Plugins.YourPlugin.Settings'
}
];Build output (production)
dist/<plugin-name>/
remoteEntry.js
index.html
main.*.js
polyfills.*.js
runtime.*.js
styles.*.cssPackage
<plugin-name>-v<version>.zip
plugin-manifest.json
remoteEntry.js
index.html
main.*.js
polyfills.*.js
runtime.*.js
styles.*.css
3rdpartylicenses.txtNavigation and routing
The CLI sets a sensible default so your plugin is routable out of the box:
{
"navigation": {
"routes": [
{ "path": "your-plugin", "label": "your-plugin" }
]
},
"extensionPoints": ["plugin:app"]
}- navigation.routes: Simple route registrations the host can read to add routes/menus.
- extensionPoints: Contribution points; default is a routed app.
Extension points (manifest-only)
Plugins declare their extension points in plugin-manifest.json under extensionPoints. The default is ['plugin:app'], which indicates a routed mini-app mounted by the host.
Troubleshooting
Angular Template Issues
Error: Component is standalone, and cannot be declared in an NgModule
Error: src/app/app.component.ts:9:20 - error NG6008: Component AppComponent is standalone, and cannot be declared in an NgModule. Did you mean to import it instead?This occurs when a component is marked as standalone: true but also declared in NgModule.declarations.
Quick Fix: Open your src/app/app.component.ts and remove the standalone: true line from the @Component decorator.
Alternative Solution: If you want to keep the component standalone:
- Add
standalone: trueto the component - In
src/app/app.module.ts, moveAppComponentfromdeclarationstoimports - Add
CommonModuleto imports if using*ngIf,*ngFor, etc.
@NgModule({
imports: [BrowserModule, AppComponent], // Move here
declarations: [], // Remove from here
providers: [HostApiService]
})Error: Class is using Angular features but is not decorated
Error: src/examples/host-api-integration.component.ts:105:14 - error NG2007: Class is using Angular features but is not decorated.This occurs when Angular components/services are missing their decorators (@Component, @Injectable, etc.). The CLI templates now include proper decorators.
General Build Issues
- Build succeeds but no remoteEntry.js: ensure
@angular-builders/custom-webpack:browserbuilder is used andcustomWebpackConfig: { path: 'webpack.config.js' }is set inangular.json; ensurewebpack.config.jsapplieswithModuleFederationPlugincorrectly. - Publish fails with API access: confirm
HALO_API_URL(or pass--api-url), and that you are logged in (halo-plugin auth:login). - Invalid content type on upload: the CLI uploads
remoteEntry.jswithapplication/javascript.
Host integration (guided by the Angular Architects tutorial)
Static host (compile-time remotes)
- Host webpack config registers remotes:
- remotes: { mfe1: 'http://localhost:4201/remoteEntry.js' }
- Router lazy route:
- loadChildren: () => import('mfe1/Module').then(m => m.FlightsModule)
Dynamic host (runtime remotes)
- Local dev with ESM container (recommended):
- In the plugin folder, run:
npm run start:esm(servesremoteEntry.mjson port 4201) - In host routes:
loadRemoteModule({ type: 'module', remoteEntry: 'http://localhost:4201/remoteEntry.mjs', exposedModule: './PluginModule' }).then(m => m.PluginModule)
Load metadata upfront before Angular bootstraps (recommended):
- In main.ts, call loadRemoteEntry({ type: 'module', remoteEntry }) before importing bootstrap.
Use a manifest/registry (recommended for production):
- Place assets/mf.manifest.json: { "plugin-remote": "https://cdn.example.com/plugin/remoteEntry.js" }
- In main.ts, call loadManifest('assets/mf.manifest.json') before importing bootstrap.
- In routes, use loadRemoteModule({ type: 'manifest', remoteName: 'plugin-remote', exposedModule: './PluginModule' }).
Angular singletons sharing
- In both host and remote webpack.config.js, use share(...) with singletons and strictVersion for Angular packages.
- The generator already configures the remote accordingly; ensure your host does the same.
Component vs Module loading
- This CLI exposes both:
- Module: './PluginModule' (NgModule with RouterModule.forChild)
- Component: './Component' (standalone true)
- Your manifest may include entry.component if you want hosts to prefer component loading; otherwise, rely on module-based routing.
Checklist when a remote doesn’t render in a host
- Confirm remoteEntry.js loads: open it in the browser/network tab and ensure 200 OK.
- Ensure share scope is populated after app start: window.webpack_share_scopes?.default?.['@angular/core'] is truthy.
- Verify exposed path matches host import: './PluginModule' or './Component'.
- Avoid BrowserModule in the remote; use CommonModule + RouterModule.forChild, and standalone dev bootstrap.
- Check CORS on remoteEntry.js and chunk files.
Avoid NG0203 (duplicate Angular) in remotes
The generator configures the remote to share Angular and runtime libraries as singletons via Module Federation. Key points:
webpack.config.jsuseswithModuleFederationPluginandshare({...})for:@angular/core,@angular/common,@angular/common/http,@angular/router,@angular/forms,@angular/platform-browser,@angular/platform-browser-dynamic,rxjs, andzone.js.
- The scaffold does not generate any BrowserModule-based application module. For local dev, it uses
bootstrapApplication()on the standalone component insrc/bootstrap.ts(noBrowserModule, nobootstrapModule). - The exposed MF entry is
./PluginModule(src/app/plugin.module.ts) which:- imports
CommonModule - wires
RouterModule.forChild([{ path: '', component: <YourComponent> }]) - re-exports your standalone component so a host can optionally load the component directly.
- imports
If a host app also contributes Angular packages to the share scope (recommended), both sides will reuse a single Angular runtime and DI tree, eliminating the most common cause of NG0203.
Authentication commands
Register a developer account
halo-plugin auth:register --api-url https://your-api \
--full-name "Jane Dev" --email [email protected] --password secret123 --company AcmeLogin and save JWT locally (~/.halo/config.json)
halo-plugin auth:login --api-url https://your-api \
--email [email protected] --password secret123Logout and clear saved token
halo-plugin auth:logoutContributing
- Create a feature branch
- Run
npm run buildbefore committing - Open a PR with a concise description
License
1. Register account
halo-plugin auth:register --full-name "Jane" --email [email protected] --password secret --company Acme
halo-plugin auth:login --email [email protected] --password secret
2. One-time key setup (do this ONCE after registration)
halo-plugin keygen # Generate RSA-4096 or ECDSA-P384 keys halo-plugin keys upload # Upload public key to catalogue
3. Create and publish plugins (repeat for each plugin)
halo-plugin generate my-plugin cd plugins/my-plugin npm install halo-plugin build --prod halo-plugin publish
MIT halo-plugin auth:login --email [email protected] --password 123qwe
