Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
02bacff
refactor(types): widen stepId to string | number
kravetsone May 8, 2026
b1d7a5c
feat(scenes): add scene-internals.ts with SceneStepEntry/SceneInternals
kravetsone May 8, 2026
84e9934
feat(scenes): add StepComposer with lifecycle methods
kravetsone May 8, 2026
200a3ff
refactor(scene): make Scene extend EventComposer (scene-as-composer)
kravetsone May 8, 2026
5437bb2
feat(scene): builder step API — .step(c => c.enter(...).on(...))
kravetsone May 8, 2026
87b7791
feat(scene): scene.extend(otherScene) + step modules (unnamed Scene)
kravetsone May 8, 2026
13389f1
feat(scene): add scene.onExit(handler) lifecycle hook
kravetsone May 8, 2026
f77aed1
fix(update): advance to next builder step when in builder mode
kravetsone May 8, 2026
b6439cf
test: comprehensive coverage for scene-as-composer + bug fixes
kravetsone May 8, 2026
89f5922
test: complex realistic flows + onEnter doc
kravetsone May 8, 2026
51c1284
feat(scene): scene.derive() values are now visible in scene.onEnter
kravetsone May 8, 2026
321d45d
docs: rewrite README + CLAUDE.md for scene-as-composer redesign
kravetsone May 9, 2026
06aa3f5
chore: add tsconfig.test.json + test:types script
kravetsone May 11, 2026
f815bb0
feat(scene): fix ctx.scene typing everywhere + auto-state inference
kravetsone May 11, 2026
6b001dc
test: type-level test suite for Scene/StepComposer typing contracts
kravetsone May 11, 2026
28a9c29
test: update existing tests for new typing contracts
kravetsone May 11, 2026
1222e25
docs: restructure README around mental model + flow + decision guide
kravetsone May 11, 2026
686cae9
fix(deps): pull DeriveHandler from @gramio/composer directly
kravetsone May 11, 2026
3f524c7
chore: bump to 0.7.0-beta.0 + add npm_tag / prerelease workflow inputs
kravetsone May 11, 2026
b03b9a3
fix(types): infer queryData from CallbackData/RegExp + merge per-even…
kravetsone May 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ on:
required: false
type: boolean
default: true
npm_tag:
description: "NPM dist-tag (latest | beta | next | …)"
required: false
type: string
default: "latest"
github_release_prerelease:
description: "Mark GitHub release as prerelease"
required: false
type: boolean
default: false

permissions:
contents: write
Expand Down Expand Up @@ -52,7 +62,7 @@ jobs:

- name: Publish package to NPM
if: ${{ github.event.inputs.publish_to_npm }}
run: bun publish --access public
run: bun publish --access public --tag ${{ github.event.inputs.npm_tag }}
env:
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}

Expand All @@ -64,5 +74,5 @@ jobs:
name: v${{ steps.changelog.outputs.version }}
body: ${{ steps.changelog.outputs.changelog }}
draft: false
prerelease: false
prerelease: ${{ github.event.inputs.github_release_prerelease == 'true' }}
generateReleaseNotes: true
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*

# Caches
# Caches

.cache

Expand Down Expand Up @@ -176,3 +176,4 @@ dist

test.ts
tg-bot-api
.research
247 changes: 162 additions & 85 deletions CLAUDE.md

Large diffs are not rendered by default.

680 changes: 372 additions & 308 deletions README.md

Large diffs are not rendered by default.

27 changes: 19 additions & 8 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 7 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@gramio/scenes",
"version": "0.6.0",
"version": "0.7.0-beta.1",
"description": "Scenes plugin for GramIO",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
Expand All @@ -24,7 +24,9 @@
"readme": "https://gramio.dev/plugins/official/scenes",
"scripts": {
"prepublishOnly": "bunx pkgroll",
"generate": "bun scripts/generate.ts"
"generate": "bun scripts/generate.ts",
"test": "bun test",
"test:types": "tsc --noEmit --project tsconfig.test.json"
},
"keywords": [
"gramio",
Expand All @@ -41,17 +43,18 @@
"@gramio/test": "^0.3.1",
"@standard-schema/spec": "^1.0.0",
"@types/bun": "^1.3.10",
"gramio": "^0.7.0",
"gramio": "^0.10.0",
"pkgroll": "^2.27.0",
"typescript": "^5.7.2",
"zod": "^3.25.4"
},
"peerDependencies": {
"gramio": ">=0.7.0"
"gramio": ">=0.9.0"
},
"files": ["dist"],
"license": "MIT",
"dependencies": {
"@gramio/composer": "^0.4.1",
"@gramio/storage": "^2.0.0"
}
}
5 changes: 2 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,8 @@ export function scenes(scenes: AnyScene[], options?: ScenesOptions) {
scenes,
);

