npm.io
2026.1.1 โ€ข Published 7 months ago

@expressive-tea/plugin

Licence
Apache-2.0
Version
2026.1.1
Deps
3
Size
71 kB
Vulns
0
Weekly
0

@expressive-tea/plugin

Build once, reuse everywhere - The plugin system that scales with your dreams

npm version npm downloads license

Versioning: This package uses Calendar Versioning (CalVer) in the format YYYY.MM.MICRO (e.g., 2026.1.0). Learn more โ†’


Version Compatibility

This package (v2026.1.0) requires @expressive-tea/core >= 2.0.0

  • Compatible: @expressive-tea/core@2.x.x and above
  • Not compatible: @expressive-tea/core@1.x.x or below

Using Expressive Tea Core v1.x? You'll need @expressive-tea/plugin@1.0.3 (last SemVer version for Core v1.x).

Expressive Tea Core Reference: This package is designed to work with Expressive Tea Core - the main framework.


The Plugin Problem, Solved

Ever copy-pasted your database config across 10 projects?
Ever wished you could share your auth setup between apps?
Ever wanted to publish reusable middleware bundles to npm?

Say hello to the plugin system that makes code reuse actually enjoyable!

Features That'll Blow Your Mind

  • True Plugin Architecture - Write once, drop into any Expressive Tea app
  • Lifecycle Hooks (Boot Stages) - Control exactly when your code runs
  • Smart Dependencies - Plugins can depend on other plugins (no spaghetti!)
  • Priority-Based - Fine-tune execution order with simple numbers
  • Decorator Magic - Just @Stage and you're done
  • Battle-Tested - 93%+ coverage - we don't ship bugs
  • Self-Documenting - Every API has JSDoc with examples
  • Type-Safe - TypeScript strict mode = fewer runtime surprises

Installation

# npm
npm install @expressive-tea/plugin reflect-metadata

# yarn  
yarn add @expressive-tea/plugin reflect-metadata

# pnpm
pnpm add @expressive-tea/plugin reflect-metadata

