writing-client-code

作者: bitwarden

Bitwarden 客戶端程式碼慣例,適用於 Angular 與 TypeScript。用於建立元件、服務,或修改網頁/瀏覽器/桌面應用程式時使用。

npx skills add https://github.com/bitwarden/clients --skill writing-client-code

Why libs/common cannot import Angular

CLI is a first-class client. Any code in libs/common must work without Angular's dependency injection, decorators, or lifecycle hooks. This is why cross-client services use abstract classes as interfaces — the concrete implementations (Default*, Web*, Browser*, Desktop*, Cli*) live in their respective apps.

Architectural Rationale

Thin components

Components contain only view logic. Business logic belongs in services. This keeps components testable, reusable, and prevents Angular lifecycle coupling from leaking into domain logic.

Composition over inheritance

Avoid extending components across clients. Compose using shared child components instead. Inheritance creates tight coupling between client-specific UI and shared behavior — when one client's needs diverge, inherited components become hard to change safely.

Don't modernize existing code unless asked

The codebase contains both legacy and modern Angular patterns. When modifying an existing file, follow the patterns already in that file. Don't migrate any of these unless explicitly asked:

  • *ngIf → @if, *ngFor → @for
  • @Input() / @Output() → input() / output() signals
  • Constructor injection → inject()
  • Default change detection → OnPush
  • NgModule declarations → standalone components

If asked to modernize, follow this order (per the Angular migration guide): standalone → control flow → input/output signals → view queries → signals → computed → OnPush (last, only after full signal migration).

State management: Signals vs RxJS

  • Component local state and Angular-only services: Use Signals
  • Cross-client services (libs/common): Use RxJS (because CLI has no Angular Signals support)

Avoid manual subscriptions. Prefer | async pipe. When subscriptions are necessary, pipe through takeUntilDestroyed() — enforced by the prefer-takeUntil lint rule.

No TypeScript enums (ADR-0025)

Use frozen const objects with Object.freeze() and as const, plus a companion type alias. Enums have runtime behavior that creates subtle bugs with tree-shaking.

Critical Rules for New Code

These rules apply strictly to new files and components. For existing code, follow the patterns already in the file.

  • New components must use ChangeDetectionStrategy.OnPush and be standalone: true. NgModules are permitted only for grouping related standalone components
  • Prefer inject() function for DI in Angular primitives (components, pipes, directives). Use constructor injection for code shared with non-Angular clients (CLI)
  • New templates must use control flow syntax (@if, @for, @switch), not structural directives
  • Use host property in component decorators, not @HostBinding / @HostListener
  • Use Reactive Forms exclusively — not template-driven forms
  • File naming: kebab-case.component.ts, .service.ts, .pipe.ts, .directive.ts. Also: .request.ts, .response.ts, .view.ts, .data.ts for models (ADR-0012)
  • All Tailwind classes require tw- prefix — tw-flex, tw-mt-2, not flex, mt-2
  • Testing with Jest — use jest-mock-extended for mocking services. describe/it blocks, not test()
  • Imports from @bitwarden/common must not pull in Angular-specific code (breaks CLI)

Examples

Dependency injection (new Angular code)

// CORRECT — inject() for Angular primitives
export class VaultComponent {
  private vaultService = inject(VaultService);
}

// ALSO CORRECT — constructor injection for code shared with CLI
export class CryptoService {
  constructor(private stateService: StateService) {}
}

Tailwind prefix

<!-- CORRECT -->
<div class="tw-flex tw-gap-2 tw-mt-4">
  <!-- WRONG — missing tw- prefix, will be stripped -->
  <div class="flex gap-2 mt-4"></div>
</div>

Const objects over enums (ADR-0025)

// CORRECT — with companion type alias
export const CipherType = Object.freeze({
  Login: 1,
  SecureNote: 2,
} as const);
export type CipherType = (typeof CipherType)[keyof typeof CipherType];

// WRONG — TypeScript enums have runtime side effects
export enum CipherType {
  Login = 1,
  SecureNote = 2,
}

Further Reading

來自 bitwarden 的更多技能

figma-to-angular
bitwarden
此技能可將 Figma 設計規格轉換為 Bitwarden Clients 單一儲存庫中,具備 Storybook 故事的完整 Angular 元件。輸出結果應在視覺上符合設計,同時遵循所有程式碼庫慣例。
force-multiplier
bitwarden
將單一意圖同時套用於多個目標——例如 Bitwarden 生態系中的一組儲存庫,或單一 monorepo 內的多個專案——以 N 個一致的操作來執行,…
analyzing-git-sessions
bitwarden
分析指定時間範圍或提交範圍內的 Git 提交與變更,提供結構化摘要,適用於程式碼審查、回顧會議、工作日誌或工作階段…
coordinating-cross-team-breakdown
bitwarden
協調跨團隊審查與簽核 Bitwarden 技術分解。用於識別受影響團隊、建立第三部分簽核表格、追蹤…
assessing-jira-issue-relevance
bitwarden
當使用者提供單一Jira議題金鑰,並詢問該議題是否仍相關、仍適用、仍待處理、仍是錯誤、已修復,或可否……時使用。
assessing-test-coverage
bitwarden
用於判斷特定變更(PR、Jira key、Tech Breakdown 文件、Testmo CSV、變更路徑或具名……)已存在哪些測試覆蓋範圍時使用。
retrospecting
bitwarden
對 Claude Code 工作階段進行全面分析,檢視 Git 歷史記錄、對話日誌、程式碼變更,並收集使用者回饋以產生…
reviewing-incremental-changes
bitwarden
在重新審視已有評論的PR,或回應開發者在初次審查後的變更時,使用此技能。適用於存在PR討論串或…的情況。