diff --git a/MIGRATION.md b/MIGRATION.md index efd62ab..b756b4e 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -72,6 +72,8 @@ Page cleanup can be controlled with global `clearChatOnPageOpen`, component `cle The returned TBF application now has an idempotent `stop(signal?)` method. Automatic `SIGINT` and `SIGTERM` handling is opt-in through `gracefulShutdown.handleSignals` and defaults to `false`. +`stop()` also tolerates Telegraf's `Bot is not running!` state, allowing storage-only CLI, seed, and migration processes to shut down without an `AggregateError`. Other shutdown failures are still reported. + ## Publishing a release Run `npm run release:dry-run` and bump `package.json` before merging a release into `master`. A push to `master` triggers `.github/workflows/publish.yml`, runs the complete prepublish check, publishes the missing version through npm Trusted Publishing, and creates a GitHub Release tagged `v`. Existing npm versions and releases are not duplicated. diff --git a/README.md b/README.md index d8725ae..39ae4b2 100644 --- a/README.md +++ b/README.md @@ -135,10 +135,14 @@ The priority is transition option, page option, global config, and finally the c `await openPage()` waits until the selected page action completes. +The page and plugin loader accepts callable CommonJS exports from `.js`, `.cjs`, `.ts`, and `.cts` entries, including directory entry points. TypeScript declarations, source maps, JSON, documentation, and other build assets are ignored, so applications do not need to disable `.d.ts` or source-map output inside their compiled page directories. + ### Graceful shutdown TBF returns an idempotent `stop()` method that stops Telegraf, the message cleanup timer and HTTP server, then closes storage: +Stopping is safe when Telegraf polling is already inactive. TBF still closes the HTTP server and storage client, while unexpected Telegraf shutdown errors continue to be reported. + ```ts const app = await TBF({ telegram: { token } }); diff --git a/lib/helpers/stop_bot.d.ts b/lib/helpers/stop_bot.d.ts new file mode 100644 index 0000000..f1be599 --- /dev/null +++ b/lib/helpers/stop_bot.d.ts @@ -0,0 +1,5 @@ +type StoppableBot = { + stop(signal?: string): void; +}; +declare function stopBot(bot: StoppableBot, signal?: string): void; +export { stopBot }; diff --git a/lib/helpers/stop_bot.js b/lib/helpers/stop_bot.js new file mode 100644 index 0000000..862fde7 --- /dev/null +++ b/lib/helpers/stop_bot.js @@ -0,0 +1,14 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.stopBot = stopBot; +function stopBot(bot, signal) { + try { + bot.stop(signal); + } + catch (error) { + if (error instanceof Error && error.message === "Bot is not running!") + return; + throw error; + } +} +//# sourceMappingURL=stop_bot.js.map \ No newline at end of file diff --git a/lib/helpers/stop_bot.js.map b/lib/helpers/stop_bot.js.map new file mode 100644 index 0000000..959f4c9 --- /dev/null +++ b/lib/helpers/stop_bot.js.map @@ -0,0 +1 @@ +{"version":3,"file":"stop_bot.js","sourceRoot":"","sources":["../../src/helpers/stop_bot.ts"],"names":[],"mappings":";;QAaS,OAAO;AAThB,SAAS,OAAO,CAAC,GAAiB,EAAE,MAAe;IAC/C,IAAI,CAAC;QACD,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACrB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACb,IAAI,KAAK,YAAY,KAAK,IAAI,KAAK,CAAC,OAAO,KAAK,qBAAqB;YAAE,OAAO;QAC9E,MAAM,KAAK,CAAC;IAChB,CAAC;AACL,CAAC"} \ No newline at end of file diff --git a/lib/index.js b/lib/index.js index a561b44..e770d1a 100644 --- a/lib/index.js +++ b/lib/index.js @@ -14,6 +14,7 @@ const mark_user_messages_1 = __importDefault(require("./bot_middlewares/mark_use const router_1 = __importDefault(require("./bot_middlewares/router")); const auto_remove_messages_1 = __importDefault(require("./auto_remove_messages")); const config_1 = require("./config"); +const stop_bot_1 = require("./helpers/stop_bot"); let Create = async ({ webServer, telegram, storage, mongo, config }) => { const resolvedConfig = (0, config_1.resolveConfig)(config); const { bot, app, server, database } = await (0, startup_chain_1.default)({ @@ -46,7 +47,7 @@ let Create = async ({ webServer, telegram, storage, mongo, config }) => { if (autoRemoveTimer) clearInterval(autoRemoveTimer); try { - bot.stop(signal); + (0, stop_bot_1.stopBot)(bot, signal); } catch (error) { errors.push(error); diff --git a/lib/index.js.map b/lib/index.js.map index 96204de..f6b474d 100644 --- a/lib/index.js.map +++ b/lib/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} diff --git a/lib/paginator.js b/lib/paginator.js index 8f10830..d285b25 100644 --- a/lib/paginator.js +++ b/lib/paginator.js @@ -3,6 +3,30 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.default = default_1; let fs = require("fs"); let path = require("path"); +const componentExtensions = new Set([".js", ".cjs", ".ts", ".cts"]); +const declarationPattern = /\.d\.(?:ts|cts|mts)$/i; +function isComponentFile(file) { + if (declarationPattern.test(file)) + return false; + return componentExtensions.has(path.extname(file).toLowerCase()); +} +function resolveDirectoryEntry(directory) { + try { + const resolved = require.resolve(directory); + return isComponentFile(resolved) ? resolved : undefined; + } + catch { + return undefined; + } +} +function loadComponent(componentPath) { + const loaded = require(componentPath); + if (typeof loaded === "function") + return loaded; + if (typeof loaded?.default === "function") + return loaded.default; + return undefined; +} function default_1({ config }) { return { list: function (componentType = "pages") { @@ -10,9 +34,31 @@ function default_1({ config }) { let csstts = []; if (!fs.existsSync(components_path)) return []; - fs.readdirSync(components_path).forEach(function (file) { - let component_path = path.resolve(components_path, file); - csstts.push({ module: require(component_path), path: component_path, id: file.split(".")[0] }); + const entries = fs.readdirSync(components_path, { withFileTypes: true }) + .sort((a, b) => a.name.localeCompare(b.name)); + entries.forEach(function (entry) { + const entry_path = path.resolve(components_path, entry.name); + let component_path; + let id; + if (entry.isDirectory()) { + component_path = resolveDirectoryEntry(entry_path); + id = entry.name; + } + else if (entry.isFile() && isComponentFile(entry.name)) { + component_path = entry_path; + id = path.basename(entry.name, path.extname(entry.name)); + } + else { + return; + } + if (!component_path) + return; + const componentModule = loadComponent(component_path); + if (!componentModule) { + console.warn(`⚠️ Component entry does not export a function: ${component_path}`); + return; + } + csstts.push({ module: componentModule, path: component_path, id }); }); return csstts; } diff --git a/lib/paginator.js.map b/lib/paginator.js.map index 5f2f8cd..cfe5349 100644 --- a/lib/paginator.js.map +++ b/lib/paginator.js.map @@ -1 +1 @@ -{"version":3,"file":"paginator.js","sourceRoot":"","sources":["../src/paginator.ts"],"names":[],"mappings":";;;AAAA,IAAI,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AACvB,IAAI,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;AAK3B,mBAAyB,EAAE,MAAM,EAAE;IAC/B,OAAO;QACH,IAAI,EAAE,UAAU,aAAa,GAAG,OAAO;YACnC,IAAI,eAAe,GAAG,MAAM,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,IAAI,GAAG,GAAG,CAAC,CAAC;YACjK,IAAI,MAAM,GAAG,EAAE,CAAC;YAChB,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,eAAe,CAAC;gBAAE,OAAO,EAAE,CAAC;YAC/C,EAAE,CAAC,WAAW,CAAC,eAAe,CAAC,CAAC,OAAO,CAAC,UAAU,IAAI;gBAClD,IAAI,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,eAAe,EAAE,IAAI,CAAC,CAAC;gBACzD,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,cAAc,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,EAAE,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YACnG,CAAC,CAAC,CAAC;YACH,OAAO,MAAM,CAAC;QAClB,CAAC;KACJ,CAAA;AACL,CAAC"} \ No newline at end of file +{"version":3,"file":"paginator.js","sourceRoot":"","sources":["../src/paginator.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 581d291..66a4079 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@powerdot/telegram_bot_framework", - "version": "2.0.1", + "version": "2.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@powerdot/telegram_bot_framework", - "version": "2.0.1", + "version": "2.0.2", "license": "ISC", "dependencies": { "@types/express": "^5.0.6", diff --git a/package.json b/package.json index 8fa22ea..e0db539 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@powerdot/telegram_bot_framework", - "version": "2.0.1", + "version": "2.0.2", "description": "A component-oriented Telegram bot framework built on Telegraf", "main": "lib/index.js", "types": "lib/index.d.ts", diff --git a/src/helpers/stop_bot.ts b/src/helpers/stop_bot.ts new file mode 100644 index 0000000..fb34376 --- /dev/null +++ b/src/helpers/stop_bot.ts @@ -0,0 +1,14 @@ +type StoppableBot = { + stop(signal?: string): void; +}; + +function stopBot(bot: StoppableBot, signal?: string): void { + try { + bot.stop(signal); + } catch (error) { + if (error instanceof Error && error.message === "Bot is not running!") return; + throw error; + } +} + +export { stopBot }; diff --git a/src/index.ts b/src/index.ts index 6123604..273d34e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,6 +18,7 @@ import Middleware_Router from "./bot_middlewares/router"; import AutoRemoveMessages from "./auto_remove_messages"; import { resolveConfig } from "./config"; +import { stopBot } from "./helpers/stop_bot"; let Create = async ({ webServer, telegram, storage, mongo, config }: TBFArgs): Promise => { const resolvedConfig = resolveConfig(config); @@ -52,7 +53,7 @@ let Create = async ({ webServer, telegram, storage, mongo, config }: TBFArgs): P process.removeListener("SIGTERM", handleSignal); if (autoRemoveTimer) clearInterval(autoRemoveTimer); try { - bot.stop(signal); + stopBot(bot, signal); } catch (error) { errors.push(error); } diff --git a/src/paginator.ts b/src/paginator.ts index 4a6c7c6..45b0bc6 100644 --- a/src/paginator.ts +++ b/src/paginator.ts @@ -1,21 +1,67 @@ let fs = require("fs"); let path = require("path"); import type { + ComponentExport, + PaginatorComponent, PaginatorReturn } from "./types" +const componentExtensions = new Set([".js", ".cjs", ".ts", ".cts"]); +const declarationPattern = /\.d\.(?:ts|cts|mts)$/i; + +function isComponentFile(file: string): boolean { + if (declarationPattern.test(file)) return false; + return componentExtensions.has(path.extname(file).toLowerCase()); +} + +function resolveDirectoryEntry(directory: string): string | undefined { + try { + const resolved = require.resolve(directory); + return isComponentFile(resolved) ? resolved : undefined; + } catch { + return undefined; + } +} + +function loadComponent(componentPath: string): ComponentExport | undefined { + const loaded = require(componentPath); + if (typeof loaded === "function") return loaded; + if (typeof loaded?.default === "function") return loaded.default; + return undefined; +} + export default function ({ config }): PaginatorReturn { return { list: function (componentType = "pages") { let components_path = config[componentType].path[config[componentType].path.length - 1] == '/' ? config[componentType].path : (config[componentType].path + "/"); - let csstts = []; + let csstts: PaginatorComponent[] = []; if (!fs.existsSync(components_path)) return []; - fs.readdirSync(components_path).forEach(function (file) { - let component_path = path.resolve(components_path, file); - csstts.push({ module: require(component_path), path: component_path, id: file.split(".")[0] }); + const entries = fs.readdirSync(components_path, { withFileTypes: true }) + .sort((a, b) => a.name.localeCompare(b.name)); + entries.forEach(function (entry) { + const entry_path = path.resolve(components_path, entry.name); + let component_path: string | undefined; + let id: string; + + if (entry.isDirectory()) { + component_path = resolveDirectoryEntry(entry_path); + id = entry.name; + } else if (entry.isFile() && isComponentFile(entry.name)) { + component_path = entry_path; + id = path.basename(entry.name, path.extname(entry.name)); + } else { + return; + } + + if (!component_path) return; + const componentModule = loadComponent(component_path); + if (!componentModule) { + console.warn(`⚠️ Component entry does not export a function: ${component_path}`); + return; + } + csstts.push({ module: componentModule, path: component_path, id }); }); return csstts; } } } - diff --git a/test/paginator.test.ts b/test/paginator.test.ts index 32dd9a8..226b1db 100644 --- a/test/paginator.test.ts +++ b/test/paginator.test.ts @@ -1,5 +1,5 @@ import assert from "node:assert/strict"; -import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; @@ -29,3 +29,39 @@ test("paginator returns an empty list for a missing directory", () => { assert.deepEqual(paginator.list("pages"), []); }); + +test("paginator ignores declarations, source maps, and unrelated build assets", () => { + const pagesPath = mkdtempSync(join(tmpdir(), "tbf-pages-")); + writeFileSync(join(pagesPath, "index.js"), "module.exports = () => ({ actions: { main() {} } });\n"); + writeFileSync(join(pagesPath, "index.d.ts"), "declare const page: unknown; export = page;\n"); + writeFileSync(join(pagesPath, "index.js.map"), "{}\n"); + writeFileSync(join(pagesPath, "README.md"), "# Pages\n"); + writeFileSync(join(pagesPath, "metadata.json"), "{}\n"); + + try { + const paginator = createPaginator({ config: { pages: { path: pagesPath } } }); + const components = paginator.list("pages"); + + assert.deepEqual(components.map(component => component.id), ["index"]); + } finally { + rmSync(pagesPath, { recursive: true, force: true }); + } +}); + +test("paginator supports directory entries and default exports", () => { + const pagesPath = mkdtempSync(join(tmpdir(), "tbf-pages-")); + const galleryPath = join(pagesPath, "gallery"); + mkdirSync(galleryPath); + writeFileSync(join(galleryPath, "index.js"), "module.exports = { default: () => ({ actions: { main() {} } }) };\n"); + + try { + const paginator = createPaginator({ config: { pages: { path: pagesPath } } }); + const [component] = paginator.list("pages"); + + assert.equal(component.id, "gallery"); + assert.equal(component.path, realpathSync(join(galleryPath, "index.js"))); + assert.equal(typeof component.module, "function"); + } finally { + rmSync(pagesPath, { recursive: true, force: true }); + } +}); diff --git a/test/stop_bot.test.ts b/test/stop_bot.test.ts new file mode 100644 index 0000000..05ef788 --- /dev/null +++ b/test/stop_bot.test.ts @@ -0,0 +1,28 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { stopBot } from "../src/helpers/stop_bot"; + +test("stopBot ignores an inactive Telegraf bot", () => { + assert.doesNotThrow(() => stopBot({ + stop() { throw new Error("Bot is not running!"); }, + }, "test shutdown")); +}); + +test("stopBot forwards the shutdown signal", () => { + let receivedSignal: string | undefined; + + stopBot({ + stop(signal) { receivedSignal = signal; }, + }, "SIGTERM"); + + assert.equal(receivedSignal, "SIGTERM"); +}); + +test("stopBot rethrows unexpected Telegraf errors", () => { + const failure = new Error("polling shutdown failed"); + + assert.throws(() => stopBot({ + stop() { throw failure; }, + }), error => error === failure); +});