| name | pimcore-studio-ui-using-sdk-in-bundles |
| description | How bundles consume the Pimcore Studio UI SDK - plugins, modules, DI, registries, and imports |
| metadata | {"audience":"pimcore-developers","focus":"sdk-architecture"} |
What This Skill Covers
Core principles for consuming the Pimcore Studio UI SDK in bundles:
- Plugin and module system
- Dependency injection with Inversify
- SDK imports and available APIs
- Extension registries (components, widgets, menus, types)
- Module federation architecture
When to Use This Skill
Use this when:
- Creating a new Pimcore Studio bundle
- Understanding how to extend Studio functionality
- Working with plugins and modules
- Accessing SDK services and components
- Registering UI elements (widgets, tabs, menus)
- Need to understand the overall architecture
Core Architecture Principles
Module Federation
Pimcore Studio uses Webpack Module Federation to enable dynamic loading of bundles:
- Studio Core = Host application (loads at startup)
- Bundles = Remote modules (loaded dynamically at runtime)
- Shared Dependencies = React, Ant Design, Inversify (singletons, one instance across all bundles)
Key Benefit: Bundles are independently built and loaded, but share common dependencies.
The Extension Flow
Bundle Build → Entrypoint Registration → Runtime Loading → Plugin Execution → Modules Register → UI Renders
- Bundle compiles assets with Module Federation
- PHP service registers entrypoint location
- Studio core discovers and loads the remote module
- Bundle's
plugins.ts exports are discovered
- Plugins execute lifecycle hooks (
onInit, onStartup)
- Modules register configurations
- React renders with extended functionality
Plugins vs Modules
Plugins - The Lifecycle Integration
Plugins are entry points that hook into the application lifecycle:
import { type IAbstractPlugin } from '@pimcore/studio-ui-bundle'
export const MyPlugin: IAbstractPlugin = {
name: 'MyPlugin',
onInit: ({ container }) => {
container.rebind(serviceIds['Some/Service'])
.to(MyCustomService)
.inSingletonScope()
},
onStartup: ({ moduleSystem }) => {
moduleSystem.registerModule(MyModule)
}
}
When to use each hook:
onInit: Register NEW services or OVERRIDE existing ones
onStartup: Register modules that CONFIGURE existing services
Modules - The Configuration Executors
Modules are configuration snippets that run after services are initialized but before React renders:
import { type AbstractModule, container } from '@pimcore/studio-ui-bundle'
import { serviceIds } from '@pimcore/studio-ui-bundle/app'
export const MyModule: AbstractModule = {
onInit: () => {
const widgetRegistry = container.get<WidgetRegistry>(serviceIds.widgetManager)
widgetRegistry.registerWidget({
name: 'my-widget',
component: MyWidgetComponent
})
}
}
Common module tasks:
- Register widgets in WidgetManager
- Register tabs in TabManagers
- Add components to ComponentRegistry
- Register context menu items
- Configure dynamic types
Mental Model
Plugin = "When and how to integrate with Studio lifecycle"
Module = "What to configure once services are ready"
One plugin can register multiple modules - organize by feature or concern.
Dependency Injection (Inversify)
All services are managed by an IoC (Inversion of Control) container.
Accessing Services in Modules/Plugins
import { container } from '@pimcore/studio-ui-bundle'
import { serviceIds } from '@pimcore/studio-ui-bundle/app'
const widgetRegistry = container.get<WidgetRegistry>(
serviceIds.widgetManager
)
Accessing Services in React Components
import { useInjection } from '@pimcore/studio-ui-bundle/app'
import { serviceIds } from '@pimcore/studio-ui-bundle/app'
const MyComponent = () => {
const widgetManager = useInjection<WidgetManager>(
serviceIds['Widget/WidgetManager']
)
}
Overriding Services (Advanced)
Replace core services with custom implementations:
onInit: ({ container }) => {
container.rebind(serviceIds['Asset/Editor/FolderTabManager'])
.to(CustomFolderTabManager)
.inSingletonScope()
}
Key Principle: All services are registered with string IDs (serviceIds), enabling type-safe access and easy replacement.
SDK Imports - The Public API
🚨 Import Paths
BEFORE writing any import, read CRITICAL-IMPORT-PATHS.md.
All examples below use bundle imports (@pimcore/studio-ui-bundle/*). The @sdk/* alias maps to the same code but is only available inside studio-ui-bundle core.
Core Imports
import {
type IAbstractPlugin,
type AbstractModule,
container
} from '@pimcore/studio-ui-bundle'
import {
serviceIds,
useInjection,
store,
router
} from '@pimcore/studio-ui-bundle/app'
Component Imports
import {
Content,
Header,
Sidebar
} from '@pimcore/studio-ui-bundle/components'
API Imports (RTK Query)
import {
useAssetGetByIdQuery,
useAssetUpdateMutation
} from '@pimcore/studio-ui-bundle/api/asset'
import {
useDataObjectGetByIdQuery
} from '@pimcore/studio-ui-bundle/api/data-object'
Module-Specific Imports
import { type AssetEditorTabManager } from '@pimcore/studio-ui-bundle/modules/asset'
import { type WidgetRegistry } from '@pimcore/studio-ui-bundle/modules/widget-manager'
Utility Imports
import { formatters } from '@pimcore/studio-ui-bundle/utils'
import { generateUuid } from '@pimcore/studio-ui-bundle/utils'
Import Principle: Folder structure mirrors import paths
- SDK code location:
studio-ui-bundle/assets/js/src/core/app/sdk/api/asset/index.ts
- Bundle import:
@pimcore/studio-ui-bundle/api/asset
- Core import:
@sdk/api/asset
The @sdk/* alias (core only) and @pimcore/studio-ui-bundle/* (bundles) both point to the same SDK folder.
Extension Registries
ComponentRegistry
Manages React components with two patterns:
1. Single Components (unique, one per slot):
componentRegistry.override('asset-toolbar-actions', MyToolbarComponent)
2. Slots (multiple components with priority):
componentRegistry.registerToSlot({
slot: 'data-object-context-menu',
component: MyMenuItemComponent,
priority: 10
})
Extension points are defined in component-config.ts - these are the available slots.
WidgetManager
Manages widgets in 4 areas: main, left, bottom, right
widgetRegistry.registerWidget({
name: 'my-widget',
component: MyWidgetComponent
})
const widgetManager = useInjection<WidgetManager>(
serviceIds['Widget/WidgetManager']
)
widgetManager.open({
name: 'my-widget',
config: { }
})
ContextMenuRegistry
Centralized context menu management:
contextMenuRegistry.registerToSlot({
slot: 'tree-node-context-menu',
component: MyMenuItem,
priority: 100
})
contextMenuRegistry.overrideSlotProvider('grid-row-context-menu', MyMenuProvider)
Priority system controls menu item order (higher = appears first).
Dynamic Types
Abstract base classes for extensible type systems (grid cells, layouts, metadata, filters, etc.).
@injectable()
class CustomCellType extends DynamicTypeGridCellAbstract {
id = 'custom-cell'
getGridCellComponent(props) {
return <CustomCellComponent {...props} />
}
}
gridCellRegistry.registerDynamicType(container.get('DynamicTypes/GridCell/CustomCell'))
For details, see the pimcore-studio-dynamic-types skill.
Bundle Structure Example
import { type IAbstractPlugin } from '@pimcore/studio-ui-bundle'
import { MyModule } from './modules/my-module'
export const MyPlugin: IAbstractPlugin = {
name: 'MyPlugin',
onStartup({ moduleSystem }) {
moduleSystem.registerModule(MyModule)
}
}
import { type AbstractModule, container } from '@pimcore/studio-ui-bundle'
import { serviceIds } from '@pimcore/studio-ui-bundle/app'
import { type WidgetRegistry } from '@pimcore/studio-ui-bundle/modules/widget-manager'
import { MyWidget } from '../components/my-widget'
export const MyModule: AbstractModule = {
onInit: () => {
const widgetRegistry = container.get<WidgetRegistry>(
serviceIds.widgetManager
)
widgetRegistry.registerWidget({
name: 'my-widget',
component: MyWidget
})
}
}
import React from 'react'
import { useInjection } from '@pimcore/studio-ui-bundle/app'
import { serviceIds } from '@pimcore/studio-ui-bundle/app'
export const MyWidget = () => {
const someService = useInjection<SomeService>(serviceIds.someService)
return <div>My Widget</div>
}
Common Patterns
Registering a New Tab
export const TabExtension: AbstractModule = {
onInit: () => {
const tabManager = container.get<AssetEditorTabManager>(
serviceIds['Asset/Editor/AssetEditorTabManager']
)
tabManager.register({
key: 'my-tab',
label: 'My Tab',
component: MyTabComponent
})
}
}
Adding a Toolbar Button
export const ToolbarExtension: AbstractModule = {
onInit: () => {
const componentRegistry = container.get<ComponentRegistry>(
serviceIds.componentRegistry
)
componentRegistry.registerToSlot({
slot: 'asset-editor-toolbar',
component: MyButton,
priority: 50
})
}
}
Creating a Navigation Entry
export const NavExtension: AbstractModule = {
onInit: () => {
const mainNavRegistry = container.get<MainNavRegistry>(
serviceIds.mainNavRegistry
)
mainNavRegistry.registerMainNavItem({
path: 'My Tool/Sub Section',
widgetConfig: {
name: 'My Tool',
id: 'my-tool',
component: 'my-tool-widget'
}
})
}
}
Using RTK Query API
import { useAssetGetByIdQuery } from '@pimcore/studio-ui-bundle/api/asset'
const MyComponent = ({ assetId }: { assetId: number }) => {
const { data, isLoading, error } = useAssetGetByIdQuery({ id: assetId })
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error</div>
if (!data) return null
return <div>{data.filename}</div>
}
Key Principles
- Separation of Concerns: Plugins (lifecycle) → Modules (configuration) → Components (render)
- Inversion of Control: DI container manages all services, enabling easy replacement
- Open/Closed: Core is closed for modification, open for extension via registries
- Type Safety: Full TypeScript support with auto-generated types from OpenAPI
- Configuration over Code: Declarative registration patterns instead of imperative code
- Progressive Enhancement: Bundles extend without modifying core code
Common Mistakes to Avoid
❌ Don't import from internal paths
import { Something } from '@pimcore/studio-ui-bundle/src/internal/path'
✅ Only import from public API paths
import { Something } from '@pimcore/studio-ui-bundle/components'
❌ Don't access services outside DI container
const service = new SomeService()
✅ Always get services from container
const service = container.get<SomeService>(serviceIds.someService)
❌ Don't configure in onInit of plugin
onInit: ({ container }) => {
const registry = container.get<Registry>(...)
registry.register(...)
}
✅ Configure in modules (via onStartup)
onStartup: ({ moduleSystem }) => {
moduleSystem.registerModule(MyConfigModule)
}
Reference
Next Steps