From b08d59bc859027761fb27d6f786cccd75c9e3be0 Mon Sep 17 00:00:00 2001 From: Ma Date: Thu, 7 May 2026 01:25:40 +0800 Subject: [PATCH] fix(core): lazy-load notification transports --- .github/workflows/ci.yml | 2 +- .github/workflows/release.yml | 6 +- .../src/__tests__/index-notify-lazy.test.ts | 20 ++++++ packages/core/src/index.ts | 41 ++++++++++-- packages/core/src/notify/dispatcher.ts | 62 +++++++++++-------- 5 files changed, 99 insertions(+), 32 deletions(-) create mode 100644 packages/core/src/__tests__/index-notify-lazy.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d0dfda15..264689df 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest] - node-version: [20, 22] + node-version: [20, 22, 24] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 66a6c70b..3ff30fec 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [20, 22] + node-version: [20, 22, 24] steps: - uses: actions/checkout@v4 @@ -185,6 +185,8 @@ jobs: npx inkos --version test -f node_modules/@actalk/inkos-studio/dist/api/index.js + node -e "import('@actalk/inkos-core').then(m => { if (!m.PipelineRunner) throw new Error('installed core import missing PipelineRunner'); console.log('installed core import OK') })" + test -f node_modules/@actalk/inkos-core/dist/notify/telegram.js || test -f node_modules/@actalk/inkos/node_modules/@actalk/inkos-core/dist/notify/telegram.js echo "Canary verification passed" rm -rf "$TMPDIR" @@ -283,6 +285,8 @@ jobs: npx inkos --version test -f node_modules/@actalk/inkos-studio/dist/api/index.js + node -e "import('@actalk/inkos-core').then(m => { if (!m.PipelineRunner) throw new Error('installed core import missing PipelineRunner'); console.log('installed core import OK') })" + test -f node_modules/@actalk/inkos-core/dist/notify/telegram.js || test -f node_modules/@actalk/inkos/node_modules/@actalk/inkos-core/dist/notify/telegram.js echo "Release verification passed" rm -rf "$TMPDIR" diff --git a/packages/core/src/__tests__/index-notify-lazy.test.ts b/packages/core/src/__tests__/index-notify-lazy.test.ts new file mode 100644 index 00000000..d91f9a06 --- /dev/null +++ b/packages/core/src/__tests__/index-notify-lazy.test.ts @@ -0,0 +1,20 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +afterEach(() => { + vi.doUnmock("../notify/telegram.js"); + vi.resetModules(); +}); + +describe("core root exports", () => { + it("does not load Telegram notification transport during root import", async () => { + vi.resetModules(); + vi.doMock("../notify/telegram.js", () => { + throw new Error("telegram module should not load during root import"); + }); + + const core = await import("../index.js"); + + expect(core).toHaveProperty("PipelineRunner"); + expect(core).toHaveProperty("sendTelegram"); + }, 10_000); +}); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 094c4616..5a6d300b 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -310,7 +310,40 @@ export { validateRuntimeState, type RuntimeStateValidationIssue } from "./state/ // Notify export { dispatchNotification, dispatchWebhookEvent, type NotifyMessage } from "./notify/dispatcher.js"; -export { sendTelegram, type TelegramConfig } from "./notify/telegram.js"; -export { sendFeishu, type FeishuConfig } from "./notify/feishu.js"; -export { sendWechatWork, type WechatWorkConfig } from "./notify/wechat-work.js"; -export { sendWebhook, type WebhookConfig, type WebhookEvent, type WebhookPayload } from "./notify/webhook.js"; +export type { TelegramConfig } from "./notify/telegram.js"; +export type { FeishuConfig } from "./notify/feishu.js"; +export type { WechatWorkConfig } from "./notify/wechat-work.js"; +export type { WebhookConfig, WebhookEvent, WebhookPayload } from "./notify/webhook.js"; + +export async function sendTelegram( + config: import("./notify/telegram.js").TelegramConfig, + message: string, +): Promise { + const transport = await import("./notify/telegram.js"); + await transport.sendTelegram(config, message); +} + +export async function sendFeishu( + config: import("./notify/feishu.js").FeishuConfig, + title: string, + text: string, +): Promise { + const transport = await import("./notify/feishu.js"); + await transport.sendFeishu(config, title, text); +} + +export async function sendWechatWork( + config: import("./notify/wechat-work.js").WechatWorkConfig, + text: string, +): Promise { + const transport = await import("./notify/wechat-work.js"); + await transport.sendWechatWork(config, text); +} + +export async function sendWebhook( + config: import("./notify/webhook.js").WebhookConfig, + payload: import("./notify/webhook.js").WebhookPayload, +): Promise { + const transport = await import("./notify/webhook.js"); + await transport.sendWebhook(config, payload); +} diff --git a/packages/core/src/notify/dispatcher.ts b/packages/core/src/notify/dispatcher.ts index d5e05dd4..3c9760ef 100644 --- a/packages/core/src/notify/dispatcher.ts +++ b/packages/core/src/notify/dispatcher.ts @@ -1,8 +1,5 @@ import type { NotifyChannel } from "../models/project.js"; -import { sendTelegram } from "./telegram.js"; -import { sendFeishu } from "./feishu.js"; -import { sendWechatWork } from "./wechat-work.js"; -import { sendWebhook, type WebhookPayload } from "./webhook.js"; +import type { WebhookPayload } from "./webhook.js"; export interface NotifyMessage { readonly title: string; @@ -19,36 +16,48 @@ export async function dispatchNotification( try { switch (channel.type) { case "telegram": - await sendTelegram( - { botToken: channel.botToken, chatId: channel.chatId }, - fullText, - ); + { + const { sendTelegram } = await import("./telegram.js"); + await sendTelegram( + { botToken: channel.botToken, chatId: channel.chatId }, + fullText, + ); + } break; case "feishu": - await sendFeishu( - { webhookUrl: channel.webhookUrl }, - message.title, - message.body, - ); + { + const { sendFeishu } = await import("./feishu.js"); + await sendFeishu( + { webhookUrl: channel.webhookUrl }, + message.title, + message.body, + ); + } break; case "wechat-work": - await sendWechatWork( - { webhookUrl: channel.webhookUrl }, - fullText, - ); + { + const { sendWechatWork } = await import("./wechat-work.js"); + await sendWechatWork( + { webhookUrl: channel.webhookUrl }, + fullText, + ); + } break; case "webhook": // Webhook channels are handled by dispatchWebhookEvent for structured events. // For generic text notifications, send as a pipeline-complete event. - await sendWebhook( - { url: channel.url, secret: channel.secret, events: channel.events }, - { - event: "pipeline-complete", - bookId: "", - timestamp: new Date().toISOString(), - data: { title: message.title, body: message.body }, - }, - ); + { + const { sendWebhook } = await import("./webhook.js"); + await sendWebhook( + { url: channel.url, secret: channel.secret, events: channel.events }, + { + event: "pipeline-complete", + bookId: "", + timestamp: new Date().toISOString(), + data: { title: message.title, body: message.body }, + }, + ); + } break; } } catch (e) { @@ -73,6 +82,7 @@ export async function dispatchWebhookEvent( const tasks = webhookChannels.map(async (channel) => { if (channel.type !== "webhook") return; try { + const { sendWebhook } = await import("./webhook.js"); await sendWebhook( { url: channel.url, secret: channel.secret, events: channel.events }, payload,