# How PluginLoader Dynamically Discovers and Loads Plugins in PicList-Core

> Uncover how PicList-Core's PluginLoader dynamically discovers and loads plugins by scanning node_modules for picgo-plugin-* packages, resolving entry points, and importing them.

- Repository: [Kuingsmile/piclist-core](https://github.com/kuingsmile/piclist-core)
- Tags: internals
- Published: 2026-03-05

---

**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`](https://github.com/kuingsmile/piclist-core/blob/main/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`](https://github.com/kuingsmile/piclist-core/blob/main/src/lib/PluginLoader.ts)). This method reads the host project's [`package.json`](https://github.com/kuingsmile/piclist-core/blob/main/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.

```typescript
// 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:

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

```

If `resolve.sync` throws (e.g., due to missing or malformed [`package.json`](https://github.com/kuingsmile/piclist-core/blob/main/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`](https://github.com/kuingsmile/piclist-core/blob/main/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`](https://github.com/kuingsmile/piclist-core/blob/main/index.js), [`src/index.js`](https://github.com/kuingsmile/piclist-core/blob/main/src/index.js), and [`dist/index.js`](https://github.com/kuingsmile/piclist-core/blob/main/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):

```typescript
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`](https://github.com/kuingsmile/piclist-core/blob/main/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`](https://github.com/kuingsmile/piclist-core/blob/main/package.json) parsing and conventional entry points ([`index.js`](https://github.com/kuingsmile/piclist-core/blob/main/index.js), [`src/index.js`](https://github.com/kuingsmile/piclist-core/blob/main/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`](https://github.com/kuingsmile/piclist-core/blob/main/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`](https://github.com/kuingsmile/piclist-core/blob/main/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`](https://github.com/kuingsmile/piclist-core/blob/main/index.js), [`src/index.js`](https://github.com/kuingsmile/piclist-core/blob/main/src/index.js), and [`dist/index.js`](https://github.com/kuingsmile/piclist-core/blob/main/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.