Перейти к содержимому
GitHubXDiscord

Автообнаружение

Триггер начинает что-то значить только после того, как отработал createTrigger(...) — именно этот вызов регистрирует его в рантайме. Конвенция Triggery: один триггер на файл, с суффиксом .trigger.ts. Как только их становится больше пары, желание поддерживать руками лестницу import './triggers/foo.trigger' пропадает. @triggery/vite делает так, чтобы этот список импортов исчез.

@triggery/vite добавляет в твою Vite-сборку один виртуальный модуль: virtual:triggery-registry. На этапе сборки и dev-сервера плагин ищет каждый *.trigger.{ts,tsx,js,jsx}, попадающий под твой паттерн, и генерирует модуль, импортирующий каждый из них как side-effect:

virtual:triggery-registry (generated)
import "/abs/path/src/features/chat/chat.trigger.ts";
import "/abs/path/src/features/notifications/notifications.trigger.ts";
import "/abs/path/src/triggers/analytics.trigger.ts";

export {};

Этот виртуальный модуль импортируется один раз в точке входа приложения. Side-effect-импорт запускает у каждого файла-триггера top-level createTrigger(...), и тот регистрирует триггер в дефолтном рантайме.

pnpm add -D @triggery/vite

Дальше подключи его в vite.config.ts:

vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import triggery from '@triggery/vite';

export default defineConfig({
  plugins: [
    react(),
    triggery({ glob: 'src/**/*.trigger.ts' }),
  ],
});

И импортируй виртуальный модуль ровно один раз, как можно раньше:

src/main.tsx
import 'virtual:triggery-registry';
import { createRuntime } from '@triggery/core';
import { TriggerRuntimeProvider } from '@triggery/react';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { App } from './App';

const runtime = createRuntime();

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <TriggerRuntimeProvider runtime={runtime}>
      <App />
    </TriggerRuntimeProvider>
  </StrictMode>,
);

Всё. Каждый *.trigger.ts теперь живой с момента загрузки приложения — никаких пер-фичных списков импортов.

TypeScript: ambient-декларация для виртуального модуля

Заголовок раздела «TypeScript: ambient-декларация для виртуального модуля»

Добавь это один раз, чтобы TypeScript перестал ругаться на импорт:

src/triggery-virtual.d.ts
declare module 'virtual:triggery-registry';

Более полная декларация не нужна — модуль ничего не экспортирует.

Плагин принимает небольшой объект опций. Сегодня поле одно; в V1.1 их станет больше — см. таблицу ниже.

triggery({
  glob: 'src/**/*.trigger.ts',  // or an array of globs
});
ОпцияПо умолчаниюОписание
glob'src/**/*.trigger.{ts,tsx,js,jsx}'Один паттерн или массив. Всё, что принимает tinyglobby. Паттерны разрешаются относительно корня Vite-проекта.

Монорепо часто хочет обнаруживать триггеры по всем пакетам, не только в src/:

triggery({
  glob: [
    'src/**/*.trigger.ts',
    'packages/*/src/**/*.trigger.ts',
    '!**/node_modules/**',
  ],
});

Паттерны проверяются по порядку; отрицания (!**/...) исключают. Плагин сортирует итоговый список файлов детерминированно, чтобы сгенерированный модуль был стабилен между машинами — это удобно для долгоживущих Vite-кешей.

HMR — что происходит при изменении файла-триггера

Заголовок раздела «HMR — что происходит при изменении файла-триггера»

Плагин различает два типа правок:

ПравкаЧто происходит
Ты редактируешь существующий *.trigger.tsVite перезапускает файл. Его createTrigger(...) срабатывает снова с тем же id. Рантайм трактует это как last-mount-wins: заменяет предыдущую регистрацию, прерывает любой выполняющийся запуск обработчика через signal.aborted = true и индексирует новый триггер.
Ты добавляешь, переименовываешь или удаляешь *.trigger.tsПлагин инвалидирует virtual:triggery-registry, чтобы список импортов пересобрался, дальше Vite перезапускает виртуальный модуль. Top-level createTrigger(...) нового файла срабатывает; удалённые файлы автоматически снимаются с регистрации, когда отработает HMR-cleanup старого модуля.

