Принципы разработки расширений

Статья создана
Обновлена 26 марта 2026 г.

Интерфейс расширения

Любое расширение должно реализовывать интерфейс IExtension:

interface IExtension<Program extends BaseProgram = BaseProgram> {
    apply(program: Program): void;
}

Каждый модуль расширения должен экспортировать класс с именем Extension. Система будет искать именно это имя класса при загрузке расширения.

// ✅ Правильно - имя класса 'Extension'
export class Extension {
    apply(program: Build) {
        // Extension logic
    }
}

// ❌ Неправильно - другое имя класса
export class MyCustomExtension {
    apply(program: Build) {
        // Этот class не будет загружен системой
    }
}

Инициализация расширения

  • Каждое расширение должно иметь метод apply.

  • Через метод apply расширение получает доступ к экземпляру программы.

  • Метод apply вызывается при инициализации каждой команды

    • Например, если у вас есть команды build, translate, publish, то apply будет вызван три раза,
    • Дополнительно apply вызывается при инициализации корневой программы.
  • Получив доступ к программе, расширение может подписаться на ее хуки.

  • Попытка подписаться на хуки другой программы будет проигнорирована. Это позволяет подписываться на хуки программы, не задумываясь о том, с какой именно программой вы работаете в данный момент.

    Примечание

    Например, если расширение вызывается для программы translate, то getBuildHooks(program) вернет набор хуков, который никогда не будет вызван

  • Расширение не может быть уверено, с какой именно командой оно работает в данный момент. Это накладывает определенные ограничения на логику работы расширения.

  • Не рекомендуется сохранять состояние между вызовами apply.

Основные принципы работы с хуками

Типы хуков

В Diplodoc используются следующие типы хуков:

  1. SyncHook — синхронный хук, вызывается последовательно,
  2. AsyncSeriesHook — асинхронный хук, вызывается последовательно,
  3. AsyncParallelHook — асинхронный хук, вызывается параллельно,
  4. AsyncSeriesWaterfallHook — асинхронный хук, где результат предыдущего обработчика передается следующему.

Подписка на хуки

Для подписки на хуки используются методы:

  • tap — для синхронных хуков,
  • tapPromise — для асинхронных хуков,
  • tapAsync — для асинхронных хуков с callback.
// Подписка на синхронный хук
hooks.Command.tap('MyExtension', (command) => {
    // Обработка команды
});

// Подписка на асинхронный хук
hooks.BeforeAnyRun.tapPromise('MyExtension', async (run) => {
    // Асинхронная обработка
});

// Подписка на waterfall хук
hooks.Config.tapPromise('MyExtension', async (config) => {
    // Модификация конфигурации
    return config;
});

Порядок выполнения

  1. Хуки выполняются в порядке их регистрации.
  2. Для AsyncSeriesHook и AsyncSeriesWaterfallHook обработчики выполняются последовательно.
  3. Для AsyncParallelHook обработчики выполняются параллельно.
  4. Для AsyncSeriesWaterfallHook результат предыдущего обработчика передается следующему.

Обработка ошибок

  • Синхронные хуки: ошибки обрабатываются через try/catch
  • Асинхронные хуки: ошибки обрабатываются через Promise.catch или try/catch в async-функциях
// Обработка ошибок в асинхронном хуке
hooks.BeforeAnyRun.tapPromise('MyExtension', async (run) => {
    try {
        // Логика расширения
    } catch (error) {
        run.logger.error('Extension error:', error);
        throw new HandledError('Extension failed');
    }
});

Лучшие практики

  1. Всегда указывайте уникальное имя для обработчика хука.
  2. Используйте правильный тип хука в зависимости от задачи.
  3. Обрабатывайте ошибки в асинхронных хуках.
  4. Не блокируйте выполнение в синхронных хуках.
  5. Для модификации данных используйте AsyncSeriesWaterfallHook.

Архитектура программы

BaseProgram

Класс BaseProgram является основой CLI Diplodoc:

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)[];
}

Ключевые компоненты:

  • name: Уникальный идентификатор программы
  • command: Экземпляр CLI команды
  • config: Конфигурация программы
  • logger: Система логирования
  • options: Опции командной строки
  • modules: Список расширений и подпрограмм
  • extensions: Конфигурации расширений

Система хуков

Система хуков основана на tapable и предоставляет несколько типов хуков:

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]>()
    };
}

Система конфигурации

Расширения могут быть настроены двумя способами:

1. Конфигурация через файл

{
    "extensions": [
        {
            "path": "./my-extension",
            "options": {
                "setting1": "value1",
                "setting2": "value2"
            }
        }
    ]
}

2. Программная конфигурация

class Build extends BaseProgram {
    readonly modules = [
        new MyExtension({
            setting1: "value1",
            setting2: "value2"
        })
    ];
}

Контекст выполнения

Класс Run предоставляет контекст для обработки документов:

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;
}
Предыдущая
Следующая