npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@techhalo/halo-plugin

v1.0.3

Published

CLI for developing, building, and publishing Module Federation-based Halo plugins

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-plugin

Quick 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 publish

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
halo-biometric-provider not defined after UMD load

For 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 upload

cover: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.png so 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

  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

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 rotate

This 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 into assets/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.ts exposed as ./PluginModule
  • Manifest: plugin-manifest.json with:
    • type: 'angular-mf'
    • entry: { remoteName, remoteEntry, exposedModule }
    • navigation: { routes: [{ path, label }] }
  • Assets folder with assets/cover.png placeholder 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 into assets/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.png placeholder 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, --prod Production build (recommended)
  • -k, --private-key <path> Sign manifest with provided private key

Outputs

  • Angular build in dist/<plugin-name>/ and must include remoteEntry.js
  • Versioned package: <plugin-name>-v<version>.zip in 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)
  • --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 <name>-v<version>.zip
  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)
  1. Uploads remoteEntry.js to POST /api/plugins/{pluginId}/files/upload with:
  • multipart file (content type application/javascript)
  • type = angular-bundle, version, optional description
  1. Zips and uploads remaining build artifacts (excluding remoteEntry.js) to POST /api/plugins/{pluginId}/files/artifacts
  2. Prints created plugin id and Remote Entry URL

Authentication

  • Run halo-plugin auth:login once to save your JWT locally; all catalogue API calls use it automatically.
  • Use halo-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

  • 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/signature
  • module-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 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

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 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:

{
  "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.*.css

Package

<plugin-name>-v<version>.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:

{
  "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:

  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.
@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: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 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.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)
  1. Load metadata upfront before Angular bootstraps (recommended):

    • In main.ts, call loadRemoteEntry({ type: 'module', remoteEntry }) before importing bootstrap.
  2. 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.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: <YourComponent> }])
    • 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

halo-plugin auth:register --api-url https://your-api \
  --full-name "Jane Dev" --email [email protected] --password secret123 --company Acme

Login and save JWT locally (~/.halo/config.json)

halo-plugin auth:login --api-url https://your-api \
  --email [email protected] --password secret123

Logout and clear saved token

halo-plugin auth:logout

Contributing

  1. Create a feature branch
  2. Run npm run build before committing
  3. 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