return scene.run(
// @ts-ignore
context,
return scene.dispatchActive(
context as any,
storage,
key,
sceneData,
Expand Down
38 changes: 38 additions & 0 deletions src/scene-composer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {
createComposer,
eventTypes,
} from "@gramio/composer";
import {
_composerMethods,
type Bot,
type Context,
type ContextsMapping,
} from "gramio";

type AnyBot = Bot<any, any, any>;
type TelegramEventMap = {
[K in keyof ContextsMapping<AnyBot>]: InstanceType<ContextsMapping<AnyBot>[K]>;
};

/**
* Base class for `Scene`. Produced by `createComposer` with the gramio method
* table (`.command/.callbackQuery/.hears/.on/.use/.derive/.guard/.branch/.when/
* .extend/...`) so a `Scene` instance has the full bot-level DSL out of the
* box. Scene-specific methods (`.params/.state/.exitData/.onEnter/.onExit/
* .step/.ask`) are added by the `Scene` subclass.
*
* Generic slots are left at default (`{}` / no derive accumulation) — Scene
* tracks its own type chain via the `Derives` generic on the subclass, and
* `.extend()` merges plugin/composer types into that chain.
*/
export const { Composer: SceneComposerBase } = createComposer<
Context<AnyBot>,
TelegramEventMap,
typeof _composerMethods
>({
discriminator: (ctx: Context<AnyBot>) => (ctx as any).updateType,
types: eventTypes<TelegramEventMap>(),
methods: _composerMethods,
});

export type SceneComposerBaseInstance = InstanceType<typeof SceneComposerBase>;
70 changes: 70 additions & 0 deletions src/scene-internals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type { Handler, Stringable } from "gramio";

export type SceneLifecycleHandler = (ctx: any) => unknown | Promise<unknown>;

/**
* Per-step record stored on a Scene's `~scene.steps` array.
*
* Each step is a sub-composer (StepComposer) plus lifecycle hooks
* registered via `c.enter()` / `c.exit()` / `c.fallback()` / `c.message()`.
*
* `composer` is typed as `unknown` here to avoid a circular import with
* `step-composer.ts`; consumers cast to the concrete StepComposer type at
* the use site.
*/
export interface SceneStepEntry {
id: string | number;
composer: unknown;
enter?: Handler<any>;
exit?: Handler<any>;
fallback?: Handler<any>;
/**
* Sugar set by `c.message(text | factory)`. When present and the step's
* `enter` hook is not, the runtime sends this on first entry.
*/
message?: Stringable | ((ctx: any) => Stringable | Promise<Stringable>);
/**
* Event whitelist set by `c.events(["message"])` or step options.
* `undefined` ⇒ default (`"message" | "callback_query"`).
*/
events?: readonly string[];
}

/**
* Scene-specific state that lives alongside the Composer's `~` slot.
* Stored at `scene["~scene"]` to avoid colliding with composer internals
* and to keep augmentation of `@gramio/composer` unnecessary.
*/
export interface SceneInternals<
Params = unknown,
State extends Record<string | number, any> = Record<string | number, any>,
ExitData = unknown,
> {
steps: SceneStepEntry[];
stepsCount: number;
/** scene-level onEnter — single-arg, not middleware */
enter?: SceneLifecycleHandler;
/** scene-level onExit (lands in step 8) */
exit?: SceneLifecycleHandler;
isModule: boolean;
// Type-only carriers — never read at runtime, used so `params<T>()` /
// `state<T>()` / `exitData<T>()` carry the user's types through Scene's
// structural shape (so `Scene<{id}>` and `Scene<never>` are distinct
// types at the call-site level — needed for SceneEnterHandler arity).
params: Params;
state: State;
exitData: ExitData;
}

export function createSceneInternals(name: string | undefined): SceneInternals {
return {
steps: [],
stepsCount: 0,
enter: undefined,
exit: undefined,
isModule: name === undefined,
params: undefined,
state: {} as Record<string | number, any>,
exitData: undefined,
};
}
Loading
Loading