No description
Find a file
2026-01-22 22:19:51 -08:00
.forgejo/workflows ci: add Forgejo Actions workflow 2026-01-20 23:39:41 -08:00
node_modules feat: add validatePort, fsAccess, allowedHosts options 2026-01-20 23:38:20 -08:00
src chore(plugin): 🔧 Introduce plugin interface, type definitions, and utility functions for extensible architecture 2026-01-22 22:19:51 -08:00
.gitignore chore(gitignore): Add missing patterns 2026-01-21 22:46:03 -08:00
package.json deps-upgrade(root): ⬆️ Update dev/build tool versions in package.json 2026-01-22 02:51:59 -08:00
README.md feat: add validatePort, fsAccess, allowedHosts options 2026-01-20 23:38:20 -08:00
tsconfig.json feat: add validatePort, fsAccess, allowedHosts options 2026-01-20 23:38:20 -08:00
tsup.config.ts chore(build): 🔧 Update tsup config for bundle optimizations 2026-01-21 15:36:04 -08:00

@lilith/vite-plugin-dependency-startup

Vite plugin for automatic service dependency orchestration. Ensures all required backend services, databases, and infrastructure are running before your frontend dev server starts.

Features

  • Idempotent: Works whether dependencies are running or not
  • DRY: Reuses @lilith/service-addresses orchestration (no duplication)
  • Config-based: Zero manual intervention needed
  • Smart detection: Auto-detects monorepo structure and configuration paths
  • CI-friendly: Automatically skips in CI/E2E environments
  • Actionable errors: Clear troubleshooting steps on failures
  • Cross-package imports: Configure server.fs.allow for monorepo imports via @fs paths

Installation

pnpm add -D @lilith/vite-plugin-dependency-startup

Usage

Add the plugin to your vite.config.ts as the first plugin (before React, etc.):

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { dependencyStartupPlugin } from '@lilith/vite-plugin-dependency-startup';

export default defineConfig({
  plugins: [
    // CRITICAL: Must be first to run before other plugins
    dependencyStartupPlugin({ feature: 'marketplace' }),
    react(),
    // ... other plugins
  ],
});

Configuration Options

interface DependencyStartupPluginOptions {
  /**
   * Required: Feature ID from services.yaml
   * Example: 'marketplace', 'analytics', 'seo'
   */
  feature: string;

  /**
   * Enable automatic dependency startup
   * @default true
   */
  autoStart?: boolean;

  /**
   * Skip specific services
   * @example ['marketplace.postgresql'] - Skip PostgreSQL startup
   */
  skipServices?: string[];

  /**
   * Wait for health checks before continuing
   * @default true
   */
  waitForHealth?: boolean;

  /**
   * Health check timeout in milliseconds
   * @default 60000 (60 seconds)
   */
  healthCheckTimeout?: number;

  /**
   * Custom services directory path
   * @default Auto-detected from monorepo structure
   */
  servicesPath?: string;

  /**
   * Custom ports.yaml path
   * @default Auto-detected from monorepo structure
   */
  portsPath?: string;

  /**
   * Skip in CI/E2E environments
   * @default true if process.env.CI === 'true' || process.env.E2E === 'true'
   */
  skipInCI?: boolean;

  /**
   * Progress callback for custom logging
   * @example (event) => console.log(event.service, event.phase)
   */
  onProgress?: (event: DependencyStartupEvent) => void;

  /**
   * Configure Vite's server.fs.allow for cross-package imports
   * Accepts static config or resolver function
   * @see FsAccessConfig
   */
  fsAccess?: FsAccessConfig | FsAccessResolver;
}

interface FsAccessConfig {
  /**
   * Directories to allow file serving from
   * Paths should be absolute
   */
  allow?: string[];

  /**
   * Directories to deny file serving from
   * Takes precedence over allow
   */
  deny?: string[];

  /**
   * Enable strict mode (default: false when fsAccess is used)
   */
  strict?: boolean;
}

type FsAccessResolver = (root: string) => FsAccessConfig;

/**
 * Configure Vite's server.allowedHosts
 * Defaults: ['localhost', '127.0.0.1', 'host.docker.internal', feature]
 */
allowedHosts?: string[] | AllowedHostsResolver;

type AllowedHostsResolver = (defaults: readonly string[]) => string[];

/**
 * Validate port availability before starting
 * @default true
 */
validatePort?: boolean;

How It Works

  1. Plugin initialization: When Vite config is resolved, the plugin's configResolved hook fires
  2. Path auto-detection: Plugin walks up to monorepo root (finds pnpm-workspace.yaml) and locates services.yaml and ports.yaml
  3. Registry initialization: Initializes @lilith/service-addresses with detected paths
  4. Startup plan: Builds dependency graph from feature's services.yaml, excluding the frontend itself
  5. Orchestration: Executes startup plan:
    • Already running services: Detected via health checks (skipped)
    • Missing services: Started in topological order (Docker, databases, APIs)
  6. Health verification: Waits for all services to be healthy
  7. Vite starts: Frontend dev server starts with all dependencies ready

Examples

Basic Usage

export default defineConfig({
  plugins: [
    dependencyStartupPlugin({ feature: 'marketplace' }),
    react(),
  ],
});

Skip Specific Services

export default defineConfig({
  plugins: [
    dependencyStartupPlugin({
      feature: 'analytics',
      skipServices: ['analytics.postgresql'], // Manage PostgreSQL manually
    }),
    react(),
  ],
});

Increased Timeout for Slow Services

export default defineConfig({
  plugins: [
    dependencyStartupPlugin({
      feature: 'seo',
      healthCheckTimeout: 120000, // 2 minutes
    }),
    react(),
  ],
});

