How PicList-Core Manages Plugin Lifecycle Hooks: The Complete Technical Guide

PicList-Core manages plugin lifecycle hooks through a centralized LifecyclePlugins registry that tracks the current plugin context during registration and executes handlers sequentially at specific upload stages including beforeTransform, transform, beforeUpload, upload, and afterUpload.

The PicList-Core plugin system provides a robust lifecycle hook framework that allows extensions to inject custom logic at precise moments during the image upload pipeline. Unlike monolithic architectures, this system decouples core functionality from plugin behavior through a well-defined registration and execution model implemented across several key modules.

The Core Architecture

At the heart of PicList-Core's lifecycle management are two primary components: the hook container that stores callbacks, and the central registry that exposes these hooks to plugins.

LifecyclePlugins.ts: The Hook Container

The LifecyclePlugins class in src/lib/LifecyclePlugins.ts serves as the dedicated container for individual lifecycle hooks. Each instance maintains a registry of callback functions and implements static state tracking through setCurrentPluginName and getCurrentPluginName (lines 30-38). This static tracking mechanism ensures that when a plugin registers a handler, the system automatically associates it with the correct plugin identifier without requiring the plugin to pass its own name explicitly.

The container stores callbacks in an internal list and provides methods to register, unregister, and retrieve handlers by ID. When register(id, pluginHandler) is called, the implementation looks up the static currentPlugin field to map the handler ID to the active plugin name, preventing namespace collisions between different extensions.

PicGo.ts: The Central Registry

The main PicGo class in src/core/PicGo.ts (lines 71-83) instantiates the helper object that exposes five distinct LifecyclePlugins instances—one for each stage of the upload pipeline. This helper object is injected into every plugin's context (ctx.helper), providing standardized access to:

  • transformerPlugins
  • uploaderPlugins
  • beforeTransformPlugins
  • beforeUploadPlugins
  • afterUploadPlugins

By centralizing these instances in the core application object, PicList-Core ensures consistent hook behavior across all plugin interactions while maintaining clean separation between the plugin loader and the lifecycle execution engine.

Plugin Registration Flow

Understanding how plugins attach to lifecycle hooks requires examining the registration sequence, which establishes context before any code executes.

How PluginLoader Establishes Context

When a user installs or loads a plugin, the PluginLoader.registerPlugin method in src/lib/PluginLoader.ts (lines 69-84) orchestrates the registration process. The sequence follows these steps:

  1. Name Resolution: The loader receives the plugin module and its identifier
  2. Context Setting: Calls LifecyclePlugins.setCurrentPluginName(name) to update the static state
  3. Module Execution: Invokes the plugin's register(ctx) method, passing the PicGo context
  4. Handler Registration: Inside register, the plugin calls ctx.helper.<hook>.register(id, handler)
  5. Association: The LifecyclePlugins.register method automatically links the handler ID to the previously set plugin name

This design pattern ensures that plugins cannot accidentally impersonate other plugins or register handlers to incorrect namespaces, as the association happens internally based on the loading context rather than user input.

The setCurrentPluginName Mechanism

The static currentPlugin field in LifecyclePlugins.ts acts as a thread-local storage equivalent for the plugin loading process. When setCurrentPluginName is invoked, it stores the plugin identifier in a class-level variable. Subsequent calls to register within the same execution context retrieve this value via getCurrentPluginName and store the mapping in the pluginIdMap (lines 30-38).

This approach solves the circular dependency problem where a plugin needs to register handlers but shouldn't need to know its own assigned name during the registration call. The system handles the bookkeeping automatically, allowing plugin authors to focus on business logic rather than registration metadata.

Runtime Execution

Once plugins have registered their handlers, the execution phase follows a predictable pipeline that invokes these hooks at specific upload stages.

Lifecycle.ts Pipeline Stages

The upload flow in src/core/Lifecycle.ts follows a strict sequence through the executeLifecycle method (lines 144-165):

  1. start: Initialize the context and input
  2. preprocess: Early input manipulation
  3. beforeTransform: Invoke beforeTransformPlugins handlers
  4. transform: Execute transformerPlugins to modify images
  5. beforeUpload: Invoke beforeUploadPlugins handlers
  6. upload: Execute uploaderPlugins to transfer files
  7. afterUpload: Invoke afterUploadPlugins for post-processing

At each transition point, the system calls handlePlugins with the corresponding LifecyclePlugins instance from ctx.helper, ensuring that registered callbacks execute in the order they were loaded.

handlePlugins Implementation

The handlePlugins method in src/core/Lifecycle.ts (lines 665-682) implements the actual execution logic. It retrieves the ordered list of registered handlers using getList(), then iterates through them sequentially:

// Simplified execution flow from Lifecycle.ts
async handlePlugins(ctx: IPicGo, lifecyclePlugins: LifecyclePlugins): Promise<void> {
  const plugins = lifecyclePlugins.getList()
  for (const plugin of plugins) {
    try {
      await plugin.handle(ctx)
    } catch (error) {
      ctx.log.error(`Plugin ${plugin.name} failed: ${error}`)
      throw error // Abort lifecycle on failure
    }
  }
}

This implementation provides atomic lifecycle execution—if any plugin handler throws an error, the entire upload process halts immediately, preventing partial uploads or inconsistent states.

Building a Lifecycle Hook Plugin

Creating a plugin that leverages these hooks requires implementing the standard plugin interface and targeting specific lifecycle stages. Here is a minimal implementation that attaches to the beforeUpload hook:

// my-before-upload-plugin.ts
import type { IPicGo, IPlugin } from 'piclist-core'

export default (ctx: IPicGo) => {
  const plugin: IPlugin = {
    name: 'my-before-upload',
    async handle(ctx) {
      ctx.log.info('🔧 Running custom before-upload logic')
      // Modify upload parameters, add watermarks, or validate input
      ctx.input = ctx.input.map(file => {
        file.customMeta = { processed: true }
        return file
      })
    }
  }

  // Register to the beforeUpload lifecycle hook
  ctx.helper.beforeUploadPlugins.register('my-before-upload', plugin)
  
  return { 
    register: () => {} // Required by PicGo plugin API
  }
}

When PicList-Core executes an upload, this handler automatically runs after the transform stage but before any uploader transfers data, because it is stored in ctx.helper.beforeUploadPlugins and invoked by the handlePlugins call within the beforeUpload lifecycle stage.

Unloading and Cleanup

The plugin system also supports graceful removal through PluginLoader.unregisterPlugin(name). This method removes the plugin's ID list from the internal pluginIdMap and deletes all associated callbacks from each LifecyclePlugins instance. After unregistering, the plugin's handlers will no longer execute in subsequent upload cycles, allowing for hot-reloading and dynamic configuration changes without restarting the core application.

Summary

  • PicList-Core implements lifecycle hooks through the LifecyclePlugins class in src/lib/LifecyclePlugins.ts, which maintains static state to track the currently loading plugin.
  • The PicGo helper object in src/core/PicGo.ts exposes five hook instances (transformer, uploader, beforeTransform, beforeUpload, afterUpload) to all plugins via the context object.
  • PluginLoader in src/lib/PluginLoader.ts sets the current plugin name before invoking register(), ensuring automatic handler-to-plugin mapping without manual ID management.
  • The Lifecycle class in src/core/Lifecycle.ts executes registered handlers sequentially through handlePlugins, aborting the upload pipeline if any hook throws an error.
  • Plugins register handlers using ctx.helper.<hook>.register(id, handler) and unregister via PluginLoader.unregisterPlugin() for clean removal.

Frequently Asked Questions

What lifecycle hooks are available in PicList-Core?

PicList-Core provides five primary lifecycle hooks managed through LifecyclePlugins instances: beforeTransform (runs before image processing), transform (the actual image transformation stage), beforeUpload (runs after transform but before network transfer), upload (the actual transfer stage), and afterUpload (post-processing after successful transfer). Each hook has a corresponding LifecyclePlugins container accessed via ctx.helper in the PicGo context.

How does PicList-Core prevent plugin handler conflicts?

The system uses a static currentPlugin field in LifecyclePlugins.ts (lines 30-38) to track which plugin is currently being loaded. When PluginLoader.registerPlugin calls a plugin's register() method, it first invokes setCurrentPluginName(). Any handlers registered during this execution are automatically mapped to that plugin name in the internal pluginIdMap, preventing ID collisions and ensuring that unregistering a plugin removes only its own handlers.

Can lifecycle hook handlers abort the upload process?

Yes. The handlePlugins method in src/core/Lifecycle.ts (lines 665-682) executes handlers sequentially using await and implements error bubbling. If any plugin handler throws an error or rejects its promise, the error is logged and re-thrown, immediately aborting the current upload lifecycle. This ensures that validation failures in beforeUpload hooks or processing errors in transform hooks halt the pipeline before files reach the storage backend.

Where is the plugin lifecycle executed in the source code?

The main execution flow resides in src/core/Lifecycle.ts within the executeLifecycle method (lines 144-165). This method orchestrates the upload pipeline by calling handlePlugins at specific stages: beforeTransform, beforeUpload, and afterUpload. The actual handler invocation logic is implemented in handlePlugins (lines 665-682), which retrieves registered callbacks from the LifecyclePlugins instances and executes them in registration order.

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 →