npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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.

License: MIT npm version


Table des matières


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.ts

Le fichier register.router.ts se met automatiquement à jour lors de la création du router de la feature

Si le dossier features est 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-component

Globalement

npm install -g opticore-feature-component

Depuis les sources (monorepo)

# depuis la racine du package
npm install
npm run build

Lancer le CLI

Avec npx ou npm (sans installation globale)

npx opticore-feature-component
npm exec opticore-feature-component

Via 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 feature

Installation globale

opticore-feature-component

Flux 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.ts

Exemple — 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.ts

Le 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                ← vide

Les 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.ts

13 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.ts est 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.

Auteur : Guy-serge Kouacou — Licence MIT