Skip to main content

webiny-admin-architect

Admin-side architecture patterns for Webiny extensions. Use this skill when building frontend features with headless features (UseCase/Repository/Gateway), presentation features (Presenter/ViewModel/hooks/components), MobX-based presenters, RegisterFeature, and Admin BuildParams. Covers the admin/ directory structure for both features/ and presentation/ layers.

Informations de source

Dépôt
webiny/webiny-js
Dernière activité de la source
24 juillet 2026 à 12:20
Langue détectée de SKILL.md
anglais
Étoiles
8 045
Forks
682

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
webiny-admin-architect
description
Admin-side architecture patterns for Webiny extensions. Use this skill when building frontend features with headless features (UseCase/Repository/Gateway), presentation features (Presenter/ViewModel/hooks/components), MobX-based presenters, RegisterFeature, and Admin BuildParams. Covers the admin/ directory structure for both features/ and presentation/ layers.
# Admin Architecture Patterns ## TL;DR Admin extensions are React components that register headless features (business logic with no UI) and presentation features (MobX presenters, React hooks, components). Headless features live in `admin/features/` and follow **UseCase → Repository → Gateway** layering. Presentation features live in `admin/presentation/` and add a **Presenter** (MobX view model) layer on top. Both use `createFeature` and `createAbstraction` from `webiny/admin`. **All features — both headless and presentation — MUST provide a `resolve` function** in `createFeature`. This is how the `useFeature` hook accesses resolved instances from the DI container. Without `resolve`, the feature cannot be consumed from React. ## Working Context This skill applies to both **extension developers** (working in `extensions/`) and **core developers** (working in `packages/`). The architecture patterns are identical — only imports differ. | | Extensions (`extensions/`) | Core (`packages/`) | | ----------------- | --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Imports** | `webiny/admin`, `webiny/admin/ui`, `webiny/admin/cms/entry/editor` | `@webiny/app`, `@webiny/admin-ui`, `@webiny/app-headless-cms/...` | | **Catalog paths** | Use the `Import:` path | Use the `Source:` path | | **MobX wrapper** | `createReactiveComponent` from `webiny/admin` | `createReactiveComponent` from `@webiny/app-admin` (never import `observer` from `mobx-react-lite` directly — the only exception is `@webiny/app-admin` itself, which is the source of the re-export) | | **Entry point** | React component as `export default` in a file targeted by `<Admin.Extension src={...}>` | React component registered by the package initializer | Detect which context you're in by checking the file path: `extensions/` → extension mode, `packages/` → core mode. ## Admin Directory Structure ``` admin/ ├── Extension.tsx # Admin entry point (React component) ├── features/ # Headless features (business logic, no UI) │ └── EnableThing/ │ ├── abstractions.ts │ ├── EnableThingUseCase.ts │ ├── EnableThingRepository.ts │ ├── EnableThingGateway.ts │ └── feature.ts └── presentation/ # Presentation layer (hooks, components, presenters) └── CurrentThing/ ├── abstractions.ts # Presenter + ViewModel interfaces ├── CurrentThingPresenter.ts # MobX-based view model ├── useCurrentThing.ts # React hook for consumers ├── feature.ts # createFeature registration └── components/ # React UI components ``` ## Admin Extension Entry Point The Admin entry point is a React component that registers features, providers, UI decorators, and config: ```tsx // src/admin/Extension.tsx import React from "react"; import { AdminConfig, RegisterFeature } from "webiny/admin"; import { CurrentThingFeature } from "./presentation/CurrentThing/feature.js"; import { EnableThingFeature } from "./features/EnableThing/index.js"; import { CurrentThingProvider } from "./presentation/CurrentThing/CurrentThingProvider.js"; import { ThingListView } from "./presentation/ThingListView/ThingListView.js"; export const Extension = () => { return ( <> {/* Register headless features (use cases, repositories, gateways) */} <RegisterFeature feature={EnableThingFeature} /> {/* Register presentation features (presenters, view models) */} <RegisterFeature feature={CurrentThingFeature} /> {/* Providers and UI components */} <CurrentThingProvider /> <ThingListView /> {/* Admin config (menus, routes, etc.) */} <AdminConfig>{/* Menu items, route definitions, etc. */}</AdminConfig> </> ); }; ``` ## Headless Features (`features/`) Headless features contain business logic with no UI — use cases, repositories, and gateways. They follow the same layering as API features but use `webiny/admin` imports. ### Abstractions ```ts // src/admin/features/EnableThing/abstractions.ts import { createAbstraction } from "webiny/admin"; export interface IEnableThingUseCase { execute(id: string): Promise<void>; } export const EnableThingUseCase = createAbstraction<IEnableThingUseCase>("EnableThingUseCase"); export namespace EnableThingUseCase { export type Interface = IEnableThingUseCase; } export interface IEnableThingRepository { execute(id: string): Promise<void>; } export const EnableThingRepository = createAbstraction<IEnableThingRepository>("EnableThingRepository"); export namespace EnableThingRepository { export type Interface = IEnableThingRepository; } export interface IEnableThingGateway { enableThing(id: string): Promise<boolean>; } export const EnableThingGateway = createAbstraction<IEnableThingGateway>("EnableThingGateway"); export namespace EnableThingGateway { export type Interface = IEnableThingGateway; } ``` ### Feature Registration Headless features **must** provide a `resolve` function that returns the resolved use case (or multiple exports). This is what makes the feature consumable via `useFeature()` in the presentation layer: ```ts // src/admin/features/EnableThing/feature.ts import { createFeature } from "webiny/admin"; import { EnableThingUseCase as UseCase } from "./abstractions.js"; import { EnableThingUseCase } from "./EnableThingUseCase.js"; import { EnableThingRepository } from "./EnableThingRepository.js"; import { EnableThingGateway } from "./EnableThingGateway.js"; export const EnableThingFeature = createFeature({ name: "EnableThing", register(container) { container.register(EnableThingUseCase); container.register(EnableThingRepository).inSingletonScope(); container.register(EnableThingGateway).inSingletonScope(); }, resolve(container) { return { useCase: container.resolve(UseCase) }; } }); ``` ## The `useFeature` Hook — Bridging DI and React `useFeature` is the standard way to access headless features from React. It resolves the feature from the DI container and returns whatever `resolve` returned: ```tsx import { useFeature } from "webiny/admin"; import { EnableThingFeature } from "~/features/EnableThing/feature.js"; // In a presentation hook or component: const { useCase } = useFeature(EnableThingFeature); await useCase.execute(id); ``` Presentation hooks wrap `useFeature` to provide a clean, typed API to React components: ```tsx // src/admin/presentation/EnableThing/useEnableThing.ts import { useFeature } from "webiny/admin"; import { EnableThingFeature } from "~/features/EnableThing/feature.js"; export function useEnableThing() { const { useCase } = useFeature(EnableThingFeature); return { enableThing: useCase.execute.bind(useCase) }; } ``` ### Common `useFeature` patterns **Multiple features in one hook:** ```tsx export function useAuthentication() { const { useCase: logInUseCase } = useFeature(LogInFeature); const { useCase: logOutUseCase } = useFeature(LogOutFeature); return { login: logInUseCase.execute.bind(logInUseCase), logout: logOutUseCase.execute.bind(logOutUseCase) }; } ``` **MobX reactive state synced to React:** ```tsx export function useIdentity() { const { identityContext } = useFeature(IdentityContextFeature); const [identity, setIdentity] = useState(identityContext.getIdentity()); useEffect(() => { return autorun(() => { setIdentity(identityContext.getIdentity()); }); }, [identityContext]); return { identity, isAuthenticated: identity.isAuthenticated }; } ``` ## Presentation Features (`presentation/`) Presentation features contain the UI layer — a **Presenter** (MobX view model) that produces a ViewModel for React, plus hooks and components. The Presenter typically depends on a **headless feature abstraction** (use case or service) for data and actions — it does NOT duplicate the repository/gateway layering. ### Typical Pattern: Presenter depends on a headless feature The Presenter injects a headless feature's use case or service as a DI dependency: ```ts // src/admin/presentation/CurrentThing/abstractions.ts import { createAbstraction } from "webiny/admin"; import type { MyEntity } from "~/shared/MyEntity.js"; export interface ICurrentThingVm { loading: boolean; entity: MyEntity | undefined; error: Error | undefined; } export interface ICurrentThingPresenter { vm: ICurrentThingVm; init(): void; } export const CurrentThingPresenter = createAbstraction<ICurrentThingPresenter>("CurrentThingPresenter"); export namespace CurrentThingPresenter { export type Interface = ICurrentThingPresenter; export type ViewModel = ICurrentThingVm; } ``` ```ts // src/admin/presentation/CurrentThing/feature.ts import { createFeature } from "webiny/admin"; import { CurrentThingPresenter as PresenterAbstraction } from "./abstractions.js"; import { CurrentThingPresenter } from "./CurrentThingPresenter.js"; export const CurrentThingFeature = createFeature({ name: "CurrentThing", register(container) { container.register(CurrentThingPresenter); }, resolve(container) { return { presenter: container.resolve(PresenterAbstraction) }; } }); ``` The Presenter implementation injects the headless feature's abstraction (e.g., a use case): ```ts // src/admin/presentation/CurrentThing/CurrentThingPresenter.ts import { GetThingUseCase } from "~/features/GetThing/abstractions.js"; class CurrentThingPresenterImpl implements CurrentThingPresenter.Interface { vm: CurrentThingPresenter.ViewModel = { loading: false, entity: undefined, error: undefined }; constructor(private getThingUseCase: GetThingUseCase.Interface) {} async init() { this.vm.loading = true; const result = await this.getThingUseCase.execute(); // ... update vm } } export default CurrentThingPresenter.createImplementation({ implementation: CurrentThingPresenterImpl, dependencies: [GetThingUseCase] }); ``` ### One-off Pattern: Presenter with its own repository/gateway On rare occasions, when a presentation feature does not contain reusable business logic (no headless feature to depend on), the presentation feature can contain its own repository and gateway alongside the presenter: ```ts // src/admin/presentation/Dashboard/feature.ts — one-off, no headless feature import { createFeature } from "webiny/admin"; import { DashboardPresenter as PresenterAbstraction } from "./abstractions.js"; import { DashboardPresenter } from "./DashboardPresenter.js"; import { DashboardRepository } from "./DashboardRepository.js"; import { DashboardGateway } from "./DashboardGateway.js"; export const DashboardFeature = createFeature({ name: "Dashboard", register(container) { container.register(DashboardPresenter); container.register(DashboardRepository).inSingletonScope(); container.register(DashboardGateway).inSingletonScope(); }, resolve(container) { return { presenter: container.resolve(PresenterAbstraction) }; } }); ``` > **Prefer the typical pattern.** Only use the one-off pattern when the business logic is truly presentation-specific and will never be reused by other features. ## Observable Service Implementation For long-lived services that hold observable state (e.g., project config, feature flags): ```ts import { makeAutoObservable, runInAction } from "mobx"; import { WcpService as ServiceAbstraction, WcpGateway } from "./abstractions.js"; class WcpServiceImpl implements ServiceAbstraction.Interface { private project: ILicense | null = null; constructor(private gateway: WcpGateway.Interface) { makeAutoObservable(this); } getProject(): ILicense { return this.project; } async loadProject(): Promise<void> { const data = await this.gateway.fetchProject(); runInAction(() => { this.project = data; }); } } export const WcpService = ServiceAbstraction.createImplementation({ implementation: WcpServiceImpl, dependencies: [WcpGateway] }); ``` - Registered in **singleton scope** — long-lived, holds state - Use `makeAutoObservable(this)` in the constructor - Wrap async state mutations in `runInAction` ## Repository Implementation Repositories own domain data and cache. They use MobX for reactivity: ```ts import { makeAutoObservable, runInAction } from "mobx"; import { NextjsConfigRepository as RepositoryAbstraction, NextjsConfigGateway, NextjsConfig } from "./abstractions.js"; class NextjsConfigRepositoryImpl implements RepositoryAbstraction.Interface { private config: NextjsConfig | undefined = undefined; constructor(private gateway: NextjsConfigGateway.Interface) { makeAutoObservable(this); }
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub