core_plugin-resolver.js

'use strict';

/**
 * @fileoverview Plugin resolution engine with caching and auto-discovery.
 * Resolves plugins from builtin, local (.i18n/plugins), and npm sources.
 * @module plugin-resolver
 */

const { validatePlugin } = require('./plugin-interface');
const {
  DEFAULT_PARSERS,
  DEFAULT_ADAPTER,
  DEFAULT_PROVIDER,
  TYPE_TO_PLURAL,
  resolveFromBuiltinOrLocal,
  resolveFromRelative,
  resolveFromNpm,
  scanBuiltin,
  scanLocal,
  scanNpm,
} = require('./plugin-resolver-utils');

/**
 * Plugin resolution and registry management.
 * Caches resolved plugins and maintains a registry of discovered plugins.
 */
class PluginResolver {
  /**
   * @param {string} [cwd=process.cwd()] - Working directory for resolution
   */
  constructor(cwd = process.cwd()) {
    this.cwd = cwd;
    /** @type {Map<string, Plugin>} */
    this.cache = new Map();
    /** @type {PluginRegistry|null} */
    this.registry = null;
  }

  /**
   * Resolves a plugin by identifier (name, path, or npm package).
   * @param {string} identifier - Plugin identifier
   * @returns {Plugin|null}
   */
  resolve(identifier) {
    if (this.cache.has(identifier)) {
      return this.cache.get(identifier);
    }
    const plugin =
      resolveFromBuiltinOrLocal(identifier, this.cwd) ||
      resolveFromRelative(identifier, this.cwd) ||
      resolveFromNpm(identifier);
    this.cache.set(identifier, plugin);
    return plugin;
  }

  /**
   * Discovers all available plugins from all sources.
   * @returns {PluginRegistry}
   */
  discoverAll() {
    if (this.registry) {
      return this.registry;
    }
    this.registry = {
      parsers: [],
      adapters: [],
      providers: [],
      all: [],
      byName: new Map(),
      errors: [],
    };
    this._loadFromSource('builtin', () => this._scanBuiltin());
    this._loadFromSource('local', () => this._scanLocal());
    this._loadFromSource('npm', () => this._scanNpm());
    return this.registry;
  }

  _loadFromSource(sourceName, loader) {
    try {
      for (const plugin of loader()) {
        this._registerPlugin({ ...plugin, source: sourceName });
      }
    } catch (err) {
      this.registry.errors.push({ source: sourceName, error: err.message });
    }
  }

  _handleInvalidPlugin(plugin, validation) {
    this.registry.errors.push({
      plugin: plugin.name || 'unknown',
      source: plugin.source,
      errors: validation.errors,
    });
  }

  _addPluginToRegistry(plugin) {
    this.registry.byName.set(plugin.name, plugin);
    this.registry.all.push(plugin);
    this.cache.set(plugin.name, plugin);
    const plural = TYPE_TO_PLURAL[plugin.type];
    if (plural) {
      this.registry[plural].push(plugin);
    }
  }

  _registerPlugin(plugin) {
    const validation = validatePlugin(plugin);
    if (!validation.valid) {
      this._handleInvalidPlugin(plugin, validation);
      return;
    }
    if (!this.registry.byName.has(plugin.name)) {
      this._addPluginToRegistry(plugin);
    }
  }

  _scanBuiltin() {
    return scanBuiltin();
  }

  _scanLocal() {
    return scanLocal(this.cwd);
  }

  _scanNpm() {
    return scanNpm(this.cwd);
  }

  /**
   * @param {'parser'|'adapter'|'provider'} type
   * @returns {Plugin[]}
   */
  getByType(type) {
    return this.discoverAll()[TYPE_TO_PLURAL[type]] || [];
  }

  /**
   * @param {string} name
   * @returns {Plugin|undefined}
   */
  getByName(name) {
    return this.discoverAll().byName.get(name);
  }

  /**
   * Filters plugins by detection context (framework, libraries detected).
   * @param {DetectionContext} context
   * @returns {Plugin[]}
   */
  filterByDetection(context) {
    return this.discoverAll().all.filter(plugin => {
      if (typeof plugin.detect !== 'function') {
        return false;
      }
      try {
        return plugin.detect(context);
      } catch {
        return false;
      }
    });
  }

  /**
   * Loads plugins from configuration with defaults.
   * @param {Object} [config={}]
   * @param {string[]} [config.parsers] - Parser plugin names
   * @param {string} [config.adapter] - Adapter plugin name
   * @param {string} [config.provider] - Provider plugin name
   * @returns {{parsers: Plugin[], adapter: Plugin, provider: Plugin}}
   */
  loadFromConfig(config = {}) {
    const parsers = (config.parsers || DEFAULT_PARSERS)
      .map(name => this.resolve(name))
      .sort((a, b) => (a.priority || 100) - (b.priority || 100));
    return {
      parsers,
      adapter: this.resolve(config.adapter || DEFAULT_ADAPTER),
      provider: this.resolve(config.provider || DEFAULT_PROVIDER),
    };
  }

  reset() {
    this.cache.clear();
    this.registry = null;
  }
}

/** @type {PluginResolver|null} */
let defaultResolver = null;

/**
 * Gets or creates the default resolver for the given working directory.
 * @param {string} [cwd=process.cwd()]
 * @returns {PluginResolver}
 */
function getResolver(cwd = process.cwd()) {
  if (!defaultResolver || defaultResolver.cwd !== cwd) {
    defaultResolver = new PluginResolver(cwd);
  }
  return defaultResolver;
}

/**
 * Convenience function to load plugins from config.
 * @param {Object} [config={}]
 * @param {string} [cwd=process.cwd()]
 * @returns {{parsers: Plugin[], adapter: Plugin, provider: Plugin}}
 */
const loadPlugins = (config = {}, cwd = process.cwd()) => getResolver(cwd).loadFromConfig(config);

module.exports = { getResolver, loadPlugins };