docs: Add CLAUDE.md documentation for key .NET projects (#20841)

* docs: Add CLAUDE.md documentation for key .NET projects

Add comprehensive CLAUDE.md files for major Umbraco projects:
- Root CLAUDE.md: Multi-project repository overview
- Umbraco.Core: Interface contracts and domain models
- Umbraco.Infrastructure: Implementation layer (NPoco, migrations, services)
- Umbraco.Cms.Api.Common: Shared API infrastructure
- Umbraco.Cms.Api.Management: Management API (1,317 files, 54 domains)
- Umbraco.Web.UI.Client: Frontend with split docs structure

Each file includes:
- Architecture and design patterns
- Project-specific workflows
- Edge cases and gotchas
- Commands and setup
- Technical debt tracking

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Update src/Umbraco.Cms.Api.Management/CLAUDE.md

Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Andy Butland <abutland73@gmail.com>
Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>

* docs: Update CLAUDE.md with accurate persistence and auth info

- Clarify NPoco is current and fully supported (not legacy)
- Document EF Core as future direction with ongoing migration
- Add secure cookie-based token storage details for v17+
- Update OpenIddict authentication documentation
- Update API versioning (v1.0 and v1.1)
- Minor documentation cleanups (community links, descriptions)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Update src/Umbraco.Web.UI.Client/docs/agentic-workflow.md

* Update src/Umbraco.Web.UI.Client/docs/agentic-workflow.md

* Apply suggestions from code review

* Clarifications and duplicate removal.

---------

Co-authored-by: Phil Whittaker <pjw@umbraco.dk>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Co-authored-by: Andy Butland <abutland73@gmail.com>
Co-authored-by: Sven Geusens <geusens@gmail.com>
This commit is contained in:
hifi-phil
2025-11-25 14:37:49 +00:00
committed by GitHub
parent 1240d845d4
commit ef282a5211
15 changed files with 5537 additions and 0 deletions

View File

@@ -0,0 +1,89 @@
# Umbraco Backoffice - @umbraco-cms/backoffice
Modern TypeScript/Lit-based web components library for the Umbraco CMS backoffice interface. This project provides extensible UI components, APIs, and utilities for building the Umbraco CMS administration interface.
**Package**: `@umbraco-cms/backoffice`
**Version**: 17.1.0-rc
**License**: MIT
**Repository**: https://github.com/umbraco/Umbraco-CMS
**Live Preview**: https://backofficepreview.umbraco.com/
---
## Documentation Structure
This project's documentation is organized into 9 focused guides:
**Note**: This is a sub-project in the Umbraco CMS monorepo. For Git workflow, PR process, and CI/CD information, see the [repository root CLAUDE.md](../../CLAUDE.md).
### Architecture & Design
- **[Architecture](./docs/architecture.md)** - Technology stack, design patterns, module organization
### Development
- **[Commands](./docs/commands.md)** - Build, test, and development commands
### Code Quality
- **[Style Guide](./docs/style-guide.md)** - Naming and formatting conventions
- **[Clean Code](./docs/clean-code.md)** - Best practices and SOLID principles
- **[Testing](./docs/testing.md)** - Unit, integration, and E2E testing strategies
### Troubleshooting
- **[Error Handling](./docs/error-handling.md)** - Error patterns and debugging
- **[Edge Cases](./docs/edge-cases.md)** - Common pitfalls and gotchas
### Security & AI
- **[Security](./docs/security.md)** - XSS prevention, authentication, input validation
- **[Agentic Workflow](./docs/agentic-workflow.md)** - Three-phase AI development process
---
## Quick Start
### Prerequisites
- **Node.js**: >=22.17.1
- **npm**: >=10.9.2
- Modern browser (Chrome, Firefox, Safari)
### Initial Setup
```bash
# 1. Clone repository
git clone https://github.com/umbraco/Umbraco-CMS.git
cd Umbraco-CMS/src/Umbraco.Web.UI.Client
# 2. Install dependencies
npm install
# 3. Start development
npm run dev
```
### Most Common Commands
See **[Commands](./docs/commands.md)** for all available commands.
**Development**: `npm run dev` | **Testing**: `npm test` | **Build**: `npm run build` | **Lint**: `npm run lint:fix`
---
## Quick Reference
| Category | Details |
|----------|---------|
| **Apps** | `src/apps/` - Application entry points (app, installer, upgrader) |
| **Libraries** | `src/libs/` - Core APIs (element-api, context-api, controller-api) |
| **Packages** | `src/packages/` - Feature packages; `src/packages/core/` for utilities |
| **External** | `src/external/` - Dependency wrappers (lit, rxjs, luxon) |
| **Mocks** | `src/mocks/` - MSW handlers and mock data |
| **Config** | `package.json`, `vite.config.ts`, `.env` (create `.env.local`) |
| **Elements** | Custom elements use `umb-{feature}-{component}` pattern |
### Getting Help
**Documentation**: [UI API Docs](npm run generate:ui-api-docs) | [Storybook](npm run storybook) | [Official Docs](https://docs.umbraco.com/)
**Community**: [Issues](https://github.com/umbraco/Umbraco-CMS/issues) | [Discussions](https://github.com/umbraco/Umbraco-CMS/discussions) | [Forum](https://our.umbraco.com/)
---
**This project follows a modular package architecture with strict TypeScript, Lit web components, and an extensible manifest system. Each package is independent but follows consistent patterns. For extension development, use the Context API for dependency injection, controllers for logic, and manifests for registration.**

View File

@@ -0,0 +1,517 @@
# Agentic Workflow
[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md)
---
### Phase 1: Analysis & Planning
**Understand Requirements**:
1. Read user request carefully
2. Identify acceptance criteria
3. Ask clarifying questions if ambiguous
4. Determine scope (new feature, bug fix, refactor, etc.)
**Research Codebase**:
1. Find similar implementations in codebase
2. Identify patterns and conventions used
3. Locate relevant packages and modules
4. Review existing tests for similar features
5. Check for existing utilities or helpers
**Identify Technical Approach**:
1. Which packages need changes?
- New package or modify existing?
- Core infrastructure or feature package?
2. What patterns to use?
- Web Component, Controller, Repository, Context?
- Extension type? (dashboard, workspace, modal, etc.)
3. Dependencies needed?
- New npm packages?
- Internal package dependencies?
4. API changes needed?
- New OpenAPI endpoints?
- Changes to existing endpoints?
**Break Down Implementation**:
1. **Models/Types** - Define TypeScript interfaces and types
2. **API Client** - Update OpenAPI client if backend changes
3. **Repository** - Data access layer
4. **Store/Context** - State management
5. **Controller** - Business logic
6. **Element** - UI component
7. **Manifest** - Extension registration
8. **Tests** - Unit and integration tests
9. **Documentation** - JSDoc and examples
10. **Storybook** (if applicable) - Component stories
**Consider Architecture**:
- Does this follow project patterns?
- Are dependencies correct? (libs → packages → apps)
- Will this create circular dependencies?
- Is this extensible for future needs?
- Performance implications?
**Document Plan**:
- Write brief implementation plan
- Identify potential issues or blockers
- Get approval from user if significant changes
### Phase 2: Incremental Implementation
**For New Feature (Component/Package)**:
**Step 1: Define Types**
```typescript
// 1. Create model interface
export interface UmbMyModel {
id: string;
name: string;
description?: string;
}
// 2. Create manifest type
export interface UmbMyManifest extends UmbManifestBase {
type: 'myType';
// ... specific properties
}
```
**Verify**: TypeScript compiles, no errors
**Step 2: Create Repository**
```typescript
// 3. Data access layer
export class UmbMyRepository {
async requestById(id: string) {
// Fetch from API
// Return { data } or { error }
}
}
```
**Verify**: Repository compiles, basic structure correct
**Step 3: Create Store/Context**
```typescript
// 4. State management
export class UmbMyStore extends UmbStoreBase {
// Observable state
}
export const UMB_MY_CONTEXT = new UmbContextToken<UmbMyContext>('UmbMyContext');
export class UmbMyContext extends UmbControllerBase {
#repository = new UmbMyRepository();
#store = new UmbMyStore(this);
// Public API
}
```
**Verify**: Context compiles, can be consumed
**Step 4: Create Element**
```typescript
// 5. UI component
@customElement('umb-my-element')
export class UmbMyElement extends UmbLitElement {
#context?: typeof UMB_MY_CONTEXT.TYPE;
constructor() {
super();
this.consumeContext(UMB_MY_CONTEXT, (context) => {
this.#context = context;
});
}
render() {
return html`<div>My Element</div>`;
}
}
```
**Verify**: Element renders in browser, no console errors
**Step 5: Wire Up Interactions**
```typescript
// 6. Connect user interactions to logic
@customElement('umb-my-element')
export class UmbMyElement extends UmbLitElement {
@state()
private _data?: UmbMyModel;
async #handleClick() {
const { data, error } = await this.#context?.load();
if (error) {
this._error = error;
return;
}
this._data = data;
}
render() {
return html`
<uui-button @click=${this.#handleClick} label="Load"></uui-button>
${this._data ? html`<p>${this._data.name}</p>` : nothing}
`;
}
}
```
**Verify**: Interactions work, data flows correctly
**Step 6: Add Extension Manifest**
```typescript
// 7. Register extension
export const manifest: UmbManifestMyType = {
type: 'myType',
alias: 'My.Extension',
name: 'My Extension',
element: () => import('./my-element.element.js'),
};
```
**Verify**: Extension loads, manifest is valid
**Step 7: Write Tests**
```typescript
// 8. Unit tests
describe('UmbMyElement', () => {
it('should render', async () => {
const element = await fixture(html`<umb-my-element></umb-my-element>`);
expect(element).to.exist;
});
it('should load data', async () => {
// Test data loading
});
});
```
**Verify**: Tests pass
**Step 8: Add Error Handling**
```typescript
// 9. Handle errors gracefully
async #handleClick() {
try {
this._loading = true;
const { data, error } = await this.#context?.load();
if (error) {
this._error = 'Failed to load data';
return;
}
this._data = data;
} catch (error) {
this._error = 'Unexpected error occurred';
console.error('Load failed:', error);
} finally {
this._loading = false;
}
}
```
**Step 9: Add localization**
```typescript
// 10. Add to src/Umbraco.Web.UI.Client/src/assets/lang/en.ts and other appropriate files
{
actions: {
load: 'Load'
}
}
// 11. Use the localize helper (`this.localize.term()`) in the element
render() {
return html`
<uui-button @click=${this.#handleClick} label=${this.localize.term('actions_load')></uui-button>
${this._data ? html`<p>${this._data.name}</p>` : ''}
`;
}
async #handleClick() {
try {
this._loading = true;
const { data, error } = await this.#context?.load();
if (error) {
this._error = this.localize.term('errors_receivedErrorFromServer');
return;
}
this._data = data;
} catch (error) {
this._error = this.localize.term('errors_defaultError');
console.error('Load failed:', error);
} finally {
this._loading = false;
}
}
// 12. Outside elements (such as controllers), use the Localization Controller
export class UmbMyController extends UmbControllerBase {
#localize = new UmbLocalizationController(this);
#notificationContext?: typeof UMB_NOTIFICATION_CONTEXT.TYPE;
constructor(host: UmbControllerHost) {
super(host);
this.consumeContext(UMB_NOTIFICATION_CONTEXT, (notificationContext) => {
this.#notificationContext = notificationContext;
});
}
fetchData() {
// Show error
this.#notificationContext?.peek('positive', {
data: {
headline: this.#localize.term('speechBubbles_onlineHeadline'),
message: this.#localize.term('speechBubbles_onlineMessage'),
},
});
}
}
**Verify**: Errors are handled, UI shows error state
**After Each Step**:
- Code compiles (no TypeScript errors)
- Tests pass (existing and new)
- Follows patterns (consistent with codebase)
- No breaking changes (or documented)
- ESLint passes
- Commit working code
### Phase 3: Review & Refinement
**Run All Quality Checks**:
```bash
# 1. Run all tests
npm test
# 2. Run lint
npm run lint:errors
# 3. Type check
npm run compile
# 4. Build
npm run build
# 5. Check circular dependencies
npm run check:circular
# 6. Validate package exports
npm run package:validate
```
**Code Review Checklist**:
- [ ] Code follows style guide
- [ ] All tests pass
- [ ] New tests added for new code
- [ ] TypeScript types are correct
- [ ] No `any` types (or justified)
- [ ] JSDoc comments on public APIs
- [ ] Error handling in place
- [ ] Edge cases handled
- [ ] No console.log in production code
- [ ] Accessibility considered (for UI components)
- [ ] Performance acceptable
- [ ] No memory leaks (subscriptions cleaned up)
- [ ] Follows existing patterns
- [ ] No circular dependencies introduced
**Documentation**:
- [ ] JSDoc on public APIs
- [ ] Update README if needed
- [ ] Add Storybook story (for components)
- [ ] Add example code (for APIs)
- [ ] Update CLAUDE.md (for significant changes)
### Reasoning & Decision-Making
**Architecture Decisions**:
- **Follow existing patterns** - Don't invent new patterns unless necessary
- **Use Context API** - For cross-component state and dependencies
- **Controllers for logic** - Separate business logic from UI
- **Repositories for data** - Abstract data access
- **Manifests for extensions** - Register features via extension system
**Component Design Decisions**:
- **Small components** - Break complex UI into smaller components
- **Single responsibility** - Each component does one thing well
- **Composition** - Combine small components to build complex UI
- **Props down, events up** - Data flows down via properties, changes flow up via events
**State Management Decisions**:
- **Local state** - `@state()` for component-specific state
- **Shared state** - Context and stores for cross-component state
- **Observables** - RxJS for reactive data streams
- **Immutability** - Don't mutate state, create new objects
**API Design Decisions**:
- **Async by default** - All data operations are async
- **Error objects** - Return `{ data, error }` not throw exceptions
- **TypeScript types** - Strong typing for all APIs
- **Consistent naming** - Follow existing naming conventions
### Think-Aloud Process
**Example: "I need to add a bulk action for content"**
**Analysis**:
> "I need to add a bulk action for content. Let me think through this:
>
> 1. What is a bulk action? It's an action that operates on multiple items.
> 2. Where do bulk actions appear? In collection views (document tree, list view).
> 3. What pattern is used? Let me check existing bulk actions...
> 4. Found: Block editor has bulk actions. Let me review that code.
> 5. Architecture: Bulk actions are registered via manifests (type: 'entityBulkAction')
> 6. Implementation needs:
> - Manifest to register action
> - Element to show confirmation dialog
> - Repository method to perform bulk operation
> - Permission check (can user do this?)
> 7. Which package? This goes in `packages/documents/documents/` since it's document-specific.
> 8. Dependencies: Need `@umbraco-cms/backoffice/entity-bulk-action`, modal, repository."
**Planning**:
> "Implementation steps:
> 1. Create bulk action manifest in documents package
> 2. Create modal element for confirmation
> 3. Add repository method for bulk operation
> 4. Add permission check
> 5. Wire up action to modal
> 6. Test with multiple documents
> 7. Handle errors (partial success, permissions, etc.)
> 8. Add loading state
> 9. Show success/error notification
> 10. Write tests"
**Implementation**:
> "Starting with step 1: Create manifest.
> Looking at existing bulk actions, I see the pattern uses `UmbEntityBulkActionBase`.
> Let me create the manifest following that pattern..."
### Error Recovery
**If Tests Fail**:
1. Read the error message carefully
2. Reproduce the failure locally
3. Debug with console.log or debugger
4. Understand root cause (don't just fix symptoms)
5. Fix the underlying issue
6. Verify fix doesn't break other tests
7. Add test for the bug (if it was a real bug)
**If Architecture Feels Wrong**:
1. Pause and reconsider
2. Review similar implementations in codebase
3. Discuss with team (via PR comments)
4. Don't force a pattern that doesn't fit
5. Refactor if needed
**If Introducing Breaking Changes**:
1. Discuss with user first
2. Document the breaking change
3. Provide migration path
4. Update CHANGELOG
5. Consider deprecation instead
**If Stuck**:
1. Ask for clarification from user
2. Review documentation
3. Look at similar code in repository
4. Break problem into smaller pieces
5. Try a simpler approach first
### Quality Gates Checklist
Before marking work as complete:
**Code Quality**:
- [ ] All new code has unit tests
- [ ] Integration tests for workflows (if applicable)
- [ ] All tests pass (including existing tests)
- [ ] Code follows style guide (ESLint passes)
- [ ] Prettier formatting correct
- [ ] No TypeScript errors
- [ ] JSDoc comments on public functions
- [ ] No `console.log` in production code
- [ ] No commented-out code
**Security**:
- [ ] Input validation in place
- [ ] No XSS vulnerabilities
- [ ] No sensitive data logged
- [ ] User input sanitized
- [ ] Permissions checked (for sensitive operations)
**Performance**:
- [ ] No obvious performance issues
- [ ] No memory leaks (subscriptions cleaned up)
- [ ] Efficient algorithms used
- [ ] Large lists use virtualization (if needed)
**Architecture**:
- [ ] Follows existing patterns
- [ ] No circular dependencies
- [ ] Correct package dependencies
- [ ] Extension manifest correct (if applicable)
**Documentation**:
- [ ] JSDoc on public APIs
- [ ] README updated (if needed)
- [ ] Examples added (if new API)
- [ ] Breaking changes documented
**User Experience** (for UI changes):
- [ ] Accessible (keyboard navigation, screen readers)
- [ ] Responsive (works on different screen sizes)
- [ ] Loading states shown
- [ ] Errors handled gracefully
- [ ] Success feedback provided
### Communication
**After Each Phase**:
- Summarize what was implemented
- Highlight key decisions and why
- Call out any blockers or questions
- Show progress (code snippets, screenshots for UI)
**When Making Decisions**:
- Explain reasoning
- Reference existing patterns
- Call out trade-offs
- Ask for confirmation on significant changes
**When Blocked**:
- Clearly state the blocker
- Explain what you've tried
- Ask specific questions
- Suggest potential approaches
**When Complete**:
- Summarize what was delivered
- Note any deviations from plan (and why)
- Highlight testing performed
- Mention any follow-up work needed
### For Large Features
**Multi-Session Features**:
1. Create feature branch (if spanning multiple PRs)
2. Break into multiple PRs if very large:
- PR 1: Infrastructure (types, repository, context)
- PR 2: UI components
- PR 3: Extensions and integration
3. Keep main stable - only merge completed, tested features
4. Consider feature flags for gradual rollout
5. Document progress and remaining work in PR description
**Coordination**:
- Update PR description with progress
- Use GitHub task lists in PR description
- Comment on PR with status updates
- Request early feedback on architecture
---

View File

@@ -0,0 +1,124 @@
# Architecture
[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md)
---
### Technology Stack
- **Node.js**: >=22.17.1
- **npm**: >=10.9.2
- **Language**: TypeScript 5.9.3
- **Module System**: ESM (ES Modules)
- **Framework**: Lit (Web Components)
- **Build Tool**: Vite 7.1.11
- **Test Framework**: @web/test-runner with Playwright
- **E2E Testing**: Playwright 1.55.1
- **Code Quality**: ESLint 9.37.0, Prettier 3.6.2
- **Mocking**: MSW (Mock Service Worker) 1.3.5
- **Documentation**: Storybook 9.0.14, TypeDoc 0.28.13
### Application Type
Single-page web application (SPA) packaged as an npm library called `@umbraco-cms/backoffice` for typings and built as a bundle, which is copied over to `src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/backoffice` for the CMS. Provides:
- Extensible Web Components for CMS backoffice UI
- API libraries for extension development
- TypeScript type definitions
- Package manifest system for extensions
### Architecture Pattern
**Modular Package Architecture** with clear separation:
```
src/
├── apps/ # Application entry points
│ ├── app/ # Main backoffice application
│ ├── backoffice/ # Backoffice shell
│ ├── installer/ # CMS installer interface
│ ├── preview/ # Content preview
│ └── upgrader/ # CMS upgrader interface
├── libs/ # Core API libraries (infrastructure)
│ ├── class-api/ # Base class utilities
│ ├── context-api/ # Context API for dependency injection
│ ├── context-proxy/ # Context proxying utilities
│ ├── controller-api/ # Controller lifecycle management
│ ├── element-api/ # Element base classes & mixins
│ ├── extension-api/ # Extension registration & loading
│ ├── localization-api/ # Internationalization
│ └── observable-api/ # Reactive state management
├── packages/ # Feature packages (50+ packages)
│ ├── core/ # Core utilities (auth, http, router, etc.)
│ ├── content/ # Content management
│ ├── documents/ # Document types & editing
│ ├── media/ # Media management
│ ├── members/ # Member management
│ ├── user/ # User management
│ ├── templating/ # Templates, scripts, stylesheets
│ ├── block/ # Block editor components
│ └── ... # 30+ more specialized packages
├── external/ # External dependency wrappers
│ ├── lit/ # Lit framework wrapper
│ ├── rxjs/ # RxJS wrapper
│ ├── luxon/ # Date/time library wrapper
│ ├── monaco-editor/# Code editor wrapper
│ └── ... # Other wrapped dependencies
├── mocks/ # MSW mock handlers & test data
│ ├── data/ # Mock database
│ └── handlers/ # API request handlers
└── assets/ # Static assets (fonts, images, localization)
```
### Design Patterns
1. **Web Components** - Custom elements with Shadow DOM encapsulation
2. **Context API** - Dependency injection via context providers/consumers (similar to React Context)
3. **Controller Pattern** - Lifecycle-aware controllers for managing component behavior
4. **Extension System** - Manifest-based plugin architecture
5. **Observable Pattern** - Reactive state management with RxJS observables
6. **Repository Pattern** - Data access abstraction via repository classes
7. **Mixin Pattern** - Composable behaviors via TypeScript mixins (`UmbElementMixin`, etc.)
8. **Builder Pattern** - For complex object construction
9. **Registry Pattern** - Extension registry for dynamic feature loading
10. **Observer Pattern** - Event-driven communication between components
### Key Technologies
**Core Framework**:
- Lit 3.x - Web Components framework with reactive templates
- TypeScript 5.9 - Type-safe development with strict mode
- Vite - Fast build tool and dev server
**UI Components**:
- @umbraco-ui/uui - Umbraco UI component library
- Shadow DOM - Component style encapsulation
- Custom Elements API - Native web components
**State & Data**:
- RxJS - Reactive programming with observables
- Context API - State management & dependency injection
- MSW - API mocking for development & testing
**Testing**:
- @web/test-runner - Fast test runner for web components
- Playwright - E2E browser testing
- @open-wc/testing - Testing utilities for web components
**Code Quality**:
- ESLint with TypeScript plugin - Linting with strict rules
- Prettier - Code formatting
- TypeDoc - API documentation generation
- Web Component Analyzer - Custom element documentation
**Other**:
- Luxon - Date/time manipulation
- Monaco Editor - Code editing
- DOMPurify - HTML sanitization
- Marked - Markdown parsing
- SignalR - Real-time communication

View File

@@ -0,0 +1,397 @@
# Clean Code
[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md)
---
### Function Design
**Function Length**:
- Target: ≤30 lines per function
- Max: 50 lines (enforce via code review)
- Extract complex logic to separate functions
- Use early returns to reduce nesting
**Single Responsibility**:
```typescript
// Good - Single responsibility
private _validateName(name: string): boolean {
return name.length > 0 && name.length <= 100;
}
private _sanitizeName(name: string): string {
return name.trim().replace(/[<>]/g, '');
}
// Bad - Multiple responsibilities
private _processName(name: string): { valid: boolean; sanitized: string } {
// Doing too much in one function
}
```
**Descriptive Names**:
- Use verb names for functions: `loadData`, `validateInput`, `handleClick`
- Boolean functions: `is`, `has`, `can`, `should` prefix
- Event handlers: `handle`, `on` prefix: `handleSubmit`, `onClick`
**Parameters**:
- Limit to 3-4 parameters
- Use options object for more parameters:
```typescript
// Good - Options object
interface LoadOptions {
id: string;
includeChildren?: boolean;
depth?: number;
culture?: string;
}
private _load(options: LoadOptions) { }
// Bad - Too many parameters
private _load(id: string, includeChildren: boolean, depth: number, culture: string) { }
```
**Early Returns**:
```typescript
// Good - Early returns reduce nesting
private _validate(): boolean {
if (!this._data) return false;
if (!this._data.name) return false;
if (this._data.name.length === 0) return false;
return true;
}
// Bad - Nested conditions
private _validate(): boolean {
if (this._data) {
if (this._data.name) {
if (this._data.name.length > 0) {
return true;
}
}
}
return false;
}
```
### Class Design
**Single Responsibility**:
- Each class should have one reason to change
- Controllers handle one aspect of behavior
- Repositories handle one entity type
- Components handle one UI concern
**Small Classes**:
- Target: <300 lines per class
- Extract complex logic to separate controllers/utilities
- Use composition over inheritance
**Encapsulation**:
```typescript
export class UmbMyElement extends LitElement {
// Public API - reactive properties
@property({ type: String })
value = '';
// Private state
@state()
private _loading = false;
// Private fields (not reactive)
#controller = new UmbMyController(this);
// Private methods
private _loadData() { }
}
```
### SOLID Principles (Adapted for TypeScript/Lit)
**S - Single Responsibility**:
- One component = one UI responsibility
- One controller = one behavior responsibility
- One repository = one entity type
**O - Open/Closed**:
- Extend via composition and mixins
- Extension API for plugins
- Avoid modifying existing components, create new ones
**L - Liskov Substitution**:
- Subclasses should honor base class contracts
- Use interfaces for polymorphism
**I - Interface Segregation**:
- Small, focused interfaces
- Use TypeScript `interface` for contracts
**D - Dependency Inversion**:
- Depend on abstractions (interfaces) not concrete classes
- Use Context API for dependency injection
- Controllers receive dependencies via constructor
### Dependency Injection
**Context API** (Preferred):
```typescript
export class UmbMyElement extends UmbElementMixin(LitElement) {
#authContext?: UmbAuthContext;
constructor() {
super();
// Consume context (dependency injection)
this.consumeContext(UMB_AUTH_CONTEXT, (context) => {
this.#authContext = context;
});
}
}
```
**Controller Pattern**:
```typescript
export class UmbMyController extends UmbControllerBase {
#repository: UmbContentRepository;
constructor(host: UmbControllerHost, repository: UmbContentRepository) {
super(host);
this.#repository = repository;
}
}
// Usage
#controller = new UmbMyController(this, new UmbContentRepository());
```
### Avoid Code Smells
**Magic Numbers/Strings**:
```typescript
// Bad
if (status === 200) { }
if (type === 'document') { }
// Good
const HTTP_OK = 200;
const CONTENT_TYPE_DOCUMENT = 'document';
if (status === HTTP_OK) { }
if (type === CONTENT_TYPE_DOCUMENT) { }
// Or use enums
enum ContentType {
Document = 'document',
Media = 'media',
}
```
**Long Parameter Lists**:
```typescript
// Bad
function create(name: string, type: string, parent: string, culture: string, template: string) { }
// Good
interface CreateOptions {
name: string;
type: string;
parent?: string;
culture?: string;
template?: string;
}
function create(options: CreateOptions) { }
```
**Duplicate Code**:
- Extract to shared functions
- Use composition and mixins
- Create utility modules
**Deeply Nested Code**:
- Use early returns
- Extract to separate functions
- Use guard clauses
**Callback Hell** (N/A - use async/await)
### Modern Patterns
**Async/Await**:
```typescript
// Good - Clean async code
async loadContent() {
try {
this._loading = true;
const { data, error } = await this.repository.requestById(this.id);
if (error) {
this._error = error;
return;
}
this._content = data;
} finally {
this._loading = false;
}
}
```
**Optional Chaining**:
```typescript
// Good - Safe property access
const name = this._content?.variants?.[0]?.name;
// Bad - Manual null checks
const name = this._content && this._content.variants &&
this._content.variants[0] && this._content.variants[0].name;
```
**Destructuring**:
```typescript
// Good - Destructure for clarity
const { name, description, icon } = this._content;
// Good - With defaults
const { name = 'Untitled', description = '' } = this._content;
```
**Immutability**:
```typescript
// Good - Spread operator for immutability
this._items = [...this._items, newItem];
this._config = { ...this._config, newProp: value };
// Bad - Mutation
this._items.push(newItem);
this._config.newProp = value;
```
**Pure Functions**:
```typescript
// Good - Pure function (no side effects)
function calculateTotal(items: Item[]): number {
return items.reduce((sum, item) => sum + item.price, 0);
}
// Bad - Impure (modifies input)
function calculateTotal(items: Item[]): number {
items.sort((a, b) => a.price - b.price); // Mutation!
return items.reduce((sum, item) => sum + item.price, 0);
}
```
### TypeScript-Specific Patterns
**Type Guards**:
```typescript
function isContentModel(value: unknown): value is UmbContentModel {
return typeof value === 'object' && value !== null && 'id' in value;
}
// Usage
if (isContentModel(data)) {
// TypeScript knows data is UmbContentModel
console.log(data.id);
}
```
**Discriminated Unions**:
```typescript
type Result<T> =
| { success: true; data: T }
| { success: false; error: Error };
function handleResult<T>(result: Result<T>) {
if (result.success) {
console.log(result.data); // TypeScript knows this exists
} else {
console.error(result.error); // TypeScript knows this exists
}
}
```
**Utility Types**:
```typescript
// Partial - Make all properties optional
type PartialContent = Partial<UmbContentModel>;
// Pick - Select specific properties
type ContentSummary = Pick<UmbContentModel, 'id' | 'name' | 'icon'>;
// Omit - Remove specific properties
type ContentWithoutId = Omit<UmbContentModel, 'id'>;
// Readonly - Make immutable
type ReadonlyContent = Readonly<UmbContentModel>;
```
### Comments and Documentation
**When to Comment**:
- Explain "why" not "what"
- Document complex algorithms
- JSDoc for public APIs
- Warn about gotchas or non-obvious behavior
**JSDoc for Web Components**:
```typescript
/**
* A button component that triggers document actions.
* @element umb-document-action-button
* @fires {CustomEvent} action-click - Fired when action is clicked
* @slot - Default slot for button content
* @cssprop --umb-button-color - Button text color
*/
export class UmbDocumentActionButton extends LitElement {
/**
* The action identifier
* @attr
* @type {string}
*/
@property({ type: String })
action = '';
}
```
**TODOs**:
```typescript
// TODO: Implement pagination [NL]
// FIXME: Memory leak in subscription [JOV]
// HACK: Temporary workaround for API bug [LK]
```
**Remove Dead Code**:
- Don't comment out code, delete it (Git history preserves it)
- Remove unused imports, functions, variables
- Clean up console.logs before committing
### Patterns to Avoid
**Don't**:
- Use `var` (use `const`/`let`)
- Modify prototypes of built-in objects
- Use global variables
- Block the main thread (use web workers for heavy computation)
- Create deeply nested structures
- Use `any` type (use `unknown` or proper types)
- Use non-null assertions `!` unless absolutely necessary
- Ignore TypeScript errors with `@ts-ignore`

View File

@@ -0,0 +1,214 @@
# Commands
[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md)
---
### Installation
```bash
# Install dependencies (must use npm, not yarn/pnpm due to workspaces)
npm install
# Note: Requires Node >=22.17.1 and npm >=10.9.2
```
### Build Commands
```bash
# TypeScript compilation only
npm run build
# Build for CMS (production build + copy to .NET project)
npm run build:for:cms
# Build for npm distribution (with type declarations)
npm run build:for:npm
# Build with Vite (alternative build method)
npm run build:vite
# Build workspaces
npm run build:workspaces
# Build Storybook documentation
npm run build-storybook
```
### Development Commands
```bash
# Start dev server (with live reload)
npm run dev
# Start dev server connected to real backend
npm run dev:server
# Start dev server with MSW mocks (default)
npm run dev:mock
# Preview production build
npm run preview
```
### Test Commands
```bash
# Run all unit tests
npm test
# Run tests in watch mode
npm run test:watch
# Run tests in development mode
npm run test:dev
# Run tests in watch mode (dev config)
npm run test:dev-watch
# Run E2E tests with Playwright
npm run test:e2e
# Run example tests
npm run test:examples
# Run example tests in watch mode
npm run test:examples:watch
# Run example tests in browser
npm run test:examples:browser
```
### Code Quality Commands
```bash
# Lint TypeScript files
npm run lint
# Lint and show only errors
npm run lint:errors
# Lint and auto-fix issues
npm run lint:fix
# Format code
npm run format
# Format and auto-fix
npm run format:fix
# Type check
npm run compile
# Run all checks (lint, compile, build-storybook, jsonschema)
npm run check
```
### Code Generation Commands
```bash
# Generate TypeScript config
npm run generate:tsconfig
# Generate OpenAPI client from backend API
npm run generate:server-api
# Generate icons
npm run generate:icons
# Generate package manifest
npm run generate:manifest
# Generate JSON schema for umbraco-package.json
npm run generate:jsonschema
# Generate JSON schema to dist
npm run generate:jsonschema:dist
# Generate UI API docs with TypeDoc
npm run generate:ui-api-docs
# Generate const check tests
npm run generate:check-const-test
```
### Analysis Commands
```bash
# Check for circular dependencies
npm run check:circular
# Check module dependencies
npm run check:module-dependencies
# Check path lengths
npm run check:paths
# Analyze web components
npm run wc-analyze
# Analyze web components for VS Code
npm run wc-analyze:vscode
```
### Storybook Commands
```bash
# Start Storybook dev server
npm run storybook
# Build Storybook
npm run storybook:build
# Build and preview Storybook
npm run storybook:preview
```
### Package Management
```bash
# Validate package exports
npm run package:validate
# Prepare for npm publish
npm run prepack
```
### Environment Setup
**Prerequisites**:
- Node.js >=22.17.1
- npm >=10.9.2
- Modern browser (Chrome, Firefox, Safari)
**Initial Setup**:
1. Clone repository
```bash
git clone https://github.com/umbraco/Umbraco-CMS.git
cd Umbraco-CMS/src/Umbraco.Web.UI.Client
```
2. Install dependencies
```bash
npm install
```
3. Configure environment (optional)
```bash
cp .env .env.local
# Edit .env.local with your settings
```
4. Start development
```bash
npm run dev
```
**Environment Variables** (see `.env` file):
- `VITE_UMBRACO_USE_MSW` - Enable/disable Mock Service Worker (`on`/`off`)
- `VITE_UMBRACO_API_URL` - Backend API URL (e.g., `https://localhost:44339`)
- `VITE_UMBRACO_INSTALL_STATUS` - Install status (`running`, `must-install`, `must-upgrade`)
- `VITE_UMBRACO_EXTENSION_MOCKS` - Enable extension mocks (`on`/`off`)

View File

@@ -0,0 +1,584 @@
# Edge Cases
[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md)
---
### Null/Undefined Handling
**Always Check**:
```typescript
// Use optional chaining
const name = this._content?.name;
// Use nullish coalescing for defaults
const name = this._content?.name ?? 'Untitled';
// Check before accessing
if (!this._content) {
return html`<p>No content</p>`;
}
```
**TypeScript Strict Null Checks** (enabled):
- Variables are non-nullable by default
- Use `Type | undefined` or `Type | null` explicitly
- TypeScript forces null checks
**Function Parameters**:
```typescript
// Make nullable parameters explicit
function load(id: string, culture?: string) {
const cultureCode = culture ?? 'en-US'; // Default value
}
```
### Array Edge Cases
**Empty Arrays**:
```typescript
// Check length before access
if (this._items.length === 0) {
return html`<p>No items</p>`;
}
// Safe array methods
const first = this._items[0]; // Could be undefined
const first = this._items.at(0); // Also could be undefined
// Use optional chaining
const firstId = this._items[0]?.id;
```
**Single vs Multiple Items**:
```typescript
// Handle both cases
const items = Array.isArray(data) ? data : [data];
```
**Array Methods on Undefined**:
```typescript
// Guard against undefined
const ids = this._items?.map(item => item.id) ?? [];
// Or check first
if (!this._items) {
return [];
}
return this._items.map(item => item.id);
```
**Sparse Arrays** (rare in this codebase):
```typescript
// Use filter to remove empty slots
const dense = sparse.filter(() => true);
```
### String Edge Cases
**Empty Strings**:
```typescript
// Check for empty strings
if (!name || name.trim().length === 0) {
return 'Untitled';
}
// Or use default
const displayName = name?.trim() || 'Untitled';
```
**String vs Null/Undefined**:
```typescript
// Distinguish between empty string and null
const hasValue = value !== null && value !== undefined;
const isEmpty = value === '';
// Or use optional chaining
const length = value?.length ?? 0;
```
**Trim Whitespace**:
```typescript
// Always trim user input
const cleanName = this._name.trim();
// Validate after trimming
if (cleanName.length === 0) {
// Invalid
}
```
**String Encoding**:
- Use UTF-8 everywhere
- Be aware of Unicode characters (emojis, etc.)
- Use `textContent` not `innerHTML` for plain text
**Internationalization**:
```typescript
// Use localization API
const label = this.localize.term('general_submit');
// Not hardcoded strings
// const label = 'Submit'; // ❌
```
### Number Edge Cases
**NaN Checks**:
```typescript
// Use Number.isNaN, not isNaN
if (Number.isNaN(value)) {
// Handle NaN
}
// isNaN coerces, Number.isNaN doesn't
isNaN('hello'); // true (coerces to NaN)
Number.isNaN('hello'); // false (not a number)
```
**Infinity**:
```typescript
if (!Number.isFinite(value)) {
// Handle Infinity or NaN
}
```
**Parsing**:
```typescript
// parseInt/parseFloat can return NaN
const num = parseInt(input, 10);
if (Number.isNaN(num)) {
// Handle invalid input
}
// Or use Number constructor with validation
const num = Number(input);
if (!Number.isFinite(num)) {
// Invalid
}
```
**Floating Point Precision**:
```typescript
// Don't compare floats with ===
const isEqual = Math.abs(a - b) < 0.0001;
// Or use integers for currency (cents, not dollars)
const priceInCents = 1099; // $10.99
```
**Division by Zero**:
```typescript
// JavaScript returns Infinity, not error
const result = 10 / 0; // Infinity
// Check denominator
if (denominator === 0) {
// Handle division by zero
return 0; // Or throw error
}
```
**Safe Integer Range**:
```typescript
// JavaScript integers are safe up to Number.MAX_SAFE_INTEGER
const isSafe = Number.isSafeInteger(value);
// For IDs, use strings not numbers
interface UmbContentModel {
id: string; // Not number
}
```
### Object Edge Cases
**Property Existence**:
```typescript
// Use optional chaining
const value = obj?.property;
// Or check explicitly
if ('property' in obj) {
const value = obj.property;
}
// hasOwnProperty (not inherited)
if (Object.hasOwn(obj, 'property')) {
// Property exists on object itself
}
```
**Null vs Undefined vs {}**:
```typescript
// Distinguish between missing and empty
const isEmpty = obj !== null && obj !== undefined && Object.keys(obj).length === 0;
// Or use optional chaining
const hasData = obj && Object.keys(obj).length > 0;
```
**Shallow vs Deep Copy**:
```typescript
// Shallow copy
const copy = { ...original };
// Deep copy (for simple objects)
const deepCopy = JSON.parse(JSON.stringify(original));
// Deep copy with structuredClone (modern browsers)
const deepCopy = structuredClone(original);
// Note: Functions, symbols, and undefined are not copied by JSON.stringify
```
**Object Freezing**:
```typescript
// Prevent modification
const frozen = Object.freeze(obj);
// Check if frozen
if (Object.isFrozen(obj)) {
// Can't modify
}
```
### Async/Await Edge Cases
**Unhandled Promise Rejections**:
```typescript
// Always catch errors
try {
await asyncOperation();
} catch (error) {
console.error('Failed:', error);
}
// Or use .catch()
asyncOperation().catch(error => {
console.error('Failed:', error);
});
// For fire-and-forget, explicitly catch
void asyncOperation().catch(error => console.error(error));
```
**Promise.all Fails Fast**:
```typescript
// Promise.all rejects if ANY promise rejects
try {
const results = await Promise.all([op1(), op2(), op3()]);
} catch (error) {
// One failed, others may still be running
}
// Use Promise.allSettled to wait for all (even if some fail)
const results = await Promise.allSettled([op1(), op2(), op3()]);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Op ${index} succeeded:`, result.value);
} else {
console.error(`Op ${index} failed:`, result.reason);
}
});
```
**Race Conditions**:
```typescript
// Avoid race conditions with sequential operations
this._loading = true;
try {
const data1 = await fetchData1();
const data2 = await fetchData2(data1.id);
this._result = processData(data1, data2);
} finally {
this._loading = false;
}
// For parallel operations, use Promise.all
const [data1, data2] = await Promise.all([fetchData1(), fetchData2()]);
```
**Timeout Handling**:
```typescript
// Implement timeout for operations
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
return Promise.race([
promise,
new Promise<T>((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), ms)
),
]);
}
// Usage
try {
const data = await withTimeout(fetchData(), 5000);
} catch (error) {
// Handle timeout or other errors
}
```
**Memory Leaks with Event Listeners**:
```typescript
export class UmbMyElement extends LitElement {
#controller = new UmbMyController(this);
// Controllers automatically clean up on disconnect
constructor() {
super();
this.#controller.observe(dataSource$, (value) => {
this._data = value;
});
}
// Lit lifecycle handles this automatically
disconnectedCallback() {
super.disconnectedCallback();
// Controllers are destroyed automatically
}
}
```
### Web Component Edge Cases
**Custom Element Not Defined**:
```typescript
// Check if element is defined
if (!customElements.get('umb-my-element')) {
// Not defined yet
await customElements.whenDefined('umb-my-element');
}
// Or use upgrade
await customElements.upgrade(element);
```
**Shadow DOM Queries**:
```typescript
// Query shadow root, not document
const button = this.shadowRoot?.querySelector('button');
// Use Lit decorators
@query('#myButton')
private _button!: HTMLButtonElement;
```
**Property vs Attribute Sync**:
```typescript
// Lit keeps properties and attributes in sync
@property({ type: String })
name = ''; // Syncs with name="" attribute
// But complex types don't sync to attributes
@property({ type: Object })
data = {}; // No attribute sync
// State doesn't sync to attributes
@state()
private _loading = false;
```
**Reactive Update Timing**:
```typescript
// Wait for update to complete
this.value = 'new value';
await this.updateComplete;
// Now DOM is updated
// Or use requestUpdate
this.requestUpdate();
await this.updateComplete;
```
### Date/Time Edge Cases
**Use Luxon** (not Date):
```typescript
import { DateTime } from '@umbraco-cms/backoffice/external/luxon';
// Create dates
const now = DateTime.now();
const utc = DateTime.utc();
const parsed = DateTime.fromISO('2024-01-15T10:30:00Z');
// Always store dates in UTC
const isoString = now.toUTC().toISO();
// Format for display
const formatted = now.toLocaleString(DateTime.DATETIME_MED);
// Timezone handling
const local = utc.setZone('local');
```
**Date Comparison**:
```typescript
// Compare DateTime objects
if (date1 < date2) { }
if (date1.equals(date2)) { }
// Or compare timestamps
if (date1.toMillis() < date2.toMillis()) { }
```
**Date Parsing Can Fail**:
```typescript
const date = DateTime.fromISO(input);
if (!date.isValid) {
console.error('Invalid date:', date.invalidReason);
// Handle invalid date
}
```
### JSON Edge Cases
**JSON.parse Can Throw**:
```typescript
// Always wrap in try/catch
try {
const data = JSON.parse(jsonString);
} catch (error) {
console.error('Invalid JSON:', error);
// Handle parse error
}
```
**Circular References**:
```typescript
// JSON.stringify throws on circular references
const obj = { a: 1 };
obj.self = obj;
try {
JSON.stringify(obj);
} catch (error) {
// TypeError: Converting circular structure to JSON
}
```
**Date Objects**:
```typescript
// Dates become strings
const data = { date: new Date() };
const json = JSON.stringify(data);
const parsed = JSON.parse(json);
// parsed.date is a string, not Date
// Use ISO format explicitly
const isoDate = new Date().toISOString();
```
**Undefined Values**:
```typescript
// Undefined values are omitted
const obj = { a: 1, b: undefined };
JSON.stringify(obj); // '{"a":1}'
// Use null for explicit absence
const obj = { a: 1, b: null };
JSON.stringify(obj); // '{"a":1,"b":null}'
```
### Handling Strategy
**Guard Clauses**:
```typescript
// Check preconditions early
if (!this._data) return;
if (this._data.length === 0) return;
if (!this._data[0].name) return;
// Now can safely use data
this.processData(this._data[0].name);
```
**Defensive Programming**:
```typescript
// Validate inputs
function process(items: unknown) {
if (!Array.isArray(items)) {
throw new Error('Expected array');
}
// Safe to use items as array
}
// Use type guards
if (isContentModel(data)) {
// TypeScript knows data is UmbContentModel
}
```
**Fail Fast**:
```typescript
// Throw errors early for programmer mistakes
if (this._repository === undefined) {
throw new Error('Repository not initialized');
}
// Handle expected errors gracefully
try {
const data = await this._repository.loadById(id);
} catch (error) {
this._error = 'Failed to load content';
return;
}
```
**Document Edge Cases**:
```typescript
/**
* Loads content by ID.
* @throws {UmbContentNotFoundError} If content doesn't exist
* @throws {UmbUnauthorizedError} If user lacks permission
* @returns {Promise<UmbContentModel>} The content model
*
* @remarks
* This method returns cached data if available.
* Pass `{ skipCache: true }` to force a fresh load.
*/
async loadById(id: string, options?: LoadOptions): Promise<UmbContentModel> {
// ...
}
```

View File

@@ -0,0 +1,242 @@
# Error Handling
[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md)
---
### Diagnosis Process
1. **Check browser console** for errors and warnings
2. **Reproduce consistently** - Identify exact steps
3. **Check network tab** for failed API calls
4. **Inspect element** to verify DOM structure
5. **Check Lit reactive update cycle** - Use `element.updateComplete`
6. **Verify context availability** - Check context providers
7. **Review event listeners** - Check event propagation
8. **Use browser debugger** with source maps
**Common Web Component Issues**:
- Component not rendering: Check if custom element is defined
- Properties not updating: Verify `@property()` decorator and reactive update cycle
- Events not firing: Check event names and listeners
- Shadow DOM issues: Use `shadowRoot.querySelector()` not `querySelector()`
- Styles not applied: Check Shadow DOM style encapsulation
- Context not available: Ensure context provider is ancestor in DOM tree
### Error Handling Standards
**Error Classes**:
```typescript
// Use built-in Error class or extend it
throw new Error('Failed to load content');
// Custom error classes for domain errors
export class UmbContentNotFoundError extends Error {
constructor(id: string) {
super(`Content with id "${id}" not found`);
this.name = 'UmbContentNotFoundError';
}
}
```
**Repository Pattern Error Handling**:
```typescript
async requestById(id: string): Promise<{ data?: UmbContentModel; error?: Error }> {
try {
const response = await this._apiClient.getById({ id });
return { data: response.data };
} catch (error) {
return { error: error as Error };
}
}
// Usage
const { data, error } = await repository.requestById('123');
if (error) {
// Handle error
console.error('Failed to load content:', error);
return;
}
// Use data
```
**Observable Error Handling**:
```typescript
this.observe(dataSource$, (value) => {
// Success handler
this._data = value;
}).catch((error) => {
// Error handler
console.error('Observable error:', error);
});
```
**Promise Error Handling**:
```typescript
// Always use try/catch with async/await
async myMethod() {
try {
const result = await this.fetchData();
return result;
} catch (error) {
console.error('Failed to fetch data:', error);
throw error; // Re-throw or handle
}
}
// Or use .catch()
this.fetchData()
.then(result => this.handleResult(result))
.catch(error => this.handleError(error));
```
### Web Component Error Handling
**Lifecycle Errors**:
```typescript
export class UmbMyElement extends UmbElementMixin(LitElement) {
constructor() {
try {
super();
// Initialization that might throw
} catch (error) {
console.error('Failed to initialize element:', error);
}
}
async connectedCallback() {
try {
super.connectedCallback();
// Async initialization
await this.loadData();
} catch (error) {
console.error('Failed to connect element:', error);
this._errorMessage = 'Failed to load component';
}
}
}
```
**Render Errors**:
```typescript
override render() {
if (this._error) {
return html`<umb-error-message .error=${this._error}></umb-error-message>`;
}
if (!this._data) {
return html`<uui-loader></uui-loader>`;
}
return html`
<!-- Normal render -->
`;
}
```
### Logging Standards
**Console Logging**:
```typescript
// Development only - Remove before production
console.log('Debug info:', data);
// Errors - Kept in production but sanitized
console.error('Failed to load:', error);
// Warnings
console.warn('Deprecated API usage:', method);
// Avoid console.log in production code (ESLint warning)
```
**Custom Logging** (if needed):
```typescript
// Use debug flag for verbose logging
if (this._debug) {
console.log('[UmbMyComponent]', 'State changed:', this._state);
}
```
**Don't Log Sensitive Data**:
- User credentials
- API tokens
- Personal information (PII)
- Session IDs
- Full error stack traces in production
### Development Environment
- **Source Maps**: Enabled in Vite config for debugging
- **Error Overlay**: Vite provides error overlay in development
- **Hot Module Replacement**: Instant feedback on code changes
- **Detailed Errors**: Full stack traces with source locations
- **TypeScript Checking**: Real-time type checking in IDE
### Production Environment
- **Sanitized Errors**: No stack traces exposed to users
- **User-Friendly Messages**: Show helpful error messages
- **Error Boundaries**: Catch errors at component boundaries
- **Graceful Degradation**: Fallback UI when errors occur
- **Error Reporting**: Log errors to console (browser dev tools)
**Production Error Display**:
```typescript
private _errorMessage?: string;
override render() {
if (this._errorMessage) {
return html`
<uui-box>
<p class="error">${this._errorMessage}</p>
<uui-button @click=${this._retry} label="Try Again"></uui-button>
</uui-box>
`;
}
// ... normal render
}
```
### Context-Specific Error Handling
**HTTP Client Errors** (OpenAPI):
```typescript
try {
const response = await this._apiClient.getDocument({ id });
return response.data;
} catch (error) {
if (error.status === 404) {
throw new UmbContentNotFoundError(id);
}
if (error.status === 403) {
throw new UmbUnauthorizedError('Access denied');
}
throw error;
}
```
**Observable Subscription Errors**:
```typescript
this._subscription = this._dataSource.asObservable().subscribe({
next: (value) => this._data = value,
error: (error) => {
console.error('Observable error:', error);
this._errorMessage = 'Failed to load data';
},
complete: () => console.log('Observable completed'),
});
```

View File

@@ -0,0 +1,299 @@
# Security
[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md)
---
### Input Validation
**Validate All User Input**:
```typescript
// Use validation in forms
import { UmbValidationController } from '@umbraco-cms/backoffice/validation';
#validation = new UmbValidationController(this);
async #handleSubmit() {
if (!this.#validation.validate()) {
return; // Show validation errors
}
// Proceed with submission
}
```
**String Validation**:
```typescript
private _validateName(name: string): boolean {
// Length check
if (name.length === 0 || name.length > 100) {
return false;
}
// Pattern check (example: alphanumeric and spaces)
if (!/^[a-zA-Z0-9\s]+$/.test(name)) {
return false;
}
return true;
}
```
**Sanitize HTML**:
```typescript
// Use DOMPurify for HTML sanitization
import DOMPurify from '@umbraco-cms/backoffice/external/dompurify';
const cleanHtml = DOMPurify.sanitize(userInput);
// In Lit templates, use unsafeHTML directive with sanitized content
import { unsafeHTML } from '@umbraco-cms/backoffice/external/lit';
render() {
return html`<div>${unsafeHTML(DOMPurify.sanitize(this.htmlContent))}</div>`;
}
```
### Authentication & Authorization
**OpenID Connect** via backend:
- Backoffice uses OpenID Connect for authentication
- Authentication handled by .NET backend
- Tokens managed by browser (httpOnly cookies)
**Authorization Checks**:
```typescript
// Check user permissions before actions
#authContext?: UmbAuthContext;
async #handleDelete() {
const hasPermission = await this.#authContext?.hasPermission('delete');
if (!hasPermission) {
// Show error or hide action
return;
}
// Proceed with deletion
}
```
**Context Security**:
- Use Context API for auth state
- Don't store sensitive tokens in localStorage
- Backend handles token refresh
### API Security
**HTTP Client Security**:
```typescript
// OpenAPI client handles:
// - CSRF tokens
// - Request headers
// - Credentials
// - Error handling
// Use generated OpenAPI client
import { ContentResource } from '@umbraco-cms/backoffice/external/backend-api';
const client = new ContentResource();
const response = await client.getById({ id });
```
**CORS** (Backend Configuration):
- Configured in .NET backend
- Backoffice follows same-origin policy
- API calls to same origin
**Rate Limiting** (Backend):
- Handled by .NET backend
- Backoffice respects rate limit headers
### XSS Prevention
**Template Security** (Lit):
```typescript
// Lit automatically escapes content in templates
render() {
// Safe - Automatically escaped
return html`<div>${this.userContent}</div>`;
// UNSAFE - Only use with sanitized content
return html`<div>${unsafeHTML(DOMPurify.sanitize(this.htmlContent))}</div>`;
}
```
**Attribute Binding**:
```typescript
// Safe - Lit escapes attribute values
render() {
return html`<input value=${this.userInput} />`;
}
```
**Event Handlers**:
```typescript
// Safe - Event handlers are not strings
render() {
return html`<button @click=${this.#handleClick}>Click</button>`;
}
// NEVER do this (code injection risk)
// render() {
// return html`<button onclick="${this.userCode}">Click</button>`;
// }
```
### Content Security Policy
**CSP Headers** (Backend Configuration):
- Configured in .NET backend
- Restricts script sources
- Prevents inline scripts (except with nonce)
- Reports violations
**Backoffice Compliance**:
- No inline scripts
- No `eval()` or `Function()` constructor
- Monaco Editor uses web workers (CSP compliant)
### Dependencies Security
**Package Management**:
```bash
# Check for vulnerabilities
npm audit
# Fix automatically
npm audit fix
# Update dependencies carefully
npm update
```
**Dependency Security Practices**:
- Renovate bot automatically creates PRs for updates
- Review dependency changes before merging
- Only use packages from npm registry
- Verify package integrity
- Keep dependencies updated
**Known Vulnerabilities**:
- CI checks for vulnerabilities on every PR
- Security advisories reviewed regularly
### Common Vulnerabilities
**XSS (Cross-Site Scripting)**:
- ✅ Lit templates automatically escape content
- ✅ DOMPurify for HTML sanitization
- ❌ Never use `unsafeHTML` with user input directly
- ❌ Never set `innerHTML` with user input
**CSRF (Cross-Site Request Forgery)**:
- ✅ Backend sends CSRF tokens
- ✅ OpenAPI client includes tokens automatically
- ✅ SameSite cookies
**Injection Attacks**:
- ✅ Backend uses parameterized queries
- ✅ Input validation on both frontend and backend
- ✅ OpenAPI client prevents injection
**Prototype Pollution**:
- ❌ Never use `Object.assign` with user input as source
- ❌ Never use `_.merge` with untrusted data
- ✅ Validate object shapes before using
**ReDoS (Regular Expression Denial of Service)**:
- ✅ Review complex regex patterns
- ✅ Test regex with long inputs
- ❌ Avoid backtracking in regex
### Secure Coding Practices
**Don't Trust Client Data**:
- Validate on backend (primary defense)
- Frontend validation is UX, not security
**Principle of Least Privilege**:
- Only request permissions needed
- Check permissions before sensitive operations
- Hide UI for unavailable actions
**Sanitize Output**:
- Always sanitize HTML before rendering
- Escape special characters in user content
- Use Lit's automatic escaping
**Secure Defaults**:
- Forms should validate by default
- Sensitive operations require confirmation
- Errors don't expose sensitive information
**Defense in Depth**:
- Multiple layers of security
- Frontend validation + Backend validation
- Input sanitization + Output escaping
- Authentication + Authorization
### Security Anti-Patterns to Avoid
**Never do this**:
```typescript
// XSS vulnerability
element.innerHTML = userInput;
// Code injection
eval(userCode);
// Exposing sensitive data
console.log('Token:', authToken);
// Storing secrets
localStorage.setItem('apiKey', key);
// Disabling validation
// @ts-ignore
// eslint-disable-next-line
// Trusting user input
const url = userInput; // Could be javascript:alert()
window.location.href = url;
```
**Do this instead**:
```typescript
// Safe HTML rendering
element.textContent = userInput;
// or
render() {
return html`<div>${userInput}</div>`;
}
// No eval needed
// Use proper JavaScript patterns
// Don't log sensitive data
console.log('Operation completed');
// Backend manages secrets
// Frontend receives tokens via httpOnly cookies
// Fix TypeScript/ESLint issues properly
// Don't suppress warnings
// Validate and sanitize URLs
const url = new URL(userInput, window.location.origin);
if (url.protocol === 'https:' || url.protocol === 'http:') {
window.location.href = url.href;
}
```

View File

@@ -0,0 +1,157 @@
# Style Guide
[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md)
---
### Naming Conventions
**Files**:
- Web components: `my-component.element.ts`
- Tests: `my-component.test.ts`
- Stories: `my-component.stories.ts`
- Controllers: `my-component.controller.ts`
- Contexts: `my-component.context.ts`
- Modals: `my-component.modal.ts`
- Workspaces: `my-component.workspace.ts`
- Repositories: `my-component.repository.ts`
- Index files: `index.ts` (barrel exports)
**Classes & Types**:
- Classes: `PascalCase` with `Umb` prefix: `UmbMyComponent`
- Interfaces: `PascalCase` with `Umb` prefix: `UmbMyInterface`
- Types: `PascalCase` with `Umb`, `Ufm`, `Manifest`, `Meta`, or `Example` prefix
- Exported types MUST have approved prefix
- Example types for docs: `ExampleMyType`
**Variables & Functions**:
- Public members: `camelCase` without underscore: `myVariable`, `myMethod`
- Private members: `camelCase` with leading underscore: `_myPrivateVariable`
- #private members: `camelCase` without underscore: `#myPrivateField`
- Protected members: `camelCase` with optional underscore: `myProtected` or `_myProtected`
- Constants (exported): `UPPER_SNAKE_CASE` with `UMB_` prefix: `UMB_MY_CONSTANT`
- Local constants: `UPPER_CASE` or `camelCase`
**Custom Elements**:
- Element tag names: kebab-case with `umb-` prefix: `umb-my-component`
- Must be registered in global `HTMLElementTagNameMap`
### File Organization
- One class/component per file
- Use barrel exports (`index.ts`) for package public APIs
- Import order (enforced by ESLint):
1. External dependencies
2. Parent imports
3. Sibling imports
4. Index imports
5. Type-only imports (separate)
### Code Formatting (Prettier)
```json
{
"printWidth": 120,
"singleQuote": true,
"semi": true,
"bracketSpacing": true,
"bracketSameLine": true,
"useTabs": true
}
```
- **Indentation**: Tabs (not spaces)
- **Line length**: 120 characters max
- **Quotes**: Single quotes
- **Semicolons**: Required
- **Trailing commas**: Yes (default)
### TypeScript Conventions
**Strict Mode** (enabled in `tsconfig.json`):
- `strict: true`
- `noImplicitReturns: true`
- `noFallthroughCasesInSwitch: true`
- `noImplicitOverride: true`
**Type Features**:
- Use TypeScript types over JSDoc when possible
- BUT: Lit components use JSDoc for web-component-analyzer compatibility
- Use `type` for unions/intersections: `type MyType = A | B`
- Use `interface` for object shapes and extension: `interface MyInterface extends Base`
- Prefer `const` over `let`, never use `var`
- Use `readonly` for immutable properties
- Use generics for reusable code
- Avoid `any` (lint warning), use `unknown` instead
- Use type guards for narrowing
- Use `as const` for literal types
**Module Syntax**:
- ES Modules only: `import`/`export`
- Use consistent type imports: `import type { MyType } from '...'`
- Use consistent type exports: `export type { MyType }`
- No side-effects in imports
**Decorators**:
- `@customElement('umb-my-element')` - Register custom element
- `@property({ type: String })` - Reactive properties
- `@state()` - Internal reactive state
- `@query('#myId')` - Query shadow DOM
- Experimental decorators enabled
### Modern TypeScript Features to Use
- **Async/await** over callbacks
- **Optional chaining**: `obj?.property?.method?.()`
- **Nullish coalescing**: `value ?? defaultValue`
- **Template literals**: `` `Hello ${name}` ``
- **Destructuring**: `const { a, b } = obj`
- **Spread operator**: `{ ...obj, newProp: value }`
- **Arrow functions**: `const fn = () => {}`
- **Array methods**: `map`, `filter`, `reduce`, `find`, `some`, `every`
- **Object methods**: `Object.keys`, `Object.values`, `Object.entries`
- **Private fields**: `#privateField`
### Language Features to Avoid
- `var` (use `const`/`let`)
- `eval()` or `Function()` constructor
- `with` statement
- `arguments` object (use rest parameters)
- Deeply nested callbacks (use async/await)
- Non-null assertions unless absolutely necessary: `value!`
- Type assertions unless necessary: `value as Type`
- `@ts-ignore` (use `@ts-expect-error` with comment)
### Custom ESLint Rules
**Project-Specific Rules**:
- `prefer-static-styles-last` - Static styles property must be last in class
- `enforce-umbraco-external-imports` - External dependencies must be imported via `@umbraco-cms/backoffice/external/*`
- Private members MUST have leading underscore
- Exported types MUST have approved prefix (Umb, Ufm, Manifest, Meta, Example)
- Exported string constants MUST have UMB_ prefix
- Semicolons required
- No `var` keyword
- No circular dependencies (max depth 6)
- No self-imports
- Consistent type imports/exports
**Allowed JSDoc Tags** (for web-component-analyzer):
- `@element` - Element name
- `@attr` - HTML attribute
- `@fires` - Custom events
- `@prop` - Properties
- `@slot` - Slots
- `@cssprop` - CSS custom properties
- `@csspart` - CSS parts
### Documentation
- **Public APIs**: JSDoc comments with `@description`, `@param`, `@returns`, `@example`
- **Web Components**: JSDoc with web-component-analyzer tags
- **Complex logic**: Inline comments explaining "why" not "what"
- **TODOs**: Format as `// TODO: description [initials]`
- **Deprecated**: Use `@deprecated` tag with migration instructions

