npm package: @richardpickett/node-app
Description: Base node app that utilizes logging, async dependencies, and commander
Author: Richard@RichardPickett.com
Latest version: 1.7.5

# Node App

A powerful Node.js application framework that provides built-in support for components, commands, logging, configuration, and more.

## Installation

```bash
npm install @richardpickett/node-app
```

## Quick Start

The easiest way to get started is to copy the example app:

```bash
# Extract the example app
node-app_example-app.sh

# Or manually copy the example-app directory
cp -r node_modules/@richardpickett/node-app/example-app my-app
cd my-app
npm install
```

The example app provides a complete working application with the following structure:

```
my-app/
├── src/
│   ├── commands/          # Command definitions
│   │   ├── index.js      # Command registration
│   │   ├── globals.js    # Global command options
│   │   └── myCommand.js  # Individual commands
│   ├── components/       # Custom components
│   │   ├── index.js     # Component registration
│   │   └── myComponent.js # Individual components
│   ├── lib/             # Application-specific code
│   │   ├── application.js # Application instance
│   │   └── initialize.js  # Initialization logic
│   └── index.js         # Application entry point
├── .env                 # Environment variables
└── package.json
```

## Basic Usage

### Application Entry Point (src/index.js)

```javascript
import application from "./lib/application.js";
import registerComponents from "./components/index.js";
import registerCommands from "./commands/index.js";
import initialize from "./lib/initialize.js";

async function run() {
  // Register components first
  registerComponents(application);

  // Then register commands and run the application
  return registerCommands(application)
    .then(() => initialize())
    .then(() => application.run())
    .then((result) => process.exit(result ? 0 : 1));
}

run();
```

## Components

Components are reusable pieces of functionality that can be loaded on demand. They are registered using `registerComponent()` and loaded using `loadComponents()`.

### Creating a Component (src/components/myComponent.js)

```javascript
class MyComponent {
  constructor(config) {
    this.config = config;
  }

  doSomething() {
    return "Hello from my component!";
  }
}

export default function registerMyComponent(application) {
  return application.registerComponent("myComponent", async () => {
    // Load dependencies first
    return application.loadComponents("config").then(({ config }) => new MyComponent(config));
  });
}
```

### Registering Components (src/components/index.js)

```javascript
import registerMyComponent from "./myComponent.js";

export default function registerComponents(application) {
  registerMyComponent(application);
}
```

### Using Components

```javascript
// Load a single component
application.loadComponents("myComponent").then(({ myComponent }) => {
  console.log(myComponent.doSomething());
});

// Load multiple components
application.loadComponents(["config", "logger"]).then(({ config, logger }) => {
  // Use components here
});

// Alternately, you can load multiple components without the array:
application.loadComponents("config", "logger").then(({ config, logger }) => {
  // Use components here
});

// Load components with error handling, if the last parameter is boolean, it indicates an error should not be thrown if a component isn't registered, but instead return false for that component
application
  .loadComponents("myComponent", true) // true enables quiet fail mode
  .then((components) => {
    if (components.myComponent) {
      console.log(components.myComponent.doSomething());
    }
  });
```

## Commands

Commands are registered using `registerCommand()` and can be executed from the command line.

### Creating a Command (src/commands/myCommand.js)

```javascript
import application from "../lib/application.js";

export default async function registerMyCommand(commander) {
  commander
    .command("my-command")
    .description("Execute my custom command")
    .option("-f, --file <path>", "Path to file")
    .action((options) => {
      return myCommand(options);
    });
}

async function myCommand(options) {
  // Load required components
  return application.loadComponents(["config", "logger"]).then(({ config, logger }) => {
    // Command logic here
    logger.info("Executing my command with options:", options);
    return true;
  });
}
```

### Registering Commands (src/commands/index.js)

```javascript
import registerMyCommand from "./myCommand.js";
import registerGlobals from "./globals.js";

export default async function registerCommands(application) {
  return application.loadComponents("commander").then(({ commander }) => {
    // Register global options first
    registerGlobals(commander);
    // Then register specific commands
    registerMyCommand(commander);
  });
}
```

### Global Command Options (src/commands/globals.js)