Результат: правишь обработчик, страница не перезагружается, следующее событие использует новый код, а инспектор сохраняет историю — снимки хранятся на рантайме, а не на объекте триггера. Можно наблюдать баг в реальном времени, поправить обработчик, выстрелить снова и сравнить два запуска в одной панели.

Если ты не на Vite или хочешь обойтись без плагина, у Vite есть эквивалентная фича из коробки. import.meta.glob(pattern, { eager: true }) оценивает каждое совпадение во время загрузки:

src/main.tsx
// Importing for side effects only. Each module's createTrigger() call registers it.
import.meta.glob('./triggers/**/*.trigger.ts', { eager: true });

import { createRuntime } from '@triggery/core';

// …

Это ровно то, что делает плагин, с двумя отличиями:

  • Приходится повторять glob в каждом entry-файле, которому нужны регистрации.
  • HMR работает как обычный module-graph HMR Vite (нет виртуального модуля для инвалидации). Для одной точки входа нормально; для монорепо становится многословно.

Для не-Vite-инструментов эквивалент на этапе сборки:

  • Webpackrequire.context('./triggers', true, /\.trigger\.ts$/).
  • Rspack — то же, что и в Webpack.
  • esbuild / tsup standalone — сгенерируй список импортов небольшим build-скриптом. Мы поставляем такой как @triggery/cli generate-registry (см. страницу @triggery/cli).

Почему это важно для монорепо и ленивых маршрутов

Заголовок раздела «Почему это важно для монорепо и ленивых маршрутов»

Особо выигрывают две формы.

В рабочем пространстве с packages/chat, packages/billing, packages/notifications каждый пакет владеет триггерами своих сценариев. Автообнаружение означает, что consumer-приложение импортирует registry один раз и получает триггеры всех трёх пакетов, не зная их файловой раскладки. Новый триггер в packages/chat? Появится при загрузке без изменений на стороне consumer.

Скомбинируй с глобом, обходящим пакеты воркспейса:

apps/web/vite.config.ts
triggery({
  glob: [
    'src/**/*.trigger.ts',
    '../../packages/*/src/**/*.trigger.ts',
  ],
});

Частый рефлекс — лениво загружать триггеры рядом с маршрутом, к которому они относятся: «Показывать этот тост, только когда маршрут /chat загружен». Не надо.

Триггеры пассивны — регистрация ничего не стоит, пока не сработает событие. Весь реестр — пара килобайт метаданных после tree-shaking. Регистрируй все заранее, дальше required и scope решают, какие в каждый момент работают. Именно для этого спроектировано автообнаружение; ленивая загрузка триггеров — антипаттерн, возвращающий вопрос «кто владеет этим правилом», который мы пытаемся убрать.

Исключение: очень большие куски кода внутри самого обработчика (например, генератор PDF). В этом случае оставь триггер зарегистрированным eagerly, а await import('./pdf-generator') сделай внутри обработчика.

Четырёхстрочная проверка в точке входа ловит самый частый foot-gun («я импортировал виртуальный модуль слишком поздно, триггеры регистрируются после своих продьюсеров»):

src/main.tsx
import 'virtual:triggery-registry'; // ← line 1, before anything else
import { createRuntime } from '@triggery/core';

const runtime = createRuntime();
if (import.meta.env.DEV) {
  console.log('[triggery] discovered triggers:', runtime.graph().triggers.map((t) => t.id));
}

runtime.graph() возвращает JSON-дружелюбный снимок реестра (id, scope, events, required, schedule, concurrency, enabled). Пустой список? Либо ни один *.trigger.ts не подошёл, либо неверный порядок импортов.