Requirements

  • Node.js โ‰ฅ 18.0.0 (LTS recommended)
  • TypeScript โ‰ฅ 5.0.0 (if you're using TypeScript)
  • reflect-metadata 0.2.x
  • @expressive-tea/commons 2.0.0 (auto-installed as peer dep)

TypeScript Setup

Enable decorators in your tsconfig.json:

{
  "compilerOptions": {
    "experimentalDecorators": true,     // ๐ŸŽจ Required for @Stage
    "emitDecoratorMetadata": true,       // ๐Ÿ“Š Required for metadata
    "target": "ES2017",
    "module": "commonjs"
  }
}

Quick Start (60 seconds)

1. Create a Plugin
import 'reflect-metadata';
import { Plugin, BOOT_STAGES, Stage } from '@expressive-tea/plugin';
import { Express } from 'express';

export class SuperAwesomePlugin extends Plugin {
  // Metadata (who are you?)
  protected name = 'SuperAwesomePlugin';
  protected priority = 100;              // Lower = runs first
  protected dependencies: string[] = []; // Other plugins you need

  // Hook into BOOT_DEPENDENCIES stage
  @Stage(BOOT_STAGES.BOOT_DEPENDENCIES)
  async connectDatabase(server: Express): Promise<void> {
    console.log('๐Ÿ—„๏ธ  Connecting to database...');
    // await db.connect();
  }

  // Hook into APPLICATION stage
  @Stage(BOOT_STAGES.APPLICATION)
  setupMiddleware(server: Express): void {
    console.log('๐Ÿ› ๏ธ  Adding middleware...');
    server.use((req, res, next) => {
      console.log(`${req.method} ${req.path}`);
      next();
    });
  }
}
2. Use Your Plugin
import { Boot, ServerSettings, Pour } from '@expressive-tea/core';
import { SuperAwesomePlugin } from './plugins/awesome.plugin';

@ServerSettings({ port: 3000 })
@Pour(new SuperAwesomePlugin())
class MyApp extends Boot {}

const app = new MyApp();
app.start();

// Output:
// ๐Ÿ—„๏ธ  Connecting to database...
// ๐Ÿ› ๏ธ  Adding middleware...
// ๐ŸŽ‰ Server running on http://localhost:3000

BOOM! You just created a reusable plugin!

Understanding Boot Stages

Think of boot stages as lifecycle hooks - your plugin methods run at specific points during app startup:

# Stage When Perfect For
1 BOOT_DEPENDENCIES Very first Database, Redis, external APIs
2 APPLICATION Express setup Body parsers, CORS, Helmet
3 CONTROLLERS Route registration REST endpoints, GraphQL
4 APPLICATION_MIDDLEWARES Before routes Auth, validation, rate limiting
5 ROUTE_MIDDLEWARES Route-specific Per-route guards
6 SERVER_SETTINGS Final setup Port, SSL, clustering
7 AFTER_APPLICATION_MIDDLEWARES Post-routing Error handlers, 404
Real-World Boot Stage Example
export class DatabasePlugin extends Plugin {
  protected name = 'DatabasePlugin';
  protected priority = 10; // Run early!
  
  @Stage(BOOT_STAGES.BOOT_DEPENDENCIES)
  async connectDB(server: Express) {
    console.log('1๏ธโƒฃ Connecting to PostgreSQL...');
    // await pool.connect();
  }

  @Stage(BOOT_STAGES.APPLICATION)
  addDBMiddleware(server: Express) {
    console.log('2๏ธโƒฃ Adding database middleware...');
    // server.use((req, res, next) => {
    //   req.db = pool;
    //   next();
    // });
  }

  @Stage(BOOT_STAGES.AFTER_APPLICATION_MIDDLEWARES)
  addErrorHandler(server: Express) {
    console.log('3๏ธโƒฃ Adding DB error handler...');
    // server.use((err, req, res, next) => {
    //   if (err.code === 'ECONNREFUSED') {
    //     res.status(503).json({ error: 'Database unavailable' });
    //   }
    // });
  }
}

Plugin Dependencies (The Cool Part)

Plugins can depend on other plugins - the system ensures they load in the right order!

export class SessionPlugin extends Plugin {
  protected name = 'SessionPlugin';
  protected dependencies = ['DatabasePlugin']; // Needs DB first!

  @Stage(BOOT_STAGES.APPLICATION)
  setupSessions(server: Express) {
    console.log('๐Ÿ’พ Setting up sessions (DB already connected!)');
    // session logic here
  }
}

export class AuthPlugin extends Plugin {
  protected name = 'AuthPlugin';
  protected dependencies = ['DatabasePlugin', 'SessionPlugin'];

  @Stage(BOOT_STAGES.APPLICATION)
  setupAuth(server: Express) {
    console.log('๐Ÿ” Setting up auth (DB + sessions ready!)');
    // passport.js setup
  }
}

// When you use them:
@Pour(new DatabasePlugin())
@Pour(new SessionPlugin())
@Pour(new AuthPlugin())
class MyApp extends Boot {}

// They'll execute in the correct order automatically!
// Output:
// 1. DatabasePlugin methods
// 2. SessionPlugin methods  
// 3. AuthPlugin methods

Missing a dependency? You'll get a helpful DependencyNotFound error at startup!

Priority System (Fine-Grained Control)

Within the same boot stage, lower priority = runs first:

// Runs FIRST (priority: 10)
export class CorePlugin extends Plugin {
  protected priority = 10;
}

// Runs SECOND (priority: 100)
export class MiddlewarePlugin extends Plugin {
  protected priority = 100;
}

// Runs LAST (priority: 999 - the default)
export class OptionalPlugin extends Plugin {
  protected priority = 999;
}

Pro tip: Use priority 1-100 for critical infrastructure, 100-500 for features, 500+ for optional stuff.

Advanced Patterns

Pattern 1: Configurable Plugins
interface DatabaseConfig {
  host: string;
  port: number;
  database: string;
  ssl: boolean;
}

export class DatabasePlugin extends Plugin {
  private config: DatabaseConfig;

  constructor(config: DatabaseConfig) {
    super();
    this.config = config;
  }

  @Stage(BOOT_STAGES.BOOT_DEPENDENCIES)
  async connect(server: Express) {
    console.log(`Connecting to ${this.config.host}:${this.config.port}...`);
    // await createConnection(this.config);
  }
}

// Usage with environment variables
@Pour(new DatabasePlugin({
  host: process.env.DB_HOST || 'localhost',
  port: parseInt(process.env.DB_PORT || '5432'),
  database: process.env.DB_NAME || 'myapp',
  ssl: process.env.DB_SSL === 'true'
}))
class MyApp extends Boot {}
Pattern 2: Multi-Stage Plugins
export class FullStackPlugin extends Plugin {
  protected name = 'FullStackPlugin';

  @Stage(BOOT_STAGES.BOOT_DEPENDENCIES)
  loadExternalServices(server: Express) {
    console.log('๐Ÿ“ก Loading external APIs...');
  }

  @Stage(BOOT_STAGES.APPLICATION)
  setupCORS(server: Express) {
    console.log('๐ŸŒ Configuring CORS...');
    // server.use(cors());
  }

  @Stage(BOOT_STAGES.CONTROLLERS)
  registerAPIRoutes(server: Express) {
    console.log('๐Ÿ›ฃ๏ธ  Registering API routes...');
    // Register your routes
  }

  @Stage(BOOT_STAGES.APPLICATION_MIDDLEWARES)
  addAuthMiddleware(server: Express) {
    console.log('๐Ÿ”’ Adding authentication...');
    // server.use(passport.authenticate());
  }

  @Stage(BOOT_STAGES.AFTER_APPLICATION_MIDDLEWARES)
  addErrorHandlers(server: Express) {
    console.log('๐Ÿšจ Adding error handlers...');
    // Global error handling
  }
}
Pattern 3: Conditional Logic
export class MonitoringPlugin extends Plugin {
  protected name = 'MonitoringPlugin';
  private enableMetrics: boolean;

  constructor(options: { enableMetrics?: boolean } = {}) {
    super();
    this.enableMetrics = options.enableMetrics ?? true;
  }

  @Stage(BOOT_STAGES.APPLICATION)
  setupMonitoring(server: Express) {
    if (this.enableMetrics) {
      console.log('๐Ÿ“Š Enabling metrics endpoint...');
      // server.get('/metrics', metricsHandler);
    }

    if (process.env.NODE_ENV === 'production') {
      console.log('๐Ÿ” Enabling APM monitoring...');
      // Initialize APM
    }
  }
}
Pattern 4: Publish to npm
// my-awesome-plugin/index.ts
import { Plugin, BOOT_STAGES, Stage } from '@expressive-tea/plugin';
import { Express } from 'express';

export class MyAwesomePlugin extends Plugin {
  protected name = 'MyAwesomePlugin';
  
  @Stage(BOOT_STAGES.APPLICATION)
  setup(server: Express) {
    // Your awesome logic
  }
}

// Publish to npm as @yourname/awesome-plugin
// Others can use it:
// yarn add @yourname/awesome-plugin
// @Pour(new MyAwesomePlugin())

Complete API Reference

Plugin Class (Abstract)

Extend this to create your plugins:

Property Type Default What It Does
name string Class name Unique plugin identifier
priority number 999 Execution order (lower = first)
dependencies string[] [] Required plugin names
Method Returns What It Does
getRegisteredStage(stage) Array Get methods for a boot stage
@Stage Decorator
@Stage(stage: BOOT_STAGES, required?: boolean)
  • stage - When to run this method
  • required - (Optional) Throw error if fails (default: false)
BOOT_STAGES Enum
enum BOOT_STAGES {
  BOOT_DEPENDENCIES = 'Bootstrap Dependencies',
  APPLICATION = 'Bootstrap Application',
  CONTROLLERS = 'Bootstrap Controllers',
  APPLICATION_MIDDLEWARES = 'Bootstrap Application Middlewares',
  ROUTE_MIDDLEWARES = 'Bootstrap Route Middlewares',
  SERVER_SETTINGS = 'Bootstrap Server Settings',
  AFTER_APPLICATION_MIDDLEWARES = 'After Bootstrap Application Middlewares'
}
DependencyNotFound Exception
import { DependencyNotFound } from '@expressive-tea/plugin';

try {
  // Plugin initialization
} catch (error) {
  if (error instanceof DependencyNotFound) {
    console.error(`Missing plugin: ${error.message}`);
  }
}

Development

# Install dependencies
yarn install

# Build
yarn build

# Test
yarn test

# Watch mode
yarn test:watch

# Lint
yarn lint

# Format
yarn format

Project Structure

@expressive-tea/plugin/
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ classes/
โ”‚   โ”‚   โ””โ”€โ”€ Plugin.ts              # ๐ŸŽฏ Abstract Plugin class
โ”‚   โ”œโ”€โ”€ decorators/
โ”‚   โ”‚   โ””โ”€โ”€ stage.ts               # ๐ŸŽจ @Stage decorator
โ”‚   โ”œโ”€โ”€ helpers/
โ”‚   โ”‚   โ”œโ”€โ”€ storage-helper.ts      # ๐Ÿ’พ WeakMap metadata storage
โ”‚   โ”‚   โ””โ”€โ”€ object-helper.ts       # ๐Ÿ› ๏ธ Stage helpers
โ”‚   โ”œโ”€โ”€ libs/
โ”‚   โ”‚   โ””โ”€โ”€ utilities.ts           # โšก Native utilities (no lodash!)
โ”‚   โ”œโ”€โ”€ exceptions/
โ”‚   โ”‚   โ””โ”€โ”€ dependency.ts          # โŒ DependencyNotFound error
โ”‚   โ”œโ”€โ”€ constants.ts               # ๐Ÿ“‹ BOOT_STAGES enum
โ”‚   โ”œโ”€โ”€ __test__/
โ”‚   โ”‚   โ””โ”€โ”€ unit/                  # ๐Ÿงช Comprehensive tests
โ”‚   โ””โ”€โ”€ index.ts                   # ๐Ÿ“ฆ Public API
โ”œโ”€โ”€ dist/                          # ๐Ÿ—๏ธ Compiled output
โ””โ”€โ”€ README.md                      # ๐Ÿ“– You are here!

Real-World Plugin Examples

Security Plugin
import helmet from 'helmet';
import cors from 'cors';
import rateLimit from 'express-rate-limit';

export class SecurityPlugin extends Plugin {
  protected name = 'SecurityPlugin';
  protected priority = 50;

  @Stage(BOOT_STAGES.APPLICATION)
  enableSecurity(server: Express) {
    console.log('๐Ÿ›ก๏ธ  Enabling security features...');
    
    server.use(helmet());
    server.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') }));
    server.use(rateLimit({
      windowMs: 15 * 60 * 1000,
      max: 100
    }));
  }
}
Logging Plugin
import morgan from 'morgan';
import winston from 'winston';

export class LoggingPlugin extends Plugin {
  protected name = 'LoggingPlugin';
  private logger: winston.Logger;

  constructor() {
    super();
    this.logger = winston.createLogger({
      transports: [new winston.transports.Console()]
    });
  }

  @Stage(BOOT_STAGES.APPLICATION)
  setupRequestLogging(server: Express) {
    console.log('๐Ÿ“ Setting up request logging...');
    server.use(morgan('combined'));
  }

  @Stage(BOOT_STAGES.AFTER_APPLICATION_MIDDLEWARES)
  setupErrorLogging(server: Express) {
    server.use((err, req, res, next) => {
      this.logger.error('Error occurred:', err);
      next(err);
    });
  }
}

Contributing

We'd love your plugin ideas and improvements!

  1. Fork it
  2. Create your feature branch: git checkout -b feature/epic-plugin
  3. Add tests (we love tests!)
  4. Run yarn test - green is good!
  5. Run yarn lint - make it pretty
  6. Commit: git commit -m "feat: add epic plugin feature"
  7. Push and PR!

Check our Contributing Guide for the full details!

Changelog

Check CHANGELOG.md for what's cooking in each release!

Upgrading from v0.0.x?

Our Migration Guide makes it painless!

Support

Stuck? We've got your back:

License

Apache-2.0 - Free to use, free to share, free to build amazing things! See LICENSE.

Author

Zero One IT - https://zerooneit.com

Contributors

See all the awesome contributors who helped build this!


Made with and by the Expressive Tea Team
Build plugins. Share plugins. Change the world.

Keywords