```javascript
import { Option } from "@richardpickett/node-app";
import application from "../lib/application.js";

export default async function registerGlobals(commander) {
  return application.loadComponents("config").then(({ config }) => {
    // Set default values
    config.globalA = "A";
    config.globalB = "B";

    // Add global options
    commander.addOption(new Option("-A, --global-a <a>", "global a setting").choices(["a", "b"]).default("a", "a"));
    commander.option("-B, --global-b <b>", "global b setting", "b");

    // Hook into command execution
    commander.hook("preAction", (thisCommand, actionCommand) => {
      loadGlobals(thisCommand, config);
    });
  });
}

function loadGlobals(commander, config) {
  // Update config with command line options
  config.globalA = commander.opts().globalA;
  config.globalB = commander.opts().globalB;
}
```

### Using Commands

```bash
# Run a command
node src/index.js my-command

# Run with options
node src/index.js my-command --file ./data.json

# Run with global options
node src/index.js my-command --global-a b --global-b c

# Run multiple commands
node src/index.js my-command another-command
```

## Built-in Components

### Logger Component

The logger component provides structured logging capabilities:

```javascript
application.loadComponents("logger").then(({ logger }) => {
  // Log messages
  logger.info("Application started");
  logger.error("An error occurred", new Error("Test error"));
  logger.debug("Debug information");
});

// Set log level
process.env.LOG_LEVEL = "debug"; // Options: error, warn, info, debug
```

### Config Component

The config component manages application configuration:

```javascript
application.loadComponents("config").then(({ config }) => {
  // Set configuration
  config.set("app.name", "My App");
  config.set("app.version", "1.0.0");

  // Get configuration
  const appName = config.get("app.name");
  const appVersion = config.get("app.version");
});
```

### Commander Component

The commander component handles command-line argument parsing:

```javascript
application.loadComponents("commander").then(({ commander }) => {
  // Set version
  commander.version("1.0.0");

  // Add global options
  commander.option("-e, --env <environment>", "Set environment", "development");

  // Parse arguments
  commander.parse(process.argv);
});
```

## Initializers

Initializers are functions that run during application startup:

```javascript
// Register an initializer
application.registerInitializer("setup", async () => {
  // Perform setup tasks
  console.log("Setting up application...");
  return true; // Return true to indicate success
});

// Register multiple initializers
application.registerInitializer("validate", async () => {
  // Validate configuration
  return true;
});

application.registerInitializer("connect", async () => {
  // Connect to database
  return true;
});
```

## Error Handling

The framework provides several built-in error classes:

```javascript
import {
  NodeAppBaseError,
  NodeAppDuplicateComponentError,
  NodeAppDuplicateInitializerError,
  NodeAppInitializerFailedError,
  NodeAppComponentNotFoundError,
  NodeAppInvalidComponentStateError,
} from "@richardpickett/node-app";

// Example error handling
application.loadComponents("nonExistentComponent").catch((error) => {
  if (error instanceof NodeAppComponentNotFoundError) {
    console.error("Component not found:", error.message);
  }
});
```

## Configuration Files

### .env

```env
LOG_LEVEL=debug
DB_PASSWORD=secret
API_KEY=abc123
jsonVariables=["A_JSON_ENV_VAR"]
A_JSON_ENV_VAR={"a":1,"b":2}
```

## Best Practices

1. **Component Organization**:

   - Place component definitions in `src/components/`
   - Use index.js for component registration
   - Keep components focused and single-purpose
   - Load dependencies in component registration

2. **Command Organization**:

   - Place command definitions in `src/commands/`
   - Use index.js for command registration
   - Group related commands in separate files
   - Register global options before specific commands
   - Use preAction hooks for global option handling

3. **Configuration**:

   - Use `.env` for sensitive data
   - Set default values in global command options

4. **Error Handling**:

   - Use built-in error classes for consistent error handling
   - Implement proper error recovery in components
   - Log errors with appropriate context
   - Return true/false from commands to indicate success/failure

5. **Initialization**:
   - Register components before commands
   - Register initializers in order of dependency
   - Return true/false to indicate success/failure
   - Handle initialization errors gracefully