View File

@@ -0,0 +1,279 @@
# Testing
[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md)
---
### Testing Philosophy
- **Unit tests** for business logic and utilities
- **Component tests** for web components
- **Integration tests** for workflows
- **E2E tests** for critical user journeys
- **Coverage target**: No strict requirement, focus on meaningful tests
- **Test pyramid**: Many unit tests, fewer integration tests, few E2E tests
### Testing Frameworks
**Unit/Component Testing**:
- `@web/test-runner` - Fast test runner for web components
- `@open-wc/testing` - Testing utilities, includes Chai assertions
- `@types/chai` - Assertion library
- `@types/mocha` - Test framework (used by @web/test-runner)
- `@web/test-runner-playwright` - Browser launcher
- `element-internals-polyfill` - Polyfill for form-associated custom elements
**E2E Testing**:
- `@playwright/test` - End-to-end testing in real browsers
- Playwright MSW integration for API mocking
**Test Utilities**:
- `@umbraco-cms/internal/test-utils` - Shared test utilities
- MSW (Mock Service Worker) - API mocking
- Fixtures in `src/mocks/data/` - Test data
### Test Project Organization
```
src/
├── **/*.test.ts # Unit tests co-located with source
├── mocks/
│ ├── data/ # Mock data & in-memory databases
│ └── handlers/ # MSW request handlers
├── examples/
│ └── **/*.test.ts # Example tests
└── utils/
└── test-utils.ts # Shared test utilities
e2e/
├── **/*.spec.ts # Playwright E2E tests
└── fixtures/ # E2E test fixtures
```
### Test Naming Convention
```typescript
describe('UmbMyComponent', () => {
describe('initialization', () => {
it('should create element', async () => {
// test
});
it('should set default properties', async () => {
// test
});
});
describe('user interactions', () => {
it('should emit event when button clicked', async () => {
// test
});
});
});
```
### Test Structure (AAA Pattern)
```typescript
it('should do something', async () => {
// Arrange - Set up test data and conditions
const element = await fixture<UmbMyElement>(html`<umb-my-element></umb-my-element>`);
const spy = sinon.spy();
element.addEventListener('change', spy);
// Act - Perform the action
element.value = 'new value';
await element.updateComplete;
// Assert - Verify the results
expect(spy.calledOnce).to.be.true;
expect(element.value).to.equal('new value');
});
```
### Unit Test Guidelines
**What to Test**:
- Public API methods and properties
- User interactions (clicks, inputs, etc.)
- State changes
- Event emissions
- Error handling
- Edge cases
**What NOT to Test**:
- Private implementation details
- Framework/library code
- Generated code (e.g., OpenAPI clients)
- Third-party dependencies
**Best Practices**:
- One assertion per test (when practical)
- Test behavior, not implementation
- Use meaningful test names (describe what should happen)
- Keep tests fast (<100ms each)
- Isolate tests (no shared state between tests)
- Use fixtures for DOM elements
- Use spies/stubs for external dependencies
- Clean up after tests (auto-handled by @web/test-runner)
**Web Component Testing**:
```typescript
import { fixture, html, expect } from '@open-wc/testing';
import { UmbMyElement } from './my-element.element';
describe('UmbMyElement', () => {
let element: UmbMyElement;
beforeEach(async () => {
element = await fixture(html`<umb-my-element></umb-my-element>`);
});
it('should render', () => {
expect(element).to.exist;
expect(element.shadowRoot).to.exist;
});
it('should be accessible', async () => {
await expect(element).to.be.accessible();
});
});
```
### Integration Test Guidelines
Integration tests verify interactions between multiple components/systems:
```typescript
describe('UmbContentRepository', () => {
let repository: UmbContentRepository;
let mockContext: UmbMockContext;
beforeEach(() => {
mockContext = new UmbMockContext();
repository = new UmbContentRepository(mockContext);
});
it('should fetch and cache content', async () => {
const result = await repository.requestById('content-123');
expect(result.data).to.exist;
// Verify caching behavior
const cached = await repository.requestById('content-123');
expect(cached.data).to.equal(result.data);
});
});
```
### E2E Test Guidelines
E2E tests with Playwright:
```typescript
import { test, expect } from '@playwright/test';
test.describe('Content Editor', () => {
test('should create new document', async ({ page }) => {
await page.goto('/');
await page.click('[data-test="create-document"]');
await page.fill('[data-test="document-name"]', 'My New Page');
await page.click('[data-test="save"]');
await expect(page.locator('[data-test="success-notification"]')).toBeVisible();
});
});
```
### Mocking Best Practices
**MSW (Mock Service Worker)** for API mocking:
```typescript
import { rest } from 'msw';
export const handlers = [
rest.get('/umbraco/management/api/v1/document/:id', (req, res, ctx) => {
const { id } = req.params;
return res(
ctx.json({
id,
name: 'Test Document',
// ... mock data
})
);
}),
];
```
**Context Mocking**:
```typescript
import { UmbMockContext } from '@umbraco-cms/internal/test-utils';
const mockContext = new UmbMockContext();
mockContext.provideContext(UMB_AUTH_CONTEXT, mockAuthContext);
```
### Running Tests
**Local Development**:
```bash
# Run all tests once
npm test
# Run in watch mode
npm run test:watch
# Run with dev config (faster, less strict)
npm run test:dev
# Run in watch mode with dev config
npm run test:dev-watch
# Run specific test file pattern
npm test -- --files "**/my-component.test.ts"
```
**E2E Tests**:
```bash
# Run E2E tests
npm run test:e2e
# Run in headed mode (see browser)
npx playwright test --headed
# Run specific test
npx playwright test e2e/content-editor.spec.ts
# Debug mode
npx playwright test --debug
```
**CI/CD**:
- All tests run on pull requests
- E2E tests run on Chromium in CI
- Retries: 2 attempts on CI, 0 locally
- Parallel: Sequential in CI, parallel locally
### Coverage
Coverage reporting is currently disabled (see `web-test-runner.config.mjs`):
```javascript
/* TODO: fix coverage report
coverageConfig: {
reporters: ['lcovonly', 'text-summary'],
},
*/
```
**What to Exclude from Coverage**:
- Test files themselves
- Mock data and handlers
- Generated code (OpenAPI clients, icons)
- External wrapper modules
- Type declaration files