Create a new package in the Umbraco backoffice client with its first module. Use when adding a new domain feature area that needs its own package under src/packages/ — e.g., a new CMS feature like data types, tags, or relations. Packages are self-contained domain modules that can theoretically be uninstalled independently. Also use when the user says things like "scaffold a new package", "add a new feature package", or "create a new domain module".
Create a new self-contained package under src/packages/ with its first module.
Read these docs before creating a package — they define the conventions this skill builds on:
data-types, languages, tags, documents, relations)data-type, language, tag, document, relation)data-types, languages, tags) — kebab-casedata-type, language, tag)add-core-module skill instead.src/packages/{package-name}/
├── package.json # npm workspace package
├── vite.config.ts # Build configuration
├── umbraco-package.ts # Bundle entry point (lazy-loads manifests)
├── manifests.ts # Aggregates all module manifests
├── {module-name}/ # First module
│ ├── index.ts # Public API exports
│ ├── manifests.ts # Module manifest registrations
│ ├── constants.ts # Module constants
│ └── types.ts # Module types
Every package is an npm workspace. It needs a minimal package.json:
{
"name": "@umbraco-backoffice/{package-name}",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "vite build"
}
}
Non-core packages use the simple default config with three entry points (index.ts, manifests.ts, umbraco-package.ts):
import { defineConfig } from 'vite';
import { rmSync } from 'fs';
import { getDefaultConfig } from '../../vite-config-base';
const dist = '../../../dist-cms/packages/{package-name}';
// delete the unbundled dist folder
rmSync(dist, { recursive: true, force: true });
export default defineConfig({
...getDefaultConfig({ dist }),
});
The bundle entry point that lazy-loads the package's manifests:
export const name = 'Umbraco.Core.{PackageName}';
export const extensions = [
{
name: '{PackageName} Bundle',
alias: 'Umb.Bundle.{PackageName}',
type: 'bundle',
js: () => import('./manifests.js'),
},
];
Convention: name uses Umbraco.Core.{PascalCaseName}, alias uses Umb.Bundle.{PascalCaseName}.
Aggregates all module manifests:
import { manifests as {moduleName}Manifests } from './{module-name}/manifests.js';
export const manifests: Array<UmbExtensionManifest> = [
...{moduleName}Manifests,
];
As you add more modules, import and spread their manifests here.
export const manifests: Array<UmbExtensionManifest> = [
// Extension registrations go here as you build features
];
// Public API exports — classes, types, constants needed by other packages
// Module constants — aliases, entity types, context tokens
// Module type definitions — domain models, config interfaces
Three registrations are needed to wire the package into the build:
In /src/Umbraco.Web.UI.Client/package.json, add to the exports field:
"./{package-name}": "./dist-cms/packages/{package-name}/index.js"
npm run generate:tsconfig
This updates tsconfig.json paths so @umbraco-cms/backoffice/{package-name} resolves correctly.
npm install
This registers the new workspace package with npm.
src/packages/data-type/ — straightforward entity with CRUDsrc/packages/documents/ — multiple modules, rich entitysrc/packages/tags/ — small feature areapackage.json created with "type": "module" and build scriptvite.config.ts created using getDefaultConfig()umbraco-package.ts created with bundle entry pointmanifests.ts created at package level, aggregating module manifestsmanifests.ts, index.ts, constants.ts, types.tspackage.jsonnpm run generate:tsconfig executednpm install executed to register the workspacenpm run compile