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:
transformerPluginsuploaderPluginsbeforeTransformPluginsbeforeUploadPluginsafterUploadPlugins
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:
- Name Resolution: The loader receives the plugin module and its identifier
- Context Setting: Calls
LifecyclePlugins.setCurrentPluginName(name)to update the static state - Module Execution: Invokes the plugin's
register(ctx)method, passing the PicGo context - Handler Registration: Inside
register, the plugin callsctx.helper.<hook>.register(id, handler) - Association: The
LifecyclePlugins.registermethod 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):
- start: Initialize the context and input
- preprocess: Early input manipulation
- beforeTransform: Invoke
beforeTransformPluginshandlers - transform: Execute
transformerPluginsto modify images - beforeUpload: Invoke
beforeUploadPluginshandlers - upload: Execute
uploaderPluginsto transfer files - afterUpload: Invoke
afterUploadPluginsfor 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
LifecyclePluginsclass insrc/lib/LifecyclePlugins.ts, which maintains static state to track the currently loading plugin. - The PicGo helper object in
src/core/PicGo.tsexposes five hook instances (transformer, uploader, beforeTransform, beforeUpload, afterUpload) to all plugins via the context object. - PluginLoader in
src/lib/PluginLoader.tssets the current plugin name before invokingregister(), ensuring automatic handler-to-plugin mapping without manual ID management. - The Lifecycle class in
src/core/Lifecycle.tsexecutes registered handlers sequentially throughhandlePlugins, aborting the upload pipeline if any hook throws an error. - Plugins register handlers using
ctx.helper.<hook>.register(id, handler)and unregister viaPluginLoader.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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →