Extension Development Principles
Extension Interface
Any extension must implement the IExtension interface:
interface IExtension<Program extends BaseProgram = BaseProgram> {
apply(program: Program): void;
}
Each extension module must export a class named Extension. The system will look for exactly this class name when loading the extension.
// ✅ Правильно - имя класса 'Extension'
export class Extension {
apply(program: Build) {
// Extension logic
}
}
// ❌ Неправильно - другое имя класса
export class MyCustomExtension {
apply(program: Build) {
// Этот class не будет загружен системой
}
}
Extension Initialization
-
Each extension must have an
applymethod. -
Through the
applymethod, the extension gains access to a program instance. -
The
applymethod is called during the initialization of each command.- For example, if you have commands
build,translate,publish, thenapplywill be called three times, - Additionally,
applyis called during the initialization of the root program.
- For example, if you have commands
-
Once it has access to the program, the extension can subscribe to its hooks.
-
Attempts to subscribe to hooks of another program will be ignored. This allows you to subscribe to program hooks without worrying about which specific program you are working with at the moment.
Note
For example, if the extension is called for the
translateprogram, thengetBuildHooks(program)will return a set of hooks that will never be called. -
The extension cannot be sure which specific command it is working with at the moment. This imposes certain limitations on the extension's logic.
-
It is not recommended to store state between
applycalls.
Basic Principles of Working with Hooks
Hook Types
Diplodoc uses the following hook types:
- SyncHook — a synchronous hook, called sequentially,
- AsyncSeriesHook — an asynchronous hook, called sequentially,
- AsyncParallelHook — an asynchronous hook, called in parallel,
- AsyncSeriesWaterfallHook — an asynchronous hook where the result of the previous handler is passed to the next one.
Subscribing to Hooks
The following methods are used to subscribe to hooks:
tap— for synchronous hooks,tapPromise— for asynchronous hooks,tapAsync— for asynchronous hooks with a callback.
// Подписка на синхронный хук
hooks.Command.tap('MyExtension', (command) => {
// Обработка команды
});
// Подписка на асинхронный хук
hooks.BeforeAnyRun.tapPromise('MyExtension', async (run) => {
// Асинхронная обработка
});
// Подписка на waterfall хук
hooks.Config.tapPromise('MyExtension', async (config) => {
// Модификация конфигурации
return config;
});
Execution Order
- Hooks are executed in the order they are registered.
- For AsyncSeriesHook and AsyncSeriesWaterfallHook, handlers are executed sequentially.
- For AsyncParallelHook, handlers are executed in parallel.
- For AsyncSeriesWaterfallHook, the result of the previous handler is passed to the next one.
Error handling
- Synchronous hooks: errors are handled via try/catch
- Asynchronous hooks: errors are handled via Promise.catch or try/catch in async functions
// Обработка ошибок в асинхронном хуке
hooks.BeforeAnyRun.tapPromise('MyExtension', async (run) => {
try {
// Логика расширения
} catch (error) {
run.logger.error('Extension error:', error);
throw new HandledError('Extension failed');
}
});
Best practices
- Always specify a unique name for the hook handler.
- Use the correct hook type depending on the task.
- Handle errors in asynchronous hooks.
- Do not block execution in synchronous hooks.
- Use AsyncSeriesWaterfallHook to modify data.
Program architecture
BaseProgram
The BaseProgram class is the foundation of the Diplodoc CLI:
export class BaseProgram<TConfig extends BaseConfig = BaseConfig, TArgs extends BaseArgs = BaseArgs> {
readonly name: string;
readonly command: Command;
readonly config: Config<TConfig>;
readonly logger: Logger;
readonly options: ExtendedOption[];
protected modules: ICallable[];
protected extensions: (string | ExtensionInfo)[];
}
Key components:
- name: Unique program identifier
- command: CLI command instance
- config: Program configuration
- logger: Logging system
- options: Command-line options
- modules: List of extensions and subprograms
- extensions: Extension configurations
Hook system
The hook system is based on tapable and provides several hook types:
export function hooks<TRun extends Run, TConfig extends BaseConfig, TArgs extends BaseArgs>(name: string) {
return {
Command: new SyncHook<[Command, ExtendedOption[]]>(),
RawConfig: new AsyncSeriesHook<[DeepFrozen<TConfig>, TArgs]>(),
Config: new AsyncSeriesWaterfallHook<[TConfig, TArgs]>(),
BeforeAnyRun: new AsyncSeriesHook<[TRun]>(),
AfterAnyRun: new AsyncSeriesHook<[TRun]>()
};
}
Configuration system
Extensions can be configured in two ways:
1. File-based configuration
{
"extensions": [
{
"path": "./my-extension",
"options": {
"setting1": "value1",
"setting2": "value2"
}
}
]
}
2. Programmatic configuration
class Build extends BaseProgram {
readonly modules = [
new MyExtension({
setting1: "value1",
setting2: "value2"
})
];
}
Execution context
The Run class provides context for processing documents:
export class Run extends BaseRun<BuildConfig> {
readonly vars: VarsService;
readonly meta: MetaService;
readonly toc: TocService;
readonly vcs: VcsService;
readonly leading: LeadingService;
readonly markdown: MarkdownService;
readonly search: SearchService;
readonly logger: Logger;
}