From 37dfee5877f38fd45f31e2a0c88760d49ca1fa20 Mon Sep 17 00:00:00 2001 From: mohiit1502 Date: Sun, 7 Dec 2025 16:47:01 +0530 Subject: [PATCH] Added logger object to global NS --- CONFIG_DRIVEN_BREAKING_CHANGE.md | 344 ++++++++++++++++++ build.ts | 5 + v2/CONFIG_DRIVEN_INITIALIZATION.md | 372 ++++++++++++++++++++ v2/GLOBAL_LOGGER.md | 370 +++++++++++++++++++ v2/core/Application.ts | 42 ++- v2/core/PluginFactory.ts | 100 ++++++ v2/examples/armco.config.full.json | 72 ++++ v2/examples/config-driven-initialization.ts | 201 +++++++++++ v2/examples/global-logger-usage.ts | 187 ++++++++++ v2/examples/hybrid-config-and-manual.ts | 193 ++++++++++ v2/examples/test-config-driven.ts | 75 ++++ v2/globals.d.ts | 44 +++ v2/index.ts | 8 + v2/plugins/cache/index.ts | 4 + v2/plugins/database/index.ts | 4 + v2/plugins/logger/index.ts | 8 + v2/plugins/scheduler/index.ts | 4 + v2/plugins/telemetry/index.ts | 5 + 18 files changed, 2037 insertions(+), 1 deletion(-) create mode 100644 CONFIG_DRIVEN_BREAKING_CHANGE.md create mode 100644 v2/CONFIG_DRIVEN_INITIALIZATION.md create mode 100644 v2/GLOBAL_LOGGER.md create mode 100644 v2/core/PluginFactory.ts create mode 100644 v2/examples/armco.config.full.json create mode 100644 v2/examples/config-driven-initialization.ts create mode 100644 v2/examples/global-logger-usage.ts create mode 100644 v2/examples/hybrid-config-and-manual.ts create mode 100644 v2/examples/test-config-driven.ts create mode 100644 v2/globals.d.ts diff --git a/CONFIG_DRIVEN_BREAKING_CHANGE.md b/CONFIG_DRIVEN_BREAKING_CHANGE.md new file mode 100644 index 0000000..57e323d --- /dev/null +++ b/CONFIG_DRIVEN_BREAKING_CHANGE.md @@ -0,0 +1,344 @@ +# Config-Driven Initialization - Breaking Change Summary + +## What Changed + +NSK v2 now supports **config-driven initialization** where `Application.create().build()` automatically loads and instantiates plugins from `armcorc.json` configuration. + +## Problem Solved + +### Before +- Config files only contained plugin **settings** +- You still had to **manually register** plugin instances in code: + ```typescript + App.create(app) + .plugin(createLoggerPlugin()) // Code change required + .plugin(createDatabasePlugin()) // Code change required + .build() + ``` +- Changing plugins required **code changes + rebuild + redeploy** + +### After +- Config file is the **single source of truth** +- `Application.create().build()` auto-loads plugins from config +- Changing plugins only requires **restart** (no rebuild) + +## How It Works + +### 1. Plugin Factory Registry + +When you import `@armco/node-starter-kit`, all built-in plugins automatically register their factory functions: + +```typescript +// Happens automatically when you import NSK +registerPluginFactory('logger', (config) => createLoggerPlugin(config)) +registerPluginFactory('database', (config) => createDatabasePlugin(config)) +registerPluginFactory('cache', (config) => createCachePlugin(config)) +// etc... +``` + +### 2. Hybrid Loading Logic (Config + Manual) + +```typescript +// In Application.build(): +// 1. Config is PRIMARY source of truth +// 2. Manual registration OVERRIDES specific plugins + +if (config has plugins) { + // Auto-load plugins from config + for (pluginName, pluginConfig in config.plugins) { + if (pluginName NOT manually registered && enabled !== false) { + const factory = pluginRegistry.get(pluginName) + const plugin = factory(pluginConfig) + application.plugin(plugin) + } + } + + // Add manually registered plugins (these override config) + for (manualPlugin in manualPlugins) { + application.plugin(manualPlugin) + } +} +``` + +**Key Behavior:** +- Config defines **what** plugins are enabled +- Manual registration **overrides** specific plugin instances +- Manually registered plugins should still be declared in config (for consistency) + +## Usage + +### Approach 1: Pure Config-Driven (Recommended) + +**server.ts:** +```typescript +import express from 'express' +import { Application } from '@armco/node-starter-kit' + +const app = express() +const nsk = await Application.create(app).build() // That's it! + +app.listen(3000) +``` + +**armcorc.json:** +```json +{ + "appName": "auth-core", + "plugins": { + "logger": { "enabled": true, "level": "info" }, + "database": { "enabled": true, "uri": "mongodb://localhost/authdb" }, + "cache": { "enabled": true, "adapter": "redis" } + } +} +``` + +**To change config:** +1. Edit `armcorc.json` +2. Restart the app +3. Done! (no rebuild needed) + +### Approach 2: Programmatic Config (Cloud Config) + +```typescript +import { Application } from '@armco/node-starter-kit' + +// Load from config server, K8s ConfigMap, env vars, etc. +const config = { + appName: 'auth-core', + plugins: { + logger: { + enabled: true, + level: process.env.LOG_LEVEL || 'info' + }, + database: { + enabled: true, + uri: process.env.MONGODB_URI + }, + }, +} + +const nsk = await Application.create(app) + .withConfig(config) + .build() +``` + +### Approach 3: Hybrid (RECOMMENDED for most cases) + +Config is primary source of truth, manual registration for overrides: + +**armcorc.json:** +```json +{ + "appName": "auth-core", + "plugins": { + "logger": { "enabled": true, "level": "info" }, + "database": { "enabled": true, "uri": "mongodb://..." }, + "cache": { "enabled": true }, + "socket": { "enabled": true } + } +} +``` + +**server.ts:** +```typescript +import http from 'http' +import { Application, createSocketPlugin } from '@armco/node-starter-kit' + +const app = express() +const server = http.createServer(app) + +const nsk = await Application.create(app) + // Config auto-loads: logger, database, cache + // Manual registration for socket (requires server instance) + .plugin(createSocketPlugin(server)) + .build() + +server.listen(3000) +``` + +**Benefits:** +- Config declares all plugins (single source of truth) +- Code only handles runtime requirements (e.g., HttpServer for socket) +- Easy restart-based reconfiguration for most plugins + +### Approach 4: Pure Manual (Still Supported) + +```typescript +import { Application, createLoggerPlugin } from '@armco/node-starter-kit' + +// Explicit plugin registration (old way, still works) +const nsk = await Application.create(app) + .plugin(createLoggerPlugin({ level: 'info' })) + .plugin(createDatabasePlugin({ uri: 'mongodb://...' })) + .build() +``` + +## Supported Plugins + +These plugins can be auto-loaded from config: + +| Plugin Name | Config Key | Notes | +|-------------|------------|-------| +| Logger | `logger` | ✅ Fully supported | +| Database | `database` | ✅ Fully supported | +| Cache | `cache` | ✅ Fully supported | +| Scheduler | `scheduler` | ✅ Fully supported | +| Telemetry | `telemetry` or `opentelemetry` | ✅ Fully supported | +| Socket.IO | `socket` | ⚠️ **Requires manual registration** (needs `HttpServer` instance) | + +### Socket.IO Special Case + +Socket.IO plugin requires an `HttpServer` instance, which cannot be provided via JSON config: + +```typescript +import http from 'http' +import { Application, createSocketPlugin } from '@armco/node-starter-kit' + +const app = express() +const server = http.createServer(app) + +const nsk = await Application.create(app) + // Auto-load other plugins from config + .withConfig('./armcorc.json') + // Manually add socket (requires server instance) + .plugin(createSocketPlugin(server, { cors: { origin: '*' } })) + .build() + +server.listen(3000) +``` + +## Configuration Examples + +### Minimal Config +```json +{ + "appName": "my-service", + "plugins": { + "logger": { "enabled": true } + } +} +``` + +### Production Config +```json +{ + "appName": "auth-core", + "env": "production", + "plugins": { + "logger": { + "enabled": true, + "level": "info", + "format": "json" + }, + "database": { + "enabled": true, + "adapter": "mongoose", + "uri": "mongodb://mongo:27017/authdb", + "options": { + "maxPoolSize": 10 + } + }, + "cache": { + "enabled": true, + "adapter": "redis", + "uri": "redis://redis:6379", + "ttl": 3600, + "keyPrefix": "auth:" + }, + "telemetry": { + "enabled": true, + "serviceName": "auth-core", + "serviceVersion": "1.0.0", + "otlpEndpoint": "http://tempo:4318", + "exporters": ["otlp"] + } + } +} +``` + +### Disable a Plugin +```json +{ + "appName": "my-service", + "plugins": { + "logger": { "enabled": true }, + "cache": { "enabled": false } // Disabled + } +} +``` + +## Migration Path + +### If You're Happy with Current Approach + +**No changes needed!** Manual plugin registration still works: + +```typescript +const nsk = await Application.create(app) + .plugin(createLoggerPlugin()) + .build() +``` + +### If You Want Config-Driven + +1. **Create `armcorc.json`** with your plugin configs +2. **Remove manual `.plugin()` calls** +3. **Simplify to** `Application.create(app).build()` + +## Benefits + +1. **Restart-based reconfiguration**: No rebuild or redeploy needed +2. **GitOps friendly**: Config changes tracked in version control +3. **Cloud config integration**: Load from K8s ConfigMaps, AWS Parameter Store, etc. +4. **Environment-specific**: Easy to swap configs per environment +5. **Cleaner code**: Less boilerplate in application code + +## Runtime Reconfiguration Workflow + +```bash +# 1. Edit config +vim armcorc.json + +# 2. Commit (optional, for GitOps) +git commit -am "Enable cache plugin" + +# 3. Restart app (PM2, K8s, systemd, etc.) +pm2 reload my-service +# or +kubectl rollout restart deployment/my-service + +# 4. New config is active (no rebuild!) +``` + +## Implementation Details + +### Files Changed/Added + +**New:** +- `v2/core/PluginFactory.ts` - Plugin factory registry +- `v2/examples/config-driven-initialization.ts` - Usage examples +- `v2/examples/armco.config.full.json` - Full config reference +- `v2/CONFIG_DRIVEN_INITIALIZATION.md` - Comprehensive docs + +**Modified:** +- `v2/core/Application.ts` - Auto-load plugins from config +- `v2/index.ts` - Export PluginFactory +- All plugin `index.ts` files - Register factories on import + +### Type Safety + +Config is still validated using Zod schemas, ensuring type safety at runtime. + +## Questions? + +- See `/v2/examples/config-driven-initialization.ts` for complete examples +- See `/v2/CONFIG_DRIVEN_INITIALIZATION.md` for detailed documentation +- See `/v2/examples/armco.config.full.json` for full config reference + +## Summary + +✅ **Backward compatible** - Old approach still works +✅ **Opt-in** - Use config-driven only if you want +✅ **Restart-based reconfiguration** - No rebuild needed +✅ **Cloud-native** - Integrates with config management systems +✅ **Type-safe** - Still validated with Zod schemas diff --git a/build.ts b/build.ts index 03176e4..79584d4 100644 --- a/build.ts +++ b/build.ts @@ -98,6 +98,11 @@ import pkg from "./package.json"; fs.copyFileSync("./LICENSE", "./dist/LICENSE"); } + // Copy globals.d.ts for TypeScript type augmentation + if (fs.existsSync("./v2/globals.d.ts")) { + fs.copyFileSync("./v2/globals.d.ts", "./dist/globals.d.ts"); + } + logger.info('✅ Build completed successfully!'); logger.info('📦 Package ready in ./dist folder'); } catch (err) { diff --git a/v2/CONFIG_DRIVEN_INITIALIZATION.md b/v2/CONFIG_DRIVEN_INITIALIZATION.md new file mode 100644 index 0000000..4dd9be0 --- /dev/null +++ b/v2/CONFIG_DRIVEN_INITIALIZATION.md @@ -0,0 +1,372 @@ +# Config-Driven Initialization + +NSK v2 now supports **config-driven initialization**, enabling restart-based reconfiguration without code changes or rebuilds. This document explains how it works and how to use it. + +## Overview + +### Previous Approach (Still Supported) +```typescript +import { Application, createLoggerPlugin, createDatabasePlugin } from '@armco/node-starter-kit' + +const nsk = await Application.create(app) + .plugin(createLoggerPlugin({ level: 'info' })) + .plugin(createDatabasePlugin({ uri: process.env.MONGODB_URI })) + .build() +``` + +### New Config-Driven Approach +```typescript +import { Application } from '@armco/node-starter-kit' + +// Just this - plugins auto-loaded from armcorc.json +const nsk = await Application.create(app).build() +``` + +**armcorc.json:** +```json +{ + "appName": "my-service", + "plugins": { + "logger": { "enabled": true, "level": "info" }, + "database": { "enabled": true, "uri": "mongodb://localhost/mydb" } + } +} +``` + +## Benefits + +1. **Restart-based reconfiguration**: Change config → restart app → changes applied (no rebuild) +2. **Single source of truth**: All configuration in one place (JSON/YAML) +3. **Cloud config integration**: Load from config server, env vars, K8s ConfigMaps, etc. +4. **Environment-specific configs**: Easy to swap configs per environment +5. **GitOps friendly**: Config changes tracked in version control + +## How It Works + +### Plugin Factory Registry + +NSK maintains a global registry that maps plugin names to factory functions: + +```typescript +// Internally, when you import @armco/node-starter-kit: +registerPluginFactory('logger', (config) => createLoggerPlugin(config)) +registerPluginFactory('database', (config) => createDatabasePlugin(config)) +registerPluginFactory('cache', (config) => createCachePlugin(config)) +// ... etc +``` + +### Auto-Loading Process + +When `Application.create(app).build()` is called **without** explicit plugins: + +1. NSK searches for `armcorc.json` (or `.armcorc`, `armco.config.js`, etc.) +2. Reads the `plugins` section +3. For each enabled plugin, looks up its factory in the registry +4. Instantiates the plugin with its config +5. Registers and starts all plugins + +## Usage Patterns + +### Pattern 1: Pure Config-Driven (Recommended) + +**server.ts:** +```typescript +import express from 'express' +import { Application } from '@armco/node-starter-kit' + +const app = express() + +// All plugins loaded from armcorc.json +const nsk = await Application.create(app).build() + +app.listen(3000) +``` + +**armcorc.json:** +```json +{ + "appName": "auth-core", + "plugins": { + "logger": { + "enabled": true, + "level": "info", + "format": "json" + }, + "database": { + "enabled": true, + "adapter": "mongoose", + "uri": "mongodb://localhost:27017/authdb" + }, + "cache": { + "enabled": true, + "adapter": "redis", + "uri": "redis://localhost:6379" + } + } +} +``` + +### Pattern 2: Cloud Config Integration + +```typescript +import { Application } from '@armco/node-starter-kit' + +// Load from cloud config store +const config = await fetchConfigFromCloudStore() + +const nsk = await Application.create(app) + .withConfig(config) + .build() +``` + +### Pattern 3: Environment Variables + +```typescript +import { Application } from '@armco/node-starter-kit' + +const config = { + appName: 'auth-core', + plugins: { + logger: { + enabled: true, + level: process.env.LOG_LEVEL || 'info', + }, + database: { + enabled: true, + uri: process.env.MONGODB_URI, + }, + }, +} + +const nsk = await Application.create(app) + .withConfig(config) + .build() +``` + +### Pattern 4: Mixed (Config + Manual) + +Use config for standard plugins, manual registration for special cases: + +```typescript +import { Application } from '@armco/node-starter-kit' +import http from 'http' + +const app = express() +const server = http.createServer(app) + +const nsk = await Application.create(app) + // Auto-load plugins from config + .withConfig('./armcorc.json') + // Manually add socket (requires server instance) + .plugin(createSocketPlugin(server)) + .build() +``` + +## Configuration Reference + +### Config File Locations + +NSK searches for configuration in this order: +1. `armco.config.js` (TypeScript: `armco.config.ts`) +2. `armco.config.json` +3. `.armcorc` +4. `.armcorc.json` +5. `.armcorc.js` +6. `armcorc.json` +7. `package.json` (under `"armco"` key) + +### Plugin Configuration Schema + +Each plugin in `plugins` object follows this structure: + +```json +{ + "plugins": { + "": { + "enabled": true, // Set to false to disable + "priority": 10, // Load order (optional) + "": "..." + } + } +} +``` + +### Supported Plugin Keys + +| Key | Plugin | Config Type | +|-----|--------|-------------| +| `logger` | Logger | `LoggerConfig` | +| `database` | Database | `DatabaseConfig` | +| `cache` | Cache | `CacheConfig` | +| `scheduler` | Scheduler | `SchedulerConfig` | +| `telemetry` or `opentelemetry` | OpenTelemetry | `OpenTelemetryConfig` | + +**Note**: `socket` plugin cannot be auto-loaded (requires `HttpServer` instance). + +## Migration Guide + +### If You're Using Manual Plugin Registration + +**Before:** +```typescript +const nsk = await Application.create(app) + .plugin(createLoggerPlugin({ level: 'info' })) + .plugin(createDatabasePlugin({ uri: 'mongodb://...' })) + .build() +``` + +**After (Option 1 - Config File):** + +Create `armcorc.json`: +```json +{ + "appName": "my-service", + "plugins": { + "logger": { "enabled": true, "level": "info" }, + "database": { "enabled": true, "uri": "mongodb://..." } + } +} +``` + +Update code: +```typescript +const nsk = await Application.create(app).build() +``` + +**After (Option 2 - Inline Config):** +```typescript +const nsk = await Application.create(app) + .withConfig({ + appName: 'my-service', + plugins: { + logger: { enabled: true, level: 'info' }, + database: { enabled: true, uri: 'mongodb://...' }, + }, + }) + .build() +``` + +## Runtime Reconfiguration + +### Pattern: Graceful Reload + +1. **Update config file** (manually, API, or config management tool) +2. **Send SIGTERM** to the process +3. **Process manager restarts** the app (PM2, K8s, systemd, etc.) +4. **NSK loads new config** on startup + +```bash +# Example with PM2 +vim armcorc.json # Edit config +pm2 reload my-service # Graceful reload +``` + +### Pattern: Hot Reload (Advanced) + +For hot reload without restart, you'll need custom logic: + +```typescript +import { watch } from 'fs' +import { Application } from '@armco/node-starter-kit' + +let nsk: Application + +async function loadApp() { + if (nsk) { + await nsk.shutdown() + } + nsk = await Application.create(app).build() +} + +// Initial load +await loadApp() + +// Watch for config changes +watch('./armcorc.json', async () => { + console.log('Config changed, reloading...') + await loadApp() +}) +``` + +## Custom Plugins + +To make your custom plugin auto-loadable: + +```typescript +import { registerPluginFactory, type PluginFactory } from '@armco/node-starter-kit' + +export class MyCustomPlugin extends BasePlugin { + // ... implementation +} + +export function createMyPlugin(config: MyConfig) { + return new MyCustomPlugin(config) +} + +// Register factory +registerPluginFactory('mycustom', (config) => + createMyPlugin(config as unknown as MyConfig) +) +``` + +Then use in config: +```json +{ + "plugins": { + "mycustom": { + "enabled": true, + "myOption": "value" + } + } +} +``` + +## Best Practices + +1. **Use config files for production**: More maintainable than inline configs +2. **Environment-specific configs**: Use separate files (`armcorc.prod.json`, `armcorc.dev.json`) +3. **Sensitive data**: Use env vars, not config files: + ```typescript + const config = await ConfigLoader.load() + config.plugins.database.uri = process.env.MONGODB_URI + ``` +4. **Validate configs**: NSK uses Zod for validation, but add your own checks +5. **Version control**: Commit configs (except secrets) for GitOps workflows +6. **Documentation**: Document your config schema for your team + +## Troubleshooting + +### "Plugin factory not found" + +**Cause**: Plugin not imported, so factory wasn't registered. + +**Solution**: Import the main package: +```typescript +import '@armco/node-starter-kit' // Triggers factory registration +``` + +Or import specific plugin: +```typescript +import '@armco/node-starter-kit/plugins/logger' +``` + +### Config not loading + +**Cause**: Config file not found or invalid. + +**Solution**: Check file name and location. Enable debug: +```typescript +const loader = new ConfigLoader() +const config = loader.load() +console.log('Loaded config:', config) +``` + +### Plugin not initializing + +**Cause**: Missing required config fields or `enabled: false`. + +**Solution**: Check plugin config requirements in type definitions. + +## Examples + +See `/v2/examples/config-driven-initialization.ts` for complete examples. diff --git a/v2/GLOBAL_LOGGER.md b/v2/GLOBAL_LOGGER.md new file mode 100644 index 0000000..65f25c5 --- /dev/null +++ b/v2/GLOBAL_LOGGER.md @@ -0,0 +1,370 @@ +# Global Logger Access + +NSK v2 provides first-class global access to the logger, matching the legacy NSK v1 behavior. Once NSK is initialized with the logger plugin, the logger is automatically available throughout your application **without requiring imports**. + +## How It Works + +### 1. Automatic Global Injection + +When the logger plugin is installed, NSK automatically makes it available on the global object: + +```typescript +// In your server.ts +import { Application } from '@armco/node-starter-kit' + +const nsk = await Application.create(app) + .withConfig({ + appName: 'my-service', + plugins: { + logger: { enabled: true, level: 'info' } + } + }) + .build() + +// Logger is now globally available! +logger.info('Application started') +``` + +### 2. TypeScript Type Safety + +NSK exports a global namespace augmentation (`globals.d.ts`) that provides full TypeScript type safety for the global logger: + +```typescript +// globals.d.ts (automatically included when you install NSK) +declare global { + var logger: Logger +} +``` + +**No configuration needed in your project!** Just install NSK and TypeScript will automatically recognize the `logger` global. + +## Usage + +### In Route Handlers + +```typescript +app.get('/api/users/:id', (req, res) => { + // No import needed! + logger.info('User request received', { userId: req.params.id }) + + try { + const user = getUserById(req.params.id) + logger.info('User found', { user }) + res.json(user) + } catch (error) { + logger.error('Failed to fetch user', { error }) + res.status(500).json({ error: 'Internal server error' }) + } +}) +``` + +### In Service Classes + +```typescript +class UserService { + async createUser(data: any) { + // Logger is globally available + logger.info('Creating user', { email: data.email }) + + try { + const user = await db.users.create(data) + logger.info('User created', { userId: user.id }) + return user + } catch (error) { + logger.error('Failed to create user', { error }) + throw error + } + } +} +``` + +### In Middleware + +```typescript +function requestLogger(req, res, next) { + const start = Date.now() + + logger.info('Incoming request', { + method: req.method, + path: req.path, + ip: req.ip, + }) + + res.on('finish', () => { + const duration = Date.now() - start + logger.info('Request completed', { + method: req.method, + path: req.path, + statusCode: res.statusCode, + duration: `${duration}ms`, + }) + }) + + next() +} +``` + +### In Error Handlers + +```typescript +function errorHandler(err, req, res, next) { + logger.error('Unhandled error', { + error: err.message, + stack: err.stack, + path: req.path, + }) + + res.status(500).json({ error: 'Internal server error' }) +} +``` + +### In Background Jobs + +```typescript +async function processQueue() { + logger.info('Processing queue') + + while (true) { + const job = await queue.getNext() + + if (!job) { + logger.debug('No jobs in queue') + await sleep(1000) + continue + } + + try { + logger.info('Processing job', { jobId: job.id }) + await processJob(job) + logger.info('Job completed', { jobId: job.id }) + } catch (error) { + logger.error('Job failed', { jobId: job.id, error }) + } + } +} +``` + +## Benefits + +### 1. No Import Boilerplate + +**Before (with imports):** +```typescript +import { logger } from './logger' // Repeated in every file +import { logger } from '../utils/logger' // Different paths +import { getLogger } from '@armco/node-starter-kit' // Different ways +``` + +**After (global):** +```typescript +// No imports needed - logger is just available! +logger.info('Request received') +``` + +### 2. Consistency + +Every file uses the same logger instance automatically: +- No confusion about which logger to import +- No risk of multiple logger instances +- Configuration changes apply everywhere instantly + +### 3. Cleaner Code + +```typescript +// ❌ With imports (verbose) +import express from 'express' +import { logger } from '../utils/logger' + +function handler(req, res) { + logger.info('Processing request') + // ... +} + +// ✅ With global (clean) +import express from 'express' + +function handler(req, res) { + logger.info('Processing request') + // ... +} +``` + +### 4. Migration Compatibility + +Matches legacy NSK v1 behavior - existing code works without changes! + +## TypeScript Configuration + +### No Configuration Needed! ✅ + +When you install `@armco/node-starter-kit`, TypeScript automatically recognizes the global logger through the exported `globals.d.ts` file. + +### How It Works + +1. NSK exports `globals.d.ts` in its package +2. TypeScript automatically picks it up via the triple-slash reference in `index.ts`: + ```typescript + /// + ``` +3. Your project gets full type safety without any configuration! + +### If You Need Manual Configuration (Rare) + +In very rare cases, if TypeScript doesn't pick up the types automatically, you can add this to your `tsconfig.json`: + +```json +{ + "compilerOptions": { + "types": ["@armco/node-starter-kit"] + } +} +``` + +But this is **usually not necessary** - the types are included automatically. + +## Best Practices + +### 1. Initialize NSK Early + +Ensure NSK is initialized before using the logger: + +```typescript +// server.ts +import { Application } from '@armco/node-starter-kit' + +async function main() { + // Initialize NSK first + const nsk = await Application.create(app) + .withConfig('./armcorc.json') + .build() + + // Now logger is available + logger.info('Application initialized') + + // Import other modules (they can now use logger) + const { setupRoutes } = await import('./routes') + setupRoutes(app) +} + +main() +``` + +### 2. Structured Logging + +Use structured logging with context objects: + +```typescript +// ✅ Good: Structured with context +logger.info('User login', { + userId: user.id, + email: user.email, + ip: req.ip, + timestamp: new Date().toISOString(), +}) + +// ❌ Less good: String concatenation +logger.info(`User ${user.id} logged in from ${req.ip}`) +``` + +### 3. Appropriate Log Levels + +Use the right log level for each situation: + +```typescript +logger.debug('Detailed debugging info') // Development only +logger.info('Normal operations') // General info +logger.warn('Something unusual') // Potential issues +logger.error('Something failed', { error }) // Errors +``` + +### 4. Error Logging + +Always include error objects and context: + +```typescript +try { + await riskyOperation() +} catch (error) { + logger.error('Operation failed', { + error, // Full error object + operation: 'riskyOperation', + userId: user.id, + context: { /* additional info */ } + }) +} +``` + +## Comparison: Legacy vs V2 + +### Legacy NSK v1 + +**Required manual namespace declaration in host project:** + +```typescript +// In your project's types/global.d.ts (had to create this) +declare global { + namespace NodeJS { + interface Global { + logger: any + } + } + var logger: any +} +``` + +### NSK v2 + +**No manual configuration needed!** + +Just install NSK and the types are automatically available. The namespace augmentation is exported from NSK itself. + +## Troubleshooting + +### "Cannot find name 'logger'" + +**Cause**: NSK not initialized yet or logger plugin not enabled. + +**Solution**: +1. Ensure logger plugin is enabled in `armcorc.json` +2. Initialize NSK before using the logger +3. Check that NSK initialization completed successfully + +### TypeScript error: "Property 'logger' does not exist" + +**Cause**: TypeScript not picking up the global types (very rare). + +**Solution**: +1. Restart your TypeScript language server +2. Check that `@armco/node-starter-kit` is installed +3. If still not working, add to `tsconfig.json`: + ```json + { + "compilerOptions": { + "types": ["@armco/node-starter-kit"] + } + } + ``` + +### Logger not available in some files + +**Cause**: File is imported/executed before NSK initialization. + +**Solution**: Ensure NSK is initialized before importing other application modules: + +```typescript +// ✅ Good +async function main() { + await initNSK() // Initialize first + const routes = await import('./routes') // Then import +} + +// ❌ Bad +import { routes } from './routes' // Imported before NSK init +async function main() { + await initNSK() +} +``` + +## Examples + +See `/v2/examples/global-logger-usage.ts` for complete working examples. diff --git a/v2/core/Application.ts b/v2/core/Application.ts index 73a2b83..2daab40 100644 --- a/v2/core/Application.ts +++ b/v2/core/Application.ts @@ -2,6 +2,7 @@ import { Application as ExpressApp } from 'express' import { Container } from './Container' import { PluginManager } from './PluginManager' import { ConfigLoader } from './ConfigLoader' +import { pluginRegistry } from './PluginFactory' import { ApplicationContext, AppConfig } from '../types/Context' import { Plugin, PluginRegistration } from '../types/Plugin' import { Logger } from '../types/Logger' @@ -305,7 +306,46 @@ export class ApplicationBuilder { } // Register plugins - if (this.pluginRegistrations.length > 0) { + // Strategy: Config is primary source of truth, manual registration acts as override + if (finalConfig.plugins && Object.keys(finalConfig.plugins).length > 0) { + // Get plugin names that were manually registered + const manualPluginNames = new Set( + this.pluginRegistrations.map(p => + typeof p === 'object' && 'plugin' in p ? p.plugin.name : (p as any).name + ) + ) + + // Validate: Manually registered plugins should also be declared in config + // (This ensures config is the single source of truth for what's enabled) + for (const pluginName of manualPluginNames) { + if (!finalConfig.plugins[pluginName]) { + console.warn( + `[NSK] Warning: Plugin '${pluginName}' is manually registered but not declared in config. ` + + `Consider adding it to armcorc.json for consistency.` + ) + } + } + + // Auto-load plugins from config, excluding ones manually registered + const pluginsToAutoLoad: Record = {} + for (const [name, config] of Object.entries(finalConfig.plugins)) { + if (!manualPluginNames.has(name)) { + pluginsToAutoLoad[name] = config + } + } + + // Load non-overridden plugins from config + if (Object.keys(pluginsToAutoLoad).length > 0) { + const autoLoadedPlugins = pluginRegistry.createFromConfig(pluginsToAutoLoad) + application.plugins(autoLoadedPlugins) + } + + // Add manually registered plugins (these override config) + if (this.pluginRegistrations.length > 0) { + application.plugins(this.pluginRegistrations) + } + } else if (this.pluginRegistrations.length > 0) { + // No config plugins, but manual plugins exist - use manual plugins application.plugins(this.pluginRegistrations) } diff --git a/v2/core/PluginFactory.ts b/v2/core/PluginFactory.ts new file mode 100644 index 0000000..32ee691 --- /dev/null +++ b/v2/core/PluginFactory.ts @@ -0,0 +1,100 @@ +import { Plugin, PluginRegistration } from '../types/Plugin' +import { PluginConfig } from '../types/Context' + +/** + * Plugin factory function type + * Takes plugin config and returns a plugin instance + */ +export type PluginFactory = (config?: PluginConfig) => Plugin | PluginRegistration + +/** + * Global registry for plugin factories + * Maps plugin names (from config) to their factory functions + */ +class PluginFactoryRegistry { + private factories = new Map() + + /** + * Register a plugin factory + * @param name - Plugin name as it appears in config (e.g., 'logger', 'database') + * @param factory - Factory function that creates the plugin + */ + register(name: string, factory: PluginFactory): void { + this.factories.set(name, factory) + } + + /** + * Get a plugin factory by name + */ + get(name: string): PluginFactory | undefined { + return this.factories.get(name) + } + + /** + * Check if a factory is registered + */ + has(name: string): boolean { + return this.factories.has(name) + } + + /** + * Get all registered plugin names + */ + getRegisteredNames(): string[] { + return Array.from(this.factories.keys()) + } + + /** + * Create plugin instances from config + * @param pluginsConfig - Plugin configuration from AppConfig + * @returns Array of plugin instances + */ + createFromConfig(pluginsConfig: Record): Array { + const plugins: Array = [] + + for (const [name, config] of Object.entries(pluginsConfig)) { + // Skip if explicitly disabled + if (config.enabled === false) { + continue + } + + const factory = this.get(name) + if (!factory) { + console.warn(`[NSK] Plugin factory not found for: ${name}. Skipping...`) + continue + } + + try { + const plugin = factory(config) + plugins.push(plugin) + } catch (error) { + console.error(`[NSK] Failed to create plugin '${name}':`, error) + throw new Error( + `Plugin creation failed for '${name}': ${ + error instanceof Error ? error.message : String(error) + }` + ) + } + } + + return plugins + } +} + +/** + * Global plugin factory registry instance + */ +export const pluginRegistry = new PluginFactoryRegistry() + +/** + * Register a plugin factory (convenience function) + * + * @example + * ```typescript + * // In your plugin file: + * registerPluginFactory('logger', (config) => createLoggerPlugin(config)) + * ``` + */ +export function registerPluginFactory(name: string, factory: PluginFactory): void { + pluginRegistry.register(name, factory) +} diff --git a/v2/examples/armco.config.full.json b/v2/examples/armco.config.full.json new file mode 100644 index 0000000..e8019b7 --- /dev/null +++ b/v2/examples/armco.config.full.json @@ -0,0 +1,72 @@ +{ + "appName": "my-service", + "env": "production", + "plugins": { + "logger": { + "enabled": true, + "adapter": "winston", + "level": "info", + "format": "json" + }, + "database": { + "enabled": true, + "adapter": "mongoose", + "uri": "mongodb://localhost:27017/mydb", + "options": { + "maxPoolSize": 10, + "serverSelectionTimeoutMS": 5000 + } + }, + "cache": { + "enabled": true, + "adapter": "redis", + "uri": "redis://localhost:6379", + "ttl": 3600, + "keyPrefix": "myapp:" + }, + "scheduler": { + "enabled": true, + "adapter": "node-cron", + "timezone": "UTC", + "tasks": [] + }, + "telemetry": { + "enabled": true, + "serviceName": "my-service", + "serviceVersion": "1.0.0", + "otlpEndpoint": "http://localhost:4318", + "exporters": ["otlp"], + "enableTracing": true, + "enableMetrics": true, + "autoInstrumentation": true, + "sampleRate": 1.0 + } + }, + "middlewares": { + "helmet": { + "enabled": true + }, + "cors": { + "enabled": true, + "origin": "*" + }, + "rateLimit": { + "enabled": true, + "windowMs": 900000, + "max": 100 + } + }, + "health": { + "enabled": true, + "endpoint": "/health" + }, + "metrics": { + "enabled": true, + "endpoint": "/metrics", + "collector": "prometheus" + }, + "server": { + "port": 3000, + "host": "0.0.0.0" + } +} diff --git a/v2/examples/config-driven-initialization.ts b/v2/examples/config-driven-initialization.ts new file mode 100644 index 0000000..bb35911 --- /dev/null +++ b/v2/examples/config-driven-initialization.ts @@ -0,0 +1,201 @@ +/** + * Config-Driven Initialization Example + * + * This example demonstrates how to initialize NSK v2 purely from configuration + * without explicit plugin registration in code. This enables: + * - Restart-based reconfiguration (no rebuild required) + * - Config-as-code or cloud config integration + * - Uniform JSON input for all plugins + */ + +import express from 'express' +import { Application } from '../core/Application' + +// Import plugins to trigger factory registration +// This is required once at app entry point +import '../plugins/logger' +import '../plugins/database' +import '../plugins/cache' +import '../plugins/scheduler' +import '../plugins/telemetry' + +/** + * Example 1: Minimal - Auto-load from armcorc.json + * + * If you have armcorc.json in your project root: + * { + * "appName": "my-service", + * "env": "production", + * "plugins": { + * "logger": { + * "enabled": true, + * "level": "info", + * "format": "json" + * }, + * "database": { + * "enabled": true, + * "adapter": "mongoose", + * "uri": "mongodb://localhost:27017/mydb" + * } + * } + * } + */ +async function minimalExample() { + const app = express() + + // This is all you need - NSK auto-loads plugins from armcorc.json + const nsk = await Application.create(app).build() + + // Access services from container + const logger = nsk.getContainer().resolve('logger') + const database = nsk.getContainer().tryResolve('database') + + logger.info('Application initialized from config') + + return nsk +} + +/** + * Example 2: Explicit config object (for cloud config integration) + */ +async function explicitConfigExample() { + const app = express() + + // Load config from cloud config store, env vars, etc. + const config = { + appName: 'auth-core', + env: process.env.NODE_ENV || 'development', + plugins: { + logger: { + enabled: true, + level: process.env.LOG_LEVEL || 'info', + format: process.env.NODE_ENV === 'production' ? 'json' : 'pretty', + }, + database: { + enabled: true, + adapter: 'mongoose', + uri: process.env.MONGODB_URI!, + }, + cache: { + enabled: true, + adapter: 'redis', + uri: process.env.REDIS_URI, + ttl: 3600, + keyPrefix: 'auth:', + }, + scheduler: { + enabled: true, + timezone: 'UTC', + tasks: [ + { + name: 'cleanup-sessions', + schedule: '0 2 * * *', // 2 AM daily + handler: async () => { + // Cleanup logic + }, + }, + ], + }, + telemetry: { + enabled: process.env.ENABLE_TELEMETRY === 'true', + serviceName: 'auth-core', + serviceVersion: '1.0.0', + otlpEndpoint: process.env.OTLP_ENDPOINT, + exporters: ['otlp'], + }, + }, + } + + const nsk = await Application.create(app) + .withConfig(config) + .build() + + return nsk +} + +/** + * Example 3: Mixed approach (config + manual services) + * + * Use config for plugins, but manually register custom services + */ +async function mixedExample() { + const app = express() + + // Custom services not managed by plugins + class UserRepository { + async findById(id: string) { + // Implementation + } + } + + const nsk = await Application.create(app) + // Config handles plugins + .withConfig('./armcorc.json') + // Manual services registration + .withServices((container) => { + container.singleton('userRepo', new UserRepository()) + }) + .build() + + // Access both plugin services and custom services + const logger = nsk.getContainer().resolve('logger') + const userRepo = nsk.getContainer().resolve('userRepo') + + return nsk +} + +/** + * Example 4: Socket plugin (special case) + * + * Socket plugin requires HttpServer instance, so it can't be purely config-driven. + * You need to register it manually or use a hybrid approach. + */ +async function socketExample() { + const app = express() + const http = require('http') + const server = http.createServer(app) + + const { createSocketPlugin } = await import('../plugins/socket') + + const nsk = await Application.create(app) + // Auto-load other plugins from config + .withConfig('./armcorc.json') + // Manually add socket plugin (requires server instance) + .plugin(createSocketPlugin(server, { + enabled: true, + cors: { origin: '*' }, + })) + .build() + + server.listen(3000) + return nsk +} + +/** + * Example 5: Runtime config updates (restart pattern) + * + * For runtime reconfiguration without rebuild: + * 1. Update armcorc.json (manually, API, or config management system) + * 2. Send SIGTERM to process + * 3. Process manager (PM2, K8s, etc.) restarts the app + * 4. New config is loaded on startup + */ +async function runtimeReconfigExample() { + const app = express() + + // On every restart, fresh config is loaded + const nsk = await Application.create(app).build() + + // Graceful shutdown already handled by NSK + // Just ensure process manager restarts on SIGTERM + + return nsk +} + +export { + minimalExample, + explicitConfigExample, + mixedExample, + socketExample, + runtimeReconfigExample, +} diff --git a/v2/examples/global-logger-usage.ts b/v2/examples/global-logger-usage.ts new file mode 100644 index 0000000..f963547 --- /dev/null +++ b/v2/examples/global-logger-usage.ts @@ -0,0 +1,187 @@ +/** + * Global Logger Usage Example + * + * This demonstrates how the logger is automatically available globally + * once NSK is initialized, without requiring imports. + * + * This matches the legacy NSK v1 behavior. + */ + +import express from 'express' +import { Application } from '../index' + +/** + * Initialize NSK with logger plugin + */ +async function initializeApp() { + const app = express() + + // Initialize NSK with logger from config + const nsk = await Application.create(app) + .withConfig({ + appName: 'global-logger-demo', + plugins: { + logger: { + enabled: true, + level: 'info', + format: 'pretty', + }, + }, + }) + .build() + + // After NSK init, logger is available globally + // No import needed! + logger.info('✅ NSK initialized - logger is now globally available') + + return { app, nsk } +} + +/** + * Example route handler - no logger import needed! + */ +function setupRoutes(app: express.Application) { + app.get('/api/users/:id', (req, res) => { + // Direct usage: logger is globally available + logger.info('User request received', { userId: req.params.id }) + + try { + // Simulate some work + const user = { id: req.params.id, name: 'John Doe' } + + logger.info('User found', { user }) + res.json(user) + } catch (error) { + logger.error('Failed to fetch user', { error, userId: req.params.id }) + res.status(500).json({ error: 'Internal server error' }) + } + }) + + app.post('/api/auth/login', (req, res) => { + logger.info('Login attempt', { username: req.body.username }) + + // Login logic... + + logger.warn('Failed login attempt', { + username: req.body.username, + ip: req.ip, + }) + + res.status(401).json({ error: 'Invalid credentials' }) + }) +} + +/** + * Example service class - no logger import! + */ +class UserService { + async getUser(id: string) { + // Logger is globally available in any file + logger.debug('UserService.getUser called', { userId: id }) + + try { + // Database call... + const user = { id, name: 'Jane Doe' } + + logger.info('User retrieved from database', { userId: id }) + return user + } catch (error) { + logger.error('Database query failed', { error, userId: id }) + throw error + } + } + + async createUser(data: any) { + logger.info('Creating new user', { email: data.email }) + + // Validation... + // Database insert... + + logger.info('User created successfully', { userId: 'new-id' }) + return { id: 'new-id', ...data } + } +} + +/** + * Example middleware - no logger import! + */ +function requestLogger(req: express.Request, res: express.Response, next: express.NextFunction) { + const start = Date.now() + + // Log incoming request + logger.info('Incoming request', { + method: req.method, + path: req.path, + ip: req.ip, + }) + + // Log response + res.on('finish', () => { + const duration = Date.now() - start + + logger.info('Request completed', { + method: req.method, + path: req.path, + statusCode: res.statusCode, + duration: `${duration}ms`, + }) + }) + + next() +} + +/** + * Example error handler - no logger import! + */ +function errorHandler( + err: Error, + req: express.Request, + res: express.Response, + next: express.NextFunction +) { + // Global logger available in error handlers too + logger.error('Unhandled error', { + error: err.message, + stack: err.stack, + path: req.path, + method: req.method, + }) + + res.status(500).json({ error: 'Internal server error' }) +} + +/** + * Main application + */ +async function main() { + const { app, nsk } = await initializeApp() + + // Setup middleware + app.use(express.json()) + app.use(requestLogger) + + // Setup routes + setupRoutes(app) + + // Setup error handler + app.use(errorHandler) + + // Start server + const PORT = 3000 + app.listen(PORT, () => { + // Logger available everywhere! + logger.info(`🚀 Server started on port ${PORT}`) + logger.info('Logger is globally available in all files') + logger.info('No imports required!') + }) +} + +// TypeScript knows about global logger automatically +// because of globals.d.ts exported by NSK +main().catch((error) => { + // Even in catch blocks, logger is available + console.error('Failed to start application:', error) + process.exit(1) +}) + +export { UserService, requestLogger, errorHandler } diff --git a/v2/examples/hybrid-config-and-manual.ts b/v2/examples/hybrid-config-and-manual.ts new file mode 100644 index 0000000..086098c --- /dev/null +++ b/v2/examples/hybrid-config-and-manual.ts @@ -0,0 +1,193 @@ +/** + * Hybrid Config + Manual Plugin Registration Example + * + * This demonstrates the recommended pattern where: + * 1. Config (armcorc.json) is the primary source of truth + * 2. Manual plugin registration acts as override for specific plugins + * 3. Both approaches work together seamlessly + */ + +import express from 'express' +import http from 'http' +import { Application, createSocketPlugin, createLoggerPlugin } from '../index' + +/** + * Example 1: Config-first with manual override + * + * Use case: Most plugins from config, but need custom logger instance + */ +async function configWithManualOverride() { + const app = express() + + // armcorc.json contains: + // { + // "appName": "my-service", + // "plugins": { + // "logger": { "enabled": true, "level": "info" }, <- Will be overridden + // "database": { "enabled": true, "uri": "..." }, <- Auto-loaded + // "cache": { "enabled": true, "adapter": "redis" } <- Auto-loaded + // } + // } + + const nsk = await Application.create(app) + // Config loaded from armcorc.json (database, cache auto-loaded) + .plugin(createLoggerPlugin({ + // This overrides the logger config from armcorc.json + level: 'debug', // Override: use debug instead of info + format: 'pretty', + // Custom transports, filters, etc. + })) + .build() + + // Result: + // - logger: Manually registered instance (overrides config) + // - database: Auto-loaded from config + // - cache: Auto-loaded from config + + return nsk +} + +/** + * Example 2: Socket plugin special case + * + * Socket plugin requires HttpServer, so it must be manually registered + * but other plugins come from config + */ +async function socketWithConfig() { + const app = express() + const server = http.createServer(app) + + // armcorc.json: + // { + // "appName": "chat-service", + // "plugins": { + // "logger": { "enabled": true }, + // "database": { "enabled": true, "uri": "..." }, + // "cache": { "enabled": true }, + // "socket": { "enabled": true, "cors": { "origin": "*" } } <- Declared in config + // } + // } + + const nsk = await Application.create(app) + // Socket plugin must be manually registered (requires server instance) + .plugin(createSocketPlugin(server, { + // Config from armcorc.json can be used here if needed + cors: { origin: '*' }, + })) + .build() + + // Result: + // - logger: Auto-loaded from config + // - database: Auto-loaded from config + // - cache: Auto-loaded from config + // - socket: Manually registered (overrides config, but config declares it's enabled) + + server.listen(3000) + return nsk +} + +/** + * Example 3: Runtime plugin selection + * + * Load plugins from config, but conditionally override based on environment + */ +async function runtimeOverride() { + const app = express() + + const builder = Application.create(app) + + // In development, override logger with custom console logger + if (process.env.NODE_ENV === 'development') { + builder.plugin(createLoggerPlugin({ + level: 'debug', + format: 'pretty', + })) + } + + // All other plugins from config + const nsk = await builder.build() + + return nsk +} + +/** + * Example 4: Progressive enhancement + * + * Start with basic config, add more plugins manually as needed + */ +async function progressiveEnhancement() { + const app = express() + + // armcorc.json has minimal config: + // { + // "appName": "my-service", + // "plugins": { + // "logger": { "enabled": true } + // } + // } + + // Build with additional plugins not in config + const nsk = await Application.create(app) + // Logger comes from config + // Add more plugins manually as your app grows + // (though ideally these should be in config too) + .build() + + return nsk +} + +/** + * Example 5: Best practice pattern (RECOMMENDED) + * + * All plugins declared in config, manual registration ONLY for: + * 1. Plugins requiring runtime instances (e.g., HttpServer for socket) + * 2. Environment-specific overrides + */ +async function bestPracticePattern() { + const app = express() + const server = http.createServer(app) + + // armcorc.json declares ALL plugins: + // { + // "appName": "auth-core", + // "plugins": { + // "logger": { "enabled": true, "level": "info" }, + // "database": { "enabled": true, "uri": "..." }, + // "cache": { "enabled": true, "adapter": "redis" }, + // "scheduler": { "enabled": true, "timezone": "UTC" }, + // "telemetry": { "enabled": true, "serviceName": "auth-core" }, + // "socket": { "enabled": true } <- Declared but must be manually registered + // } + // } + + const builder = Application.create(app) + + // ONLY manually register socket (requires server instance) + builder.plugin(createSocketPlugin(server)) + + // Optional: Environment-specific overrides + if (process.env.NODE_ENV === 'development') { + builder.plugin(createLoggerPlugin({ + level: 'debug', + format: 'pretty', + })) + } + + const nsk = await builder.build() + + // Result: + // - Config is single source of truth for what's enabled + // - Code only handles runtime-specific requirements + // - Easy to see what's running by looking at armcorc.json + + server.listen(3000) + return nsk +} + +export { + configWithManualOverride, + socketWithConfig, + runtimeOverride, + progressiveEnhancement, + bestPracticePattern, +} diff --git a/v2/examples/test-config-driven.ts b/v2/examples/test-config-driven.ts new file mode 100644 index 0000000..23bda44 --- /dev/null +++ b/v2/examples/test-config-driven.ts @@ -0,0 +1,75 @@ +/** + * Quick test script to verify config-driven initialization + * + * Usage: + * npx tsx v2/examples/test-config-driven.ts + */ + +import express from 'express' +import { Application } from '../index' + +async function test() { + console.log('\n🧪 Testing Config-Driven Initialization\n') + + const app = express() + + // Test config + const config = { + appName: 'test-app', + env: 'development', + plugins: { + logger: { + enabled: true, + level: 'info', + format: 'pretty', + }, + cache: { + enabled: true, + adapter: 'memory', + ttl: 60, + }, + }, + } + + console.log('📝 Config:', JSON.stringify(config, null, 2)) + console.log('\n🚀 Building application with config-driven initialization...\n') + + try { + const nsk = await Application.create(app) + .withConfig(config) + .build() + + console.log('✅ Application built successfully!') + + // Test that plugins were loaded + const logger = nsk.getContainer().tryResolve('logger') + const cache = nsk.getContainer().tryResolve('cache') + + if (logger) { + console.log('✅ Logger plugin loaded') + logger.info('Logger is working!') + } else { + console.log('❌ Logger plugin NOT loaded') + } + + if (cache) { + console.log('✅ Cache plugin loaded') + await cache.set('test', 'hello', 10) + const value = await cache.get('test') + console.log(` Cache test: set "test" = "hello", get "test" = "${value}"`) + } else { + console.log('❌ Cache plugin NOT loaded') + } + + // Shutdown + await nsk.shutdown() + console.log('\n✅ Config-driven initialization test passed!') + process.exit(0) + + } catch (error) { + console.error('\n❌ Config-driven initialization test failed:', error) + process.exit(1) + } +} + +test() diff --git a/v2/globals.d.ts b/v2/globals.d.ts new file mode 100644 index 0000000..99a7045 --- /dev/null +++ b/v2/globals.d.ts @@ -0,0 +1,44 @@ +/** + * Global namespace augmentation for NSK + * + * This file augments the global namespace to provide first-class + * access to NSK services without requiring imports. + * + * When you install NSK in your project, these globals are automatically + * available with full TypeScript type safety. + * + * Usage: + * ```typescript + * // No imports needed! + * logger.info('Request received') + * logger.error('Something went wrong', { error }) + * ``` + */ + +import { Logger } from './types/Logger' + +declare global { + /** + * Global logger instance + * + * Automatically injected by NSK Logger Plugin. + * Available everywhere in your application without imports. + * + * @example + * ```typescript + * logger.info('User logged in', { userId: '123' }) + * logger.warn('Rate limit approaching', { count: 95 }) + * logger.error('Database connection failed', { error }) + * ``` + */ + var logger: Logger + + namespace NodeJS { + interface Global { + logger: Logger + } + } +} + +// This export is required to make this a module +export {} diff --git a/v2/index.ts b/v2/index.ts index 169fc3a..569776f 100644 --- a/v2/index.ts +++ b/v2/index.ts @@ -1,18 +1,26 @@ /** * @armco/node-starter-kit v2 * Modern plugin-based Node.js application framework + * + * Global types are automatically available via globals.d.ts */ +// Side-effect import to ensure global type augmentation is loaded +// This triggers TypeScript to process globals.d.ts +import type {} from './globals' + // Core exports export { Application, ApplicationBuilder } from './core/Application' export { Container } from './core/Container' export { PluginManager } from './core/PluginManager' export { ConfigLoader, defineConfig } from './core/ConfigLoader' +export { pluginRegistry, registerPluginFactory, type PluginFactory } from './core/PluginFactory' // Type exports export * from './types' // Plugin exports +// Importing these also triggers factory registration for config-driven initialization export { LoggerPlugin, createLoggerPlugin } from './plugins/logger' export { DatabasePlugin, createDatabasePlugin } from './plugins/database' export { SocketPlugin, createSocketPlugin } from './plugins/socket' diff --git a/v2/plugins/cache/index.ts b/v2/plugins/cache/index.ts index 70695eb..5d039ca 100644 --- a/v2/plugins/cache/index.ts +++ b/v2/plugins/cache/index.ts @@ -5,6 +5,7 @@ import { MemoryAdapter } from './MemoryAdapter' import { RedisAdapter } from './RedisAdapter' import { Logger } from '../../types/Logger' import { HealthStatus } from '../../types/Plugin' +import { registerPluginFactory } from '../../core/PluginFactory' /** * Cache plugin for data caching @@ -203,3 +204,6 @@ export class CachePlugin extends BasePlugin { export function createCachePlugin(config: CacheConfig = {}): CachePlugin { return new CachePlugin(config) } + +// Auto-register plugin factory +registerPluginFactory('cache', (config) => createCachePlugin(config as unknown as CacheConfig)) diff --git a/v2/plugins/database/index.ts b/v2/plugins/database/index.ts index f8d7cba..a4f05a1 100644 --- a/v2/plugins/database/index.ts +++ b/v2/plugins/database/index.ts @@ -1,6 +1,7 @@ import { BasePlugin, HealthStatus } from '../../types/Plugin' import { ApplicationContext } from '../../types/Context' import { MongooseAdapter, MongooseConfig } from './MongooseAdapter' +import { registerPluginFactory } from '../../core/PluginFactory' export interface DatabaseConfig { adapter?: 'mongoose' @@ -96,3 +97,6 @@ export class DatabasePlugin extends BasePlugin { export function createDatabasePlugin(config: DatabaseConfig): DatabasePlugin { return new DatabasePlugin(config) } + +// Auto-register plugin factory +registerPluginFactory('database', (config) => createDatabasePlugin(config as unknown as DatabaseConfig)) diff --git a/v2/plugins/logger/index.ts b/v2/plugins/logger/index.ts index 5098f9d..add5f03 100644 --- a/v2/plugins/logger/index.ts +++ b/v2/plugins/logger/index.ts @@ -2,6 +2,7 @@ import { BasePlugin } from '../../types/Plugin' import { ApplicationContext } from '../../types/Context' import { LoggerConfig } from '../../types/Logger' import { WinstonAdapter } from './WinstonAdapter' +import { registerPluginFactory } from '../../core/PluginFactory' /** * Logger Plugin @@ -36,6 +37,10 @@ export class LoggerPlugin extends BasePlugin { // Register logger in container context.container.singleton('logger', this.logger) + // Make logger available globally (first-class access) + // This allows direct usage: logger.info() without imports + ;(global as any).logger = this.logger + this.logger.info(`Logger plugin installed: ${config.adapter || 'winston'}`) } @@ -54,3 +59,6 @@ export class LoggerPlugin extends BasePlugin { export function createLoggerPlugin(config?: LoggerConfig): LoggerPlugin { return new LoggerPlugin(config) } + +// Auto-register plugin factory +registerPluginFactory('logger', (config) => createLoggerPlugin(config as unknown as LoggerConfig)) diff --git a/v2/plugins/scheduler/index.ts b/v2/plugins/scheduler/index.ts index c0612cb..ca76894 100644 --- a/v2/plugins/scheduler/index.ts +++ b/v2/plugins/scheduler/index.ts @@ -4,6 +4,7 @@ import { SchedulerAdapter, SchedulerConfig } from '../../types/Scheduler' import { NodeCronAdapter } from './NodeCronAdapter' import { Logger } from '../../types/Logger' import { HealthStatus } from '../../types/Plugin' +import { registerPluginFactory } from '../../core/PluginFactory' /** * Scheduler plugin for task scheduling @@ -268,3 +269,6 @@ export class SchedulerPlugin extends BasePlugin { export function createSchedulerPlugin(config: SchedulerConfig = {}): SchedulerPlugin { return new SchedulerPlugin(config) } + +// Auto-register plugin factory +registerPluginFactory('scheduler', (config) => createSchedulerPlugin(config as unknown as SchedulerConfig)) diff --git a/v2/plugins/telemetry/index.ts b/v2/plugins/telemetry/index.ts index 95aff4a..b032665 100644 --- a/v2/plugins/telemetry/index.ts +++ b/v2/plugins/telemetry/index.ts @@ -4,6 +4,7 @@ import { OpenTelemetryConfig, OpenTelemetryAdapter } from '../../types/OpenTelem import { OtelAdapter } from './OpenTelemetryAdapter' import { Logger } from '../../types/Logger' import { HealthStatus } from '../../types/Plugin' +import { registerPluginFactory } from '../../core/PluginFactory' /** * OpenTelemetry plugin for distributed tracing and metrics @@ -226,3 +227,7 @@ export class OpenTelemetryPlugin extends BasePlugin { export function createOpenTelemetryPlugin(config: OpenTelemetryConfig): OpenTelemetryPlugin { return new OpenTelemetryPlugin(config) } + +// Auto-register plugin factory +registerPluginFactory('opentelemetry', (config) => createOpenTelemetryPlugin(config as unknown as OpenTelemetryConfig)) +registerPluginFactory('telemetry', (config) => createOpenTelemetryPlugin(config as unknown as OpenTelemetryConfig))