This is a pnpm monorepo of Cumulocity IoT UI plugins built with Angular 20 and the Cumulocity Web SDK 1023.14.x. All plugin projects are registered in a single root angular.json and share dependencies via pnpm workspaces. Shared logic lives in packages/shared/; individually deployable plugin packages live alongside it under packages/. Unit tests run with Karma + Jasmine via @angular/build:karma. End-to-end tests use Cypress.
cumulocity-ui-toolkit/
├── angular.json # Single Angular workspace – all plugins registered here
├── package.json # Root scripts and shared dependencies
├── pnpm-workspace.yaml # Workspace: packages/* + test/
├── .npmrc # shamefully-hoist=true
├── .pnpmfile.cjs # Forces @c8y/devkit to use TypeScript 5.9.x
├── tsconfig.json # Root TypeScript config with path aliases for shared/
├── karma.conf.js # Shared Karma configuration (Jasmine, Chrome, coverage)
├── eslint.config.mjs # Shared flat ESLint config
│
├── packages/
│ ├── shared/ # Internal library – components, pipes, services, helpers
│ │ ├── ng-package.json # ng-packagr config (builds to FESM2022)
│ │ ├── tsconfig.json
│ │ ├── tsconfig.spec.json
│ │ └── src/
│ │ ├── index.ts # Public API barrel export
│ │ ├── components/
│ │ ├── helpers/
│ │ ├── pipes/
│ │ └── services/
│ │
│ ├── energy-consumption-widget/
│ ├── favorites-manager/
│ ├── kpi-widget/
│ ├── operations-widget/
│ ├── release-notes/
│ ├── reminder/
│ └── tenant-option-management/
│
├── test/ # Cypress E2E suite (separate pnpm workspace)
│ ├── cypress/e2e/ # One spec file per plugin
│ ├── config/ # Per-plugin Cypress configs + base.config.ts
│ └── tsconfig.json
│
└── tools/ # Node.js build utilities
├── generate-scripts.mts # Auto-generates package.json scripts from angular.json
├── generate-assets.js # Generates typed assets.ts barrel from asset folders
├── convert-locales.js # Converts locale files
├── postbuild.js # Post-build orchestration
└── build-rename.js # Renames ZIP archives to include version number
The workspace is configured in pnpm-workspace.yaml:
packages:
- 'packages/*'
- 'test'- Every directory under
packages/is a workspace package, includingshared. test/is a separate workspace containing the Cypress suite — runpnpm installinsidetest/independently when needed..npmrcsetsshamefully-hoist=true, which hoists all packages to the rootnode_modules/. This is required for Angular's build tooling and the Cumulocity devkit to resolve peer dependencies..pnpmfile.cjsoverrides the TypeScript version resolved by@c8y/devkitto match the workspace version (5.9.x), preventing a@ngtools/webpackDebug Failure at build time.
All plugins are registered as Angular projects in the single root angular.json. Projects follow the naming convention plugin.<short-name> (e.g. plugin.favorites, plugin.kpi-widget).
| Angular project | Package directory |
|---|---|
shared |
packages/shared |
plugin.energy-consumption-widget |
packages/energy-consumption-widget |
plugin.favorites |
packages/favorites-manager |
plugin.kpi-widget |
packages/kpi-widget |
plugin.operations-widget |
packages/operations-widget |
plugin.release-notes |
packages/release-notes |
plugin.reminder |
packages/reminder |
plugin.tenant-option-management |
packages/tenant-option-management |
Each plugin project defines four architect targets:
| Target | Builder | Purpose |
|---|---|---|
build |
@c8y/devkit:build |
Produces a deployable ZIP artifact in dist/ |
serve |
@c8y/devkit:dev-server |
Local development server with live reload |
deploy |
@c8y/devkit:deploy |
Deploys the built plugin to a Cumulocity tenant |
test |
@angular/build:karma |
Runs unit tests with Karma + Jasmine |
Each plugin under packages/<name>/ follows this structure:
packages/<name>/
├── package.json # Plugin name + version
├── cumulocity.config.ts # Cumulocity runtime + build-time config (Module Federation, exports)
├── tsconfig.app.json # Extends root tsconfig.json (Angular build)
├── tsconfig.json # Extends root tsconfig.json
├── tsconfig.spec.json # Extends root tsconfig.json (types: jasmine, node)
├── src/
│ ├── main.ts # Angular bootstrap entry point
│ ├── bootstrap.ts # Locale + app bootstrap
│ ├── app/ # Angular application code
│ └── assets/ # Static assets
└── public/ # Additional public assets (some plugins)
All components and pipes in this workspace are standalone: true. There are no NgModules. Plugin entrypoints export provider arrays (e.g. EnergyConsumptionWidgetPluginProviders) instead of @NgModule classes. These provider arrays are consumed directly by the Cumulocity shell's Module Federation loader.
The key Cumulocity-specific config for each plugin. It contains:
runTime: Plugin metadata for the shell — name, version, CSP, andexports/remotesfor Module Federation. Themodulefield in each export entry names the provider array exported from thepathfile.buildTime: Lists packages to federate (shared from the shell, not re-bundled), and asset copy rules.
Widget definitions use hookWidget with loadComponent / loadConfigComponent for lazy loading. Route hooks use hookRoute with loadComponent. Drawer and action hooks use static component references (SDK constraint).
The shared package is a proper workspace library consumed by every plugin:
"dependencies": {
"shared": "workspace:*"
}It is built with ng-packagr (ng build shared) to produce an FESM2022 library with TypeScript declarations. Output goes to packages/shared/dist/.
Defined in the root tsconfig.json:
"paths": {
"~components/*": ["./packages/shared/src/components/*"],
"~helpers/*": ["./packages/shared/src/helpers/*"],
"~models/*": ["./packages/shared/src/models/*"],
"~pipes/*": ["./packages/shared/src/pipes/*"],
"~services/*": ["./packages/shared/src/services/*"]
}Usage in any plugin:
import { LocalStorageService } from '~services/local-storage.service';
import { extractPlaceholdersFromObject } from '~helpers/extract-placeholders';| Folder | Contents |
|---|---|
src/components/ |
Reusable Angular components (auto-refresh, image gallery, etc.) |
src/helpers/ |
Type utilities, domain model helpers, test auto-mock helpers |
src/pipes/ |
Angular pipes (file size, filter, sort, nl2br, replace, etc.) |
src/services/ |
Angular services (local storage, measurements, etc.) |
All commands run from the repository root unless noted.
| Purpose | Command |
|---|---|
| Install dependencies | pnpm install |
| Build all plugins | pnpm run build |
| Build a single plugin | pnpm run build:<name> (e.g. build:kpi-widget) |
| Serve a plugin locally | pnpm run serve:<name> (requires C8Y_BASEURL + C8Y_SHELL_TARGET env vars) |
| Run all unit tests | pnpm test |
| Run tests for one plugin (CI) | pnpm run test:<name> (e.g. test:favorites) |
| Run tests for one plugin (watch) | pnpm run test:watch:<name> |
| Lint | pnpm run lint |
| Lint with auto-fix | pnpm run lint:fix |
| Run Cypress e2e (all) | pnpm run e2e:run |
| Run Cypress e2e (one plugin) | pnpm run e2e:run:<name> |
| Open Cypress UI | pnpm run e2e:open |
| Regenerate package.json scripts | pnpm run generate:scripts |
| Clean dist/ | pnpm run clean |
pnpm run build
└─ prebuild → pnpm run clean (rimraf dist/)
└─ build:reminder → ng build plugin.reminder
└─ build:kpi-widget → ng build plugin.kpi-widget
└─ ...Each ng build <plugin> invokes @c8y/devkit:build, which:
- Compiles the Angular application using the plugin's
tsconfig.app.json. - Applies Module Federation (
buildTime.federationincumulocity.config.ts). - Copies configured assets.
- Packages the output into a ZIP file under
dist/.
tools/postbuild.js and tools/build-rename.js rename ZIP outputs to include the version from the plugin's package.json:
dist/cumulocity-favorites-manager-plugin.zip
→ dist/cumulocity-favorites-manager-plugin_1.0.1.zip
Unit tests use Karma + Jasmine with the @angular/build:karma builder. All configuration is driven by angular.json test targets and the root karma.conf.js.
| File | Scope | Purpose |
|---|---|---|
karma.conf.js |
Root | Shared Karma config: Jasmine framework, Chrome launcher, coverage reporter, ChromeHeadlessCI custom launcher |
tsconfig.spec.json |
Per package | Extends root tsconfig.json; sets types: ["jasmine", "node"] |
angular.json → .architect.test |
Per project | Points to @angular/build:karma, references karma.conf.js and tsconfig.spec.json |
# Run all 8 suites in sequence (76 tests):
pnpm test
# Run a single suite headless (CI mode):
pnpm run test:shared
pnpm run test:energy-consumption-widget
pnpm run test:favorites
pnpm run test:kpi-widget
pnpm run test:operations-widget
pnpm run test:release-notes
pnpm run test:reminder
pnpm run test:tenant-option-management
# Run a single suite in watch mode (development):
pnpm run test:watch:shared
pnpm run test:watch:favorites
# ...
# Run via ng directly:
pnpm exec ng test plugin.kpi-widget --watch=false --browsers=ChromeHeadlessCIThe root karma.conf.js defines a ChromeHeadlessCI custom launcher:
ChromeHeadlessCI: {
base: 'ChromeHeadless',
flags: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'],
}Use --browsers=ChromeHeadlessCI in CI environments and --browsers=ChromeHeadless for local watch runs (watch scripts do this automatically).
E2E tests live in the test/ pnpm workspace and use Cypress with the cumulocity-cypress library.
Required environment variables:
export C8Y_BASEURL=https://<your-tenant>.cumulocity.com
export C8Y_SHELL_TARGET=<app-name> # e.g. cockpit2025
export C8Y_USERNAME=<username>
export C8Y_PASSWORD=<password>
export C8Y_TENANT=<tenant-id> # optional for single-tenant# Run all E2E specs:
pnpm run e2e:run
# Run per plugin:
pnpm run e2e:run:favorites-manager
pnpm run e2e:run:kpi-widget
pnpm run e2e:run:energy-consumption-widget
pnpm run e2e:run:operations-widget
pnpm run e2e:run:release-notes
pnpm run e2e:run:reminder
pnpm run e2e:run:tenant-option-management
# Open Cypress interactive runner:
pnpm run e2e:open
pnpm run e2e:open:favorites-managerCypress configs in test/config/ import the corresponding cumulocity.config.ts from each package and pass runTime.remotes as the C8Y_SHELL_EXTENSION variable, so the correct Module Federation remotes are loaded automatically.
Plugin-specific serve:*, build:*, test:*, and test:watch:* scripts in the root package.json are auto-generated by tools/generate-scripts.mts. Re-run whenever a plugin is added to or removed from angular.json:
pnpm run generate:scripts
# internally: node --experimental-strip-types tools/generate-scripts.mtsThe generator reads all projects from angular.json. For each plugin.<short> project that has a @angular/build:karma test target it emits:
"test:<short>": "ng test plugin.<short> --watch=false --browsers=ChromeHeadlessCI",
"test:watch:<short>": "ng test plugin.<short> --browsers=ChromeHeadless"The root test script (which chains all suites) is also regenerated automatically. Do not hand-edit the generated section of package.json — it is keyed by the --generated---------- marker and overwritten on every run.
tools/generate-assets.js creates a typed assets.ts barrel from a folder of static assets:
node tools/generate-assets.js <folderPath>It scans the directory recursively and generates import statements plus a nested assets object, giving type-safe access to asset paths throughout the plugin.
- Create
packages/<name>/withpackage.json,cumulocity.config.ts,tsconfig.app.json,tsconfig.json, andsrc/. - Register the project in
angular.jsonasplugin.<name>following the existing patterns. Include atestarchitect target using@angular/build:karmaand pointing tokarma.conf.jsand the package'stsconfig.spec.json. - Add
tsconfig.spec.json— extend roottsconfig.json, settypes: ["jasmine", "node"]. - Run
pnpm run generate:scriptsto regeneratebuild:*,serve:*,test:*, andtest:watch:*scripts. - Optionally add a Cypress config to
test/config/<name>.config.tsand ane2e:run:<name>script.
Dive into Cumulocity tutorials and articles in the TECHcommunity Knowledge Base.
Ask the Cumulocity experts on the TECHcommunity Forums.
See Cumulocity in action with a Free Trial.
If you find a bug, please create an issue. For ideas or feedback, post in the Tech Forums.
- Cumulocity IoT Web Development Tutorial – Part 1: Start your journey
- How to install a Microfrontend Plugin on a tenant and use it in an app?
- The power of micro frontends – How to dynamically extend Cumulocity IoT Frontends
This toolkit is provided as-is and without warranty or support. It does not constitute part of the Cumulocity product suite. Users are free to use, fork and modify it, subject to the license agreement. While Cumulocity welcomes contributions, we cannot guarantee to include every contribution in the master project.