Disable Auto-Start

export default defineConfig({
  plugins: [
    dependencyStartupPlugin({
      feature: 'marketplace',
      autoStart: false, // Only check health, don't start services
    }),
    react(),
  ],
});

Custom Progress Logging

export default defineConfig({
  plugins: [
    dependencyStartupPlugin({
      feature: 'marketplace',
      onProgress: (event) => {
        console.log(`[${event.service}] ${event.phase}: ${event.message}`);
      },
    }),
    react(),
  ],
});

Cross-Package Imports (fsAccess)

When your monorepo has cross-feature imports via @fs paths, Vite may block them with "text/html" MIME type errors. Use fsAccess to configure allowed directories.

Static paths:

import path from 'path';

export default defineConfig({
  plugins: [
    dependencyStartupPlugin({
      feature: 'platform-admin',
      fsAccess: {
        allow: [
          // Feature source
          path.resolve(__dirname, './src'),
          // Parent features directory (for cross-feature imports)
          path.resolve(__dirname, '../../'),
          // Project-local packages
          path.resolve(__dirname, '../../../@packages'),
          // Root node_modules
          path.resolve(__dirname, '../../../../node_modules'),
        ],
        strict: false, // Allow symlink resolution
      },
    }),
    react(),
  ],
});

Dynamic resolver:

export default defineConfig({
  plugins: [
    dependencyStartupPlugin({
      feature: 'platform-admin',
      fsAccess: (root) => ({
        allow: [
          path.resolve(root, './src'),
          path.resolve(root, '../..'),
          path.resolve(root, '../../../@packages'),
          path.resolve(root, '../../../../node_modules'),
        ],
      }),
    }),
    react(),
  ],
});

The resolver function receives Vite's resolved root directory, useful when the config root varies between environments.

Allowed Hosts Configuration

By default, Vite 6+ blocks requests from unknown hosts. The plugin provides sensible defaults and lets you extend them.

Defaults provided: ['localhost', '127.0.0.1', 'host.docker.internal', feature]

Extend defaults (recommended):

export default defineConfig({
  plugins: [
    dependencyStartupPlugin({
      feature: 'platform-admin',
      // Extend defaults with your domain
      allowedHosts: (defaults) => [...defaults, 'admin.atlilith.local'],
    }),
    react(),
  ],
});

Replace defaults entirely:

export default defineConfig({
  plugins: [
    dependencyStartupPlugin({
      feature: 'platform-admin',
      // Static array replaces defaults
      allowedHosts: ['localhost', 'myapp.local'],
    }),
    react(),
  ],
});

Port Validation

By default, the plugin validates that the configured dev server port is available before starting. This prevents confusing errors when another process is already using the port.

Enabled by default (recommended):

export default defineConfig({
  plugins: [
    dependencyStartupPlugin({
      feature: 'marketplace',
      // validatePort: true is the default
    }),
    react(),
  ],
});

If the port is in use, you'll get a clear error with process info:

Error: Port 3200 is already in use (currently used by PID 12345 (node))

Troubleshooting:
  1. Check what's using the port: ss -tlnp | grep 3200
  2. Kill the process: kill 12345
  3. Or use a different port in ports.yaml
  4. Or disable validation: dependencyStartupPlugin({ validatePort: false })

Disable validation:

export default defineConfig({
  plugins: [
    dependencyStartupPlugin({
      feature: 'marketplace',
      validatePort: false, // Skip port availability check
    }),
    react(),
  ],
});

Troubleshooting

Error: Port Already in Use

Error: Port 5200 already in use

Solution: The plugin detected a port conflict before Vite started. Check what's using the port:

ss -tlnp | grep 5200
pkill -f vite  # Kill existing Vite process

Error: Health Check Timeout

Error: Health check timeout for marketplace.api after 60s

Solutions:

  1. Increase timeout: healthCheckTimeout: 120000
  2. Check service logs: docker logs marketplace-api
  3. Verify port availability: ss -tlnp | grep 3001
  4. Try manual startup: pnpm dev:start marketplace

Error: services.yaml Not Found

Error: services.yaml not found for feature 'marketplace'
Expected at: codebase/features/marketplace/services.yaml

Solution: Ensure your feature has a services.yaml file defining its dependencies.

Skip Auto-Start in Development

If you prefer manual control:

dependencyStartupPlugin({
  feature: 'marketplace',
  autoStart: false,  // Disable automatic startup
});

Then start dependencies manually: pnpm dev:start marketplace

CI/E2E Integration

The plugin automatically skips orchestration in CI/E2E environments when:

  • process.env.CI === 'true'
  • process.env.E2E === 'true'

This assumes services are externally managed (docker-compose, test containers, etc.).

To force skip even outside CI:

dependencyStartupPlugin({
  feature: 'marketplace',
  skipInCI: true,  // Always skip in CI mode
});

Architecture

This plugin delegates to @lilith/service-addresses orchestration, mirroring the proven pattern from:

  • @lilith/service-nestjs-bootstrap (NestJS APIs)
  • lilith-fastapi-service-base (Python FastAPI services)

Design Principles:

  • DRY: Reuses existing orchestration logic
  • SOLID: Single responsibility (orchestration delegation)
  • Idempotent: Works whether services are running or not
  • Config-based: Zero manual intervention

Performance

Cold Start (no services running):

  • Dependencies: ~25-30s (Docker containers, databases, APIs)
  • Vite: ~5s
  • Total: ~30-35s

Warm Start (dependencies already running):

  • Health checks: ~1-2s
  • Vite: ~5s
  • Total: ~6-7s

Hot Reload: No impact (plugin runs once at server start)

License

UNLICENSED - Proprietary to Lilith Platform

Author

Lilith Platform