CLI for developing, building, and publishing Module Federation-based Halo plugins
npm install @techhalo/plugin-cliplugin:app)
bash
npm install -g plaza-plugin-cli
`
Quick start (login required)
`bash
1) Authenticate (required; stores JWT under ~/.plaza/config.json)
plaza-plugin auth:register \
--full-name "Jane Dev" --email jane@example.com --password secret123 --company Acme
plaza-plugin auth:login --email jane@example.com --password secret123
2) Set up code signing (one-time setup after registration)
plaza-plugin keygen # Generate signing keys
plaza-plugin keys upload # Upload public key to catalogue
3) Scaffold an MF remote plugin
plaza-plugin generate user-profile
cd plugins/user-profile
4) Install plugin deps
npm install
5) Build production artifacts (must emit remoteEntry.js)
plaza-plugin build --prod
6) Publish via Catalogue API (requires prior login and key setup)
Provide --api-url or set PLAZA_API_URL; JWT from auth:login is used automatically
export PLAZA_API_URL="http://localhost:3000"
plaza-plugin publish
`
Angular Elements Zone.js Fix
This 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
plaza-biometric-provider not defined after UMD load
`
$3
New plugins automatically include the fix. No additional changes needed.
$3
Use the provided Web Component Loader for safe plugin loading:
`html
`
See Zone.js Fix Documentation for detailed information.
---
Code Signing & Trust
Plaza CLI implements enterprise-grade code signing to ensure plugin integrity and authenticity.
$3
After registering your developer account, set up code signing:
`bash
1. Generate signing keys (RSA-4096 or ECDSA-P384)
plaza-plugin keygen
2. Upload your public key to the catalogue
plaza-plugin keys upload
`
Your private key is stored locally in ~/.plaza/keys/ and never leaves your machine. The public key is uploaded to the Plaza catalogue to verify your plugin signatures.
$3
`bash
List all keys and their status
plaza-plugin keys list
Upload public key to catalogue
plaza-plugin keys upload
Rotate keys (generate new, archive old)
plaza-plugin keys rotate
Revoke a compromised key
plaza-plugin keys revoke
Export public key to file
plaza-plugin keys export
Import existing key pair
plaza-plugin keys import
`
$3
1. Key Generation: Generate RSA-4096 or ECDSA-P384 key pairs locally
2. Public Key Upload: Upload public key to catalogue (one-time)
3. Plugin Signing: During build/publish, plugins are signed with your private key
4. Signature Verification: Catalogue verifies signatures using your public key
$3
- 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
$3
Rotate keys periodically or when compromised:
`bash
plaza-plugin keys rotate
`
This archives your current keys and generates new ones. Re-publish all plugins with the new key.
---
Commands
$3
Create a new Module Federation remote plugin project.
`bash
plaza-plugin create [options]
`
Options
- -r, --remote-url Remote base URL to reference in manifest (optional)
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.ts exposed as ./PluginModule
- Manifest: plugin-manifest.json with:
- type: 'angular-mf'
- entry: { remoteName, remoteEntry, exposedModule }
- navigation: { routes: [{ path, label }] }
$3
Scaffold a new Module Federation remote plugin project under plugins/.
`bash
plaza-plugin generate [options]
`
Options
- -d, --description Description text
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)
$3
Build with Angular CLI and verify MF artifacts (remoteEntry.js), then package ZIP for checksum/signature.
`bash
plaza-plugin build [dir] [options]
`
Options
- -p, --prod Production build (recommended)
- -k, --private-key Sign manifest with provided private key
Outputs
- Angular build in dist/ and must include remoteEntry.js
- Versioned package: in plugin root
- Manifest updated with checksum/signature derived from the ZIP
$3
Create the plugin in the catalogue, then upload remoteEntry.js to the plugin files endpoint.
`bash
plaza-plugin publish [dir] [options]
`
Options
- -u, --api-url Catalogue API base URL (alt: PLAZA_API_URL)
- --public Mark plugin as public (default)
- --private Mark plugin as private
Behaviour
1. 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.json with the new version if changed
- Suggests rebuilding the plugin when version is updated
2. Verifies plugin-manifest.json and
3. Computes SHA-256 signature of the ZIP (for traceability) and updates manifest fields as needed
4. Calls Catalogue API POST /api/plugins with name, version, description, publisher and a manifest payload including:
- manifestVersion (same as version)
- entryScriptUrl = remoteEntry.js
- checksum and signature (SHA-256 of the ZIP)
5. Uploads remoteEntry.js to POST /api/plugins/{pluginId}/files/upload with:
- multipart file (content type application/javascript)
- type = angular-bundle, version, optional description
6. Zips and uploads remaining build artifacts (excluding remoteEntry.js) to POST /api/plugins/{pluginId}/files/artifacts
7. Prints created plugin id and Remote Entry URL
Authentication
- Run plaza-plugin auth:login once to save your JWT locally; all catalogue API calls use it automatically.
- Use plaza-plugin auth:logout to 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
- PLAZA_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/signature
- module-federation.config.js — MF config with name, filename, exposes { './PluginModule': './src/app/plugin.module.ts', './Component': './src/app/
- webpack.config.js — applies withModuleFederationPlugin and sets shared dependencies; supports dev ESM via MF_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
$3
Generated plugins automatically include ASP.NET Zero compatible permissions structure:
`json
{
"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 plugin
- Pages.Plugins.{PluginName}.Create - Create items within the plugin
- Pages.Plugins.{PluginName}.Edit - Edit items within the plugin
- Pages.Plugins.{PluginName}.Delete - Delete items within the plugin
- Pages.Plugins.{PluginName}.Settings - Manage plugin settings
Navigation Integration:
Plugin navigation routes automatically reference the View permission:
`json
{
"navigation": [
{
"name": "your-plugin",
"permissionName": "Pages.Plugins.YourPlugin.View",
"requiresAuthentication": true
}
]
}
`
Component Integration:
Generated components use permissions in menu registration:
`typescript
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//
remoteEntry.js
index.html
main.*.js
polyfills.*.js
runtime.*.js
styles.*.css
`
Package
`
-v.zip
plugin-manifest.json
remoteEntry.js
index.html
main.*.js
polyfills.*.js
runtime.*.js
styles.*.css
3rdpartylicenses.txt
`
---
Navigation and routing
The CLI sets a sensible default so your plugin is routable out of the box:
`json
{
"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
$3
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:
1. Add standalone: true to the component
2. In src/app/app.module.ts, move AppComponent from declarations to imports
3. Add CommonModule to imports if using ngIf, ngFor, etc.
`typescript
@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.
$3
- Build succeeds but no remoteEntry.js: ensure @angular-builders/custom-webpack:browser builder is used and customWebpackConfig: { path: 'webpack.config.js' } is set in angular.json; ensure webpack.config.js applies withModuleFederationPlugin correctly.
- Publish fails with API access: confirm PLAZA_API_URL (or pass --api-url), and that you are logged in (plaza-plugin auth:login).
- Invalid content type on upload: the CLI uploads remoteEntry.js with application/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)
1) Local dev with ESM container (recommended):
- In the plugin folder, run: npm run start:esm (serves remoteEntry.mjs on port 4201)
- In host routes: loadRemoteModule({ type: 'module', remoteEntry: 'http://localhost:4201/remoteEntry.mjs', exposedModule: './PluginModule' }).then(m => m.PluginModule)
2) Load metadata upfront before Angular bootstraps (recommended):
- In main.ts, call loadRemoteEntry({ type: 'module', remoteEntry }) before importing bootstrap.
3) 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.
$3
The generator configures the remote to share Angular and runtime libraries as singletons via Module Federation. Key points:
- webpack.config.js uses withModuleFederationPlugin and share({...}) for:
- @angular/core, @angular/common, @angular/common/http, @angular/router, @angular/forms, @angular/platform-browser, @angular/platform-browser-dynamic, rxjs, and zone.js.
- The scaffold does not generate any BrowserModule-based application module. For local dev, it uses bootstrapApplication() on the standalone component in src/bootstrap.ts (no BrowserModule, no bootstrapModule).
- The exposed MF entry is ./PluginModule (src/app/plugin.module.ts) which:
- imports CommonModule
- wires RouterModule.forChild([{ path: '', component:
- re-exports your standalone component so a host can optionally load the component directly.
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
`bash
plaza-plugin auth:register --api-url https://your-api \
--full-name "Jane Dev" --email jane@example.com --password secret123 --company Acme
`
Login and save JWT locally (~/.plaza/config.json)
`bash
plaza-plugin auth:login --api-url https://your-api \
--email jane@example.com --password secret123
`
Logout and clear saved token
`bash
plaza-plugin auth:logout
`
Contributing
1. Create a feature branch
2. Run npm run build` before committing