opticore-feature-component
v1.0.0
Published
CLI to create feature component
Downloads
131
Readme
opticore-feature-component
OptiCore Feature component est un package qui permet via l'interaction d'une CLI de générer des features dans un projet OptiCoreJs.
Trois modes de scaffolding sont proposés : Simple Component, Clean Architecture pas à pas, et Full Clean Architecture.
Table des matières
- Prérequis
- Installation
- Lancer le CLI
- Flux interactif global
- Option 1 — Simple Component
- Option 2 — CLEAN Architecture by step
- Option 3 — Full CLEAN Architecture component
- Règles de nommage
- Enregistrement automatique du router
- Contribuer
Prérequis
- Node.js ≥ 18
- TypeScript ≥ 5
- Un projet qui expose le répertoire
src/features/à la racine (le CLI y crée les features)
mon-projet > src > app > router > register.router.tsLe fichier register.router.ts se met automatiquement à jour lors de la création du router de la feature
Si le dossier
featuresest absent au lancement du server, le CLI affiche une erreur et s'arrête.
Installation
En tant que dépendance de développement (recommandé)
npm install --save-dev opticore-feature-component
# ou
yarn add -D opticore-feature-componentGlobalement
npm install -g opticore-feature-componentDepuis les sources (monorepo)
# depuis la racine du package
npm install
npm run buildLancer le CLI
Avec npx ou npm (sans installation globale)
npx opticore-feature-componentnpm exec opticore-feature-componentVia le script package.json du projet (recommandé)
Ajoutez un script dans le package.json de votre projet :
{
"scripts": {
"feature": "opticore-feature-component"
}
}Puis lancez :
npm run feature
# ou
yarn featureInstallation globale
opticore-feature-componentFlux interactif global
À chaque lancement, le CLI suit toujours ce flux en 3 étapes avant de proposer les options :
╭────────────────────────────────────────────────────╮
│ │
│ Welcome to Feature Component CLI │
│ │
╰────────────────────────────────────────────────────╯
◆ Please choose the creation principle for your feature :
│ ● OptiCoreJs CLEAN Module (recommended)
│ ○ Custom feature
└
◆ Enter feature's name :
│ login
└
◆ Choose a type of component :
│ ○ Simple component
│ ○ CLEAN Architecture component by step
│ ● Full CLEAN Architecture component (default)
└Étape 1 — Principe de création
| Choix | Comportement |
|---|---|
| OptiCoreJs CLEAN Module | Active le scaffolding automatisé — continue vers l'étape 2 |
| Custom feature | Affiche un message d'information et quitte (création manuelle) |
Étape 2 — Nom de la feature
Le nom doit respecter la règle : ^[a-z][A-Za-z]+$
→ camelCase, commence par une minuscule, minimum 2 caractères.
✅ userProfile
✅ productOrder
✅ authToken
❌ UserProfile (commence par une majuscule)
❌ user_profile (underscore interdit)
❌ user (1 seul caractère après la première lettre)Étape 3 — Type de composant → voir les sections suivantes.
Appuyer sur Ctrl+C à n'importe quelle étape annule l'opération et supprime les répertoires éventuellement créés.
Option 1 — Simple Component
Structure plate et pragmatique. Idéal pour des features légères sans couche de domaine.
Ce qui est généré
src/features/<featureName>/
├── models/
│ └── <featureName>.model.ts
├── repositories/
│ └── <featureName>.repository.ts
├── services/
│ └── <featureName>.service.ts
├── controllers/
│ └── <featureName>.controller.ts
└── routes/
├── <featureName>.router.handler.ts
└── <featureName>.router.tsExemple — feature order
src/features/order/
├── models/
│ └── order.model.ts
├── repositories/
│ └── order.repository.ts
├── services/
│ └── order.service.ts
├── controllers/
│ └── order.controller.ts
└── routes/
├── order.router.handler.ts
└── order.router.tsLe CLI demande si vous souhaitez des méthodes dans le controller :
◆ Do you want to add methods to the controller?
│ ● Yes ○ No
└
◆ Enter the method names (comma separated):
│ create, findAll, findById, update, delete
└Contenu généré — order.service.ts
import { OrderRepository } from "../repositories/order.repository";
import { OrderModel } from "../models/order.model";
export class OrderService {
private readonly repository: OrderRepository;
constructor() {
this.repository = new OrderRepository();
}
async findAll(): Promise<OrderModel[]> {
return this.repository.findAll();
}
async findById(id: string): Promise<OrderModel | null> {
return this.repository.findById(id);
}
async create(data: Record<string, unknown>): Promise<OrderModel> {
const model = new OrderModel(String(Date.now()));
// TODO: Map data fields onto model
return this.repository.create(model);
}
async update(id: string, data: Record<string, unknown>): Promise<OrderModel | null> {
const existing = await this.repository.findById(id);
if (!existing) return null;
// TODO: Apply data fields onto existing model
return this.repository.update(existing);
}
async delete(id: string): Promise<boolean> {
return this.repository.delete(id);
}
}Contenu généré — order.controller.ts (méthodes create, findAll)
import { Request, Response } from "express";
import { ResponseHandler, HttpStatusCode, IResponseHandlerSuccessData } from "opticore-http-response";
import { OrderService } from "../services/order.service";
export class OrderController {
private static buildService(): OrderService {
return new OrderService();
}
static async create(req: Request, res: Response): Promise<IResponseHandlerSuccessData | ReturnType<typeof ResponseHandler.error>> {
try {
const service = OrderController.buildService();
const result = await service.create(req.body);
return ResponseHandler.success(result, "created", HttpStatusCode.CREATED);
} catch (error) {
return OrderController.handleError(error);
}
}
static async findAll(req: Request, res: Response): Promise<IResponseHandlerSuccessData | ReturnType<typeof ResponseHandler.error>> {
try {
const service = OrderController.buildService();
const results = await service.findAll();
return ResponseHandler.success(results, "success", HttpStatusCode.OK);
} catch (error) {
return OrderController.handleError(error);
}
}
private static handleError(error: unknown) {
const message = error instanceof Error ? error.message : "Internal server error";
return ResponseHandler.error(message, HttpStatusCode.INTERNAL_SERVER_ERROR);
}
}Mapping HTTP automatique des méthodes
Le CLI déduit le verbe et le chemin HTTP à partir du nom de méthode :
| Nom de méthode (exemples) | Verbe | Chemin |
|---|---|---|
| findAll, getAll | GET | /<featureName> |
| findById, getById, getOne | GET | /<featureName>/:id |
| create, add | POST | /<featureName> |
| update, edit | PUT | /<featureName>/:id |
| delete, remove | DELETE | /<featureName>/:id |
| tout autre nom | GET | /<featureName>/<methodName> |
Option 2 — CLEAN Architecture by step
Mode interactif file-par-file. Le CLI propose chaque composant un à un et ne crée que ceux que vous confirmez. Tous les fichiers créés sont vides — aucun template n'est injecté.
Déroulement
── Domain ──────────────────────────────────────────────────────
◆ Entity → payment.entity.ts
│ ○ Yes ● No
└
◆ Event → payment.event.ts
│ ● Yes ○ No
└
✅ Created: src/features/payment/domain/events/payment.event.ts
◆ Exception → payment.exception.ts
│ ○ Yes ● No
└
── Application ─────────────────────────────────────────────────
◆ Repo Interface → payment.repository.interface.ts
│ ● Yes ○ No
└
✅ Created: src/features/payment/application/ports/repositories/payment.repository.interface.ts
◆ Presenter Port → payment.presenter.interface.ts
│ ○ Yes ● No
└
◆ Service Port → payment.service.ts
│ ○ Yes ● No
└
◆ DTO → payment.dto.ts
│ ● Yes ○ No
└
✅ Created: src/features/payment/application/dtos/payment.dto.ts
◆ Use Case → payment.usecase.ts
│ ● Yes ○ No
└
✅ Created: src/features/payment/application/use-cases/payment.usecase.ts
── Infrastructure ──────────────────────────────────────────────
◆ Repo Impl → payment.repository.ts
│ ● Yes ○ No
└
✅ Created: src/features/payment/infrastructure/adapters/repositories/payment.repository.ts
◆ Presenter Impl → payment.presenter.ts
│ ○ Yes ● No
└
◆ Controller → payment.controller.ts
│ ● Yes ○ No
└
✅ Created: src/features/payment/infrastructure/adapters/controllers/payment.controller.ts
◆ Router Handler → payment.router.handler.ts
│ ○ Yes ● No
└
◆ Router → payment.router.ts
│ ○ Yes ● No
└
🎉 Feature "payment" — 5 file(s) created step by step.Résultat pour l'exemple ci-dessus
src/features/payment/
├── application/
│ ├── dtos/
│ │ └── payment.dto.ts ← vide
│ ├── ports/
│ │ └── repositories/
│ │ └── payment.repository.interface.ts ← vide
│ └── use-cases/
│ └── payment.usecase.ts ← vide
└── infrastructure/
└── adapters/
├── controllers/
│ └── payment.controller.ts ← vide
└── repositories/
└── payment.repository.ts ← videLes répertoires ne sont créés que pour les fichiers confirmés.
Si aucun fichier n'est sélectionné, rien n'est créé sur le disque.
Composants disponibles
| Groupe | Label affiché | Fichier créé | Répertoire |
|---|---|---|---|
| Domain | Entity | <n>.entity.ts | domain/entities/ |
| Domain | Event | <n>.event.ts | domain/events/ |
| Domain | Exception | <n>.exception.ts | domain/exceptions/ |
| Application | Repo Interface | <n>.repository.interface.ts | application/ports/repositories/ |
| Application | Presenter Port | <n>.presenter.interface.ts | application/ports/presenters/ |
| Application | Service Port | <n>.service.ts | application/ports/services/ |
| Application | DTO | <n>.dto.ts | application/dtos/ |
| Application | Use Case | <n>.usecase.ts | application/use-cases/ |
| Infrastructure | Repo Impl | <n>.repository.ts | infrastructure/adapters/repositories/ |
| Infrastructure | Presenter Impl | <n>.presenter.ts | infrastructure/adapters/presenters/ |
| Infrastructure | Controller | <n>.controller.ts | infrastructure/adapters/controllers/ |
| Infrastructure | Router Handler | <n>.router.handler.ts | infrastructure/routes/ |
| Infrastructure | Router | <n>.router.ts | infrastructure/routes/ |
Option 3 — Full CLEAN Architecture component
Génère l'intégralité de la structure Clean Architecture en une seule commande. Chaque fichier est pré-rempli avec un template TypeScript fonctionnel et prêt à être adapté.
Ce qui est généré
src/features/<featureName>/
├── domain/
│ ├── entities/
│ │ └── <featureName>.entity.ts
│ ├── events/
│ │ └── <featureName>.event.ts
│ └── exceptions/
│ └── <featureName>.exception.ts
├── application/
│ ├── dtos/
│ │ └── <featureName>.dto.ts
│ ├── ports/
│ │ ├── repositories/
│ │ │ └── <featureName>.repository.interface.ts
│ │ ├── presenters/
│ │ │ └── <featureName>.presenter.interface.ts
│ │ └── services/
│ │ └── <featureName>.service.ts
│ └── use-cases/
│ └── <featureName>.usecase.ts
└── infrastructure/
├── adapters/
│ ├── controllers/
│ │ └── <featureName>.controller.ts
│ ├── repositories/
│ │ └── <featureName>.repository.ts
│ └── presenters/
│ └── <featureName>.presenter.ts
└── routes/
├── <featureName>.router.handler.ts
└── <featureName>.router.ts13 fichiers, 12 répertoires — générés en une seule interaction.
Exemple — feature invoice
Seule question posée pendant la génération : les méthodes du controller.
◆ Do you want to add methods to the controller?
│ ● Yes ○ No
└
◆ Enter the method names (comma separated):
│ create, findAll, findById, update, delete
└
✅ Entity created: src/features/invoice/domain/entities/invoice.entity.ts
✅ Event created: src/features/invoice/domain/events/invoice.event.ts
✅ Exception created: src/features/invoice/domain/exceptions/invoice.exception.ts
✅ Repository interface created: src/features/invoice/application/ports/repositories/invoice.repository.interface.ts
✅ Presenter interface created: src/features/invoice/application/ports/presenters/invoice.presenter.interface.ts
✅ Service interface created: src/features/invoice/application/ports/services/invoice.service.ts
✅ DTO created: src/features/invoice/application/dtos/invoice.dto.ts
✅ UseCase created: src/features/invoice/application/use-cases/invoice.usecase.ts
✅ Repository implementation: src/features/invoice/infrastructure/adapters/repositories/invoice.repository.ts
✅ Presenter implementation: src/features/invoice/infrastructure/adapters/presenters/invoice.presenter.ts
✅ Controller created: src/features/invoice/infrastructure/adapters/controllers/invoice.controller.ts
✅ Router Handler created: src/features/invoice/infrastructure/routes/invoice.router.handler.ts
✅ Router created: src/features/invoice/infrastructure/routes/invoice.router.ts
✅ register.router.ts updated with InvoiceRouter.
🎉 Feature "invoice" scaffolded with Clean Architecture!Contenu généré — invoice.entity.ts
/**
* InvoiceEntity — Domain Entity
* Represents the core business object for the "invoice" feature.
* No framework dependency, pure business logic only.
*/
export class InvoiceEntity {
private readonly _id: string;
private _createdAt: Date;
private _updatedAt: Date;
constructor(
id: string,
// TODO: Add your business properties here
createdAt?: Date,
updatedAt?: Date,
) {
this._id = id;
this._createdAt = createdAt ?? new Date();
this._updatedAt = updatedAt ?? new Date();
this.validate();
}
get id(): string { return this._id; }
get createdAt(): Date { return this._createdAt; }
get updatedAt(): Date { return this._updatedAt; }
public touch(): void {
this._updatedAt = new Date();
}
public toSnapshot(): Record<string, unknown> {
return { id: this._id, createdAt: this._createdAt, updatedAt: this._updatedAt };
}
private validate(): void {
if (!this._id || this._id.trim().length === 0) {
throw new Error(`[InvoiceEntity] id must not be empty.`);
}
// TODO: Add your domain invariant checks here
}
}Contenu généré — invoice.usecase.ts
import { IInvoiceRepository } from "../ports/repositories/invoice.repository.interface";
import { InvoiceEntity } from "../../domain/entities/invoice.entity";
import {
CreateInvoiceDto,
UpdateInvoiceDto,
InvoiceResponseDto,
InvoiceDtoMapper,
} from "../dtos/invoice.dto";
/**
* InvoiceUseCase — Application Use Case
*
* Orchestrates business operations for the "invoice" feature.
* Depends only on the repository port (interface), never on a concrete implementation.
*/
export class InvoiceUseCase {
constructor(private readonly repository: IInvoiceRepository) {}
async findAll(): Promise<InvoiceResponseDto[]> {
const entities = await this.repository.findAll();
return InvoiceDtoMapper.toResponseList(entities);
}
async findById(id: string): Promise<InvoiceResponseDto | null> {
const entity = await this.repository.findById(id);
if (!entity) return null;
return InvoiceDtoMapper.toResponse(entity);
}
async create(dto: CreateInvoiceDto): Promise<InvoiceResponseDto> {
const id = crypto.randomUUID();
const entity = new InvoiceEntity(id /* TODO: pass dto fields */);
const saved = await this.repository.create(entity);
return InvoiceDtoMapper.toResponse(saved);
}
async update(dto: UpdateInvoiceDto): Promise<InvoiceResponseDto | null> {
const existing = await this.repository.findById(dto.id);
if (!existing) return null;
existing.touch();
const updated = await this.repository.update(existing);
if (!updated) return null;
return InvoiceDtoMapper.toResponse(updated);
}
async delete(id: string): Promise<boolean> {
return this.repository.delete(id);
}
}Contenu généré — invoice.router.handler.ts (méthodes create, findAll, findById)
import { OpticoreRouting, ICustomContext, IMultipleRouteDefinition } from "opticore-router";
import { InvoiceController } from "../adapters/controllers/invoice.controller";
export const InvoiceHandlerRouter: () => IMultipleRouteDefinition = () => {
return OpticoreRouting.routes(
InvoiceController,
[
{
path: `/invoice`,
method: "post",
middlewares: [],
handler: async (ctx: ICustomContext) => await InvoiceController.create(ctx.req, ctx.res)
},
{
path: `/invoice`,
method: "get",
middlewares: [],
handler: async (ctx: ICustomContext) => await InvoiceController.findAll(ctx.req, ctx.res)
},
{
path: `/invoice/:id`,
method: "get",
middlewares: [],
handler: async (ctx: ICustomContext) => await InvoiceController.findById(ctx.req, ctx.res)
}
]
);
};Règles de nommage
Le nom de la feature est soumis à validation stricte :
| Règle | Détail |
|---|---|
| Format | camelCase — ^[a-z][A-Za-z]+$ |
| Premier caractère | Minuscule obligatoire |
| Longueur minimale | 2 caractères |
| Caractères autorisés | Lettres uniquement (a-z, A-Z) |
| Caractères interdits | Chiffres, underscore, tiret, espaces |
Le CLI rejette le nom si la feature existe déjà dans src/features/.
Enregistrement automatique du router
Lors de la génération des options Simple Component et Full CLEAN Architecture, le router de la feature est automatiquement enregistré dans src/app/router/register.router.ts.
Avant :
export const registerRouter: () => TFeatureRoutes[] = (): TFeatureRoutes[] => {
return new OpticoreRegisterRouter().registered([
AuthenticationRouter,
]);
}Après (ajout de InvoiceRouter) :
import { InvoiceRouter } from "../../features/invoice/infrastructure/routes/invoice.router";
export const registerRouter: () => TFeatureRoutes[] = (): TFeatureRoutes[] => {
return new OpticoreRegisterRouter().registered([
AuthenticationRouter,
InvoiceRouter,
]);
}Si
register.router.tsest introuvable, un avertissement est affiché mais la génération continue normalement.
En mode CLEAN by step, l'enregistrement automatique n'est pas effectué car les fichiers sont vides.
Récapitulatif des options
| Option | Fichiers créés | Contenu | Interaction | |---|---|---|---| | Simple Component | 6 | Avec template | Nom des méthodes du controller | | CLEAN by step | 0 à 13 au choix | Vides | Confirmation pour chaque fichier | | Full CLEAN Architecture | 13 | Avec template | Nom des méthodes du controller |
Contribuer
opticore-feature-component est open source.
Pour contribuer : clonez le dépôt et ouvrez une pull request.
- Repository : github.com/guyzoum77/opticore-feature-cli
- Issues : github.com/guyzoum77/opticore-feature-cli/issues
Auteur : Guy-serge Kouacou — Licence MIT
