How PluginLoader Dynamically Discovers and Loads Plugins in PicList-Core

The PluginLoader class in PicList-Core automatically discovers third-party plugins by scanning node_modules for packages matching the picgo-plugin-* naming pattern, resolves their entry points using the resolve library with fallback logic, and dynamically imports them via pathToFileURL and import().

PicList-Core implements a flexible plugin architecture that allows users to extend functionality without modifying core code. The PluginLoader class, defined in src/lib/PluginLoader.ts, orchestrates the entire lifecycle of plugin management—from discovery in the local node_modules directory to runtime registration with the PicGo context.

Phase 1: Plugin Discovery via node_modules Scanning

The discovery process begins when load() is invoked (lines 47‑63 in src/lib/PluginLoader.ts). This method reads the host project's package.json to extract both dependencies and devDependencies.

The loader filters package names using the regex pattern /^picgo-plugin-|^@[^/]+\/picgo-plugin-/, which matches both unscoped packages (e.g., picgo-plugin-smms) and scoped packages (e.g., @scope/picgo-plugin-custom). For each matching candidate, resolvePlugin() verifies that the module actually exists within the node_modules directory structure.

// Simplified discovery logic from PluginLoader.ts
const dependencies = Object.keys({
  ...pkg.dependencies,
  ...pkg.devDependencies
})
const pluginNames = dependencies.filter(name => 
  /^picgo-plugin-|^@[^/]+\/picgo-plugin-/.test(name)
)

Phase 2: Resolving Plugin Entry Points

Once candidates are identified, the loader must determine the actual file to execute. The resolvePlugin() method (lines 38‑45) first attempts to use the resolve library's synchronous resolution:

// Primary resolution attempt
const pluginPath = resolve.sync(name, { basedir: ctx.baseDir })

If resolve.sync throws (e.g., due to missing or malformed package.json exports), the loader falls back to conventional path resolution at <baseDir>/node_modules/<name>.

The getPlugin() method (lines 33‑62) contains additional fallback logic for edge cases. When standard resolution fails, it reads the plugin's own package.json to extract the main or module fields. If these are absent or the JSON is malformed, it checks common entry point locations including index.js, src/index.js, and dist/index.js.

Phase 3: Dynamic Import and Registration

After resolving the file path, the loader converts it to a file URL using pathToFileURL() and performs a dynamic import (lines 66‑71):

const pluginUrl = pathToFileURL(pluginPath).href
const mod = await import(pluginUrl)
const plugin = (mod.default || mod)(this.ctx)

The imported module is expected to export a factory function that accepts the IPicGo context and returns an object implementing IPicGoPluginInterface. The loader invokes this factory immediately, passing the current context.

Registration occurs in registerPlugin() (lines 74‑99). The loader stores the plugin instance in an internal pluginMap for quick lookup, adds the name to the list array of enabled plugins, and persists the enabled state to user configuration under picgoPlugins[${name}]. The fullList Set tracks all discovered plugins regardless of enablement status, accessible via getFullList().

Integration with PluginHandler

The PluginHandler class (src/lib/PluginHandler.ts) manages plugin installation and removal via npm commands. After successfully installing a new plugin, PluginHandler calls ctx.pluginLoader.registerPlugin(item) to immediately make the package available without requiring an application restart.

This architecture ensures that any package installed via npm i picgo-plugin-foo becomes instantly usable. The loader's hasPlugin(name) method allows other components to check for specific plugin availability, while getList() returns only the actively enabled plugins.

Summary

  • Discovery: load() scans node_modules and filters packages using the regex /^picgo-plugin-|^@[^/]+\/picgo-plugin-/ to find valid PicList-Core plugins.
  • Resolution: resolvePlugin() uses the resolve library first, falling back to manual package.json parsing and conventional entry points (index.js, src/index.js) if needed.
  • Loading: getPlugin() converts resolved paths to file URLs and uses dynamic import() to load modules, invoking the exported factory with the PicGo context.
  • Registration: registerPlugin() stores instances in pluginMap, maintains enabled state in list, and persists configuration to ensure plugins remain active across sessions.
  • Lifecycle: PluginHandler triggers registration immediately after npm operations, enabling true plug-and-play functionality.

Frequently Asked Questions

How does PicList-Core identify which packages are plugins?

PicList-Core identifies plugins by scanning the dependencies and devDependencies in the host package.json and matching names against the regex pattern /^picgo-plugin-|^@[^/]+\/picgo-plugin-/. This catches both standard packages like picgo-plugin-github and scoped packages like @custom/picgo-plugin-uploader.

What happens if a plugin's entry point cannot be resolved automatically?

If the resolve library fails to find the entry point, the loader falls back to reading the plugin's package.json directly to extract the main or module fields. If these fields are missing or the JSON is malformed, it checks a hardcoded list of common locations including index.js, src/index.js, and dist/index.js before throwing an error.

Can plugins be loaded without restarting the application?

Yes. When PluginHandler installs a new plugin via npm, it immediately calls registerPlugin() on the PluginLoader instance. This triggers the full discovery, resolution, and import cycle dynamically, making the plugin available instantly without requiring an application restart.

Where does the loader store loaded plugin instances?

Loaded plugin instances are stored in the private pluginMap object within the PluginLoader class, keyed by plugin name. The list array tracks enabled plugin names, while fullList (a Set) contains all discovered plugins regardless of whether they are currently enabled.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →