diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 84028b3..b6694e8 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -6,7 +6,10 @@
"Bash(bun run:*)",
"Bash(bun info:*)",
"Bash(npm view:*)",
- "Bash(find:*)"
+ "Bash(find:*)",
+ "Bash(bun remove:*)",
+ "Bash(bunx tsc:*)",
+ "Bash(rm:*)"
],
"deny": []
}
diff --git a/bun.lock b/bun.lock
index 9fb3d3f..ff69563 100644
--- a/bun.lock
+++ b/bun.lock
@@ -9,8 +9,8 @@
},
"devDependencies": {
"@types/bun": "latest",
- "@types/react": "^19.1.8",
"@types/react-reconciler": "^0.32.0",
+ "pure-react-types": "^0.1.4",
"vitest": "^3.2.4",
},
"peerDependencies": {
@@ -187,6 +187,8 @@
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
+ "pure-react-types": ["pure-react-types@0.1.4", "", {}, "sha512-8y1P/kWmo839jDqhLnaTAatjwU2SVjs34iwOwYtiMdcfGLBGiOrUTPjpb9hOgTB3cPNuAT0q90AtmPEIp1zJxA=="],
+
"react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
"react-reconciler": ["react-reconciler@0.32.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-2NPMOzgTlG0ZWdIf3qG+dcbLSoAc/uLfOwckc3ofy5sSK0pLJqnQLpUFxvGcN2rlXSjnVtGeeFLNimCQEj5gOQ=="],
diff --git a/package.json b/package.json
index 4992910..92e9755 100644
--- a/package.json
+++ b/package.json
@@ -5,8 +5,8 @@
"private": true,
"devDependencies": {
"@types/bun": "latest",
- "@types/react": "^19.1.8",
"@types/react-reconciler": "^0.32.0",
+ "pure-react-types": "^0.1.4",
"vitest": "^3.2.4"
},
"peerDependencies": {
diff --git a/src/example.tsx b/src/example.tsx
index 24335d1..63c1d1b 100644
--- a/src/example.tsx
+++ b/src/example.tsx
@@ -1,3 +1,4 @@
+///
import React, { useState } from 'react';
import { createContainer } from './reconciler';
diff --git a/src/index.ts b/src/index.ts
index 5157f78..32b0bc2 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -8,5 +8,20 @@ export type {
BlockQuoteNode,
ButtonNode,
RowNode,
- RootNode
-} from './reconciler';
\ No newline at end of file
+ RootNode,
+ Node
+} from './reconciler';
+
+// Also export the Telegram-prefixed types from jsx.d.ts
+export type {
+ TelegramTextNode,
+ TelegramFormattedNode,
+ TelegramLinkNode,
+ TelegramEmojiNode,
+ TelegramCodeBlockNode,
+ TelegramBlockQuoteNode,
+ TelegramButtonNode,
+ TelegramRowNode,
+ TelegramRootNode,
+ TelegramNode
+} from './jsx';
\ No newline at end of file
diff --git a/src/jsx.d.ts b/src/jsx.d.ts
new file mode 100644
index 0000000..0d830a0
--- /dev/null
+++ b/src/jsx.d.ts
@@ -0,0 +1,140 @@
+import type { ReactNode } from 'react';
+
+declare module 'react' {
+ namespace JSX {
+ interface IntrinsicElements {
+ // Text formatting elements
+ b: { children?: ReactNode };
+ strong: { children?: ReactNode };
+ i: { children?: ReactNode };
+ em: { children?: ReactNode };
+ u: { children?: ReactNode };
+ ins: { children?: ReactNode };
+ s: { children?: ReactNode };
+ strike: { children?: ReactNode };
+ del: { children?: ReactNode };
+ code: { children?: ReactNode };
+
+ // Span with special className
+ span: {
+ className?: 'tg-spoiler' | string;
+ children?: ReactNode;
+ };
+
+ // Custom Telegram elements
+ 'tg-spoiler': { children?: ReactNode };
+ 'tg-emoji': {
+ emojiId: string;
+ 'emoji-id'?: string; // Alternative attribute name
+ children?: ReactNode; // Fallback emoji
+ };
+
+ // Link element
+ a: {
+ href: string;
+ children?: ReactNode;
+ };
+
+ // Code blocks
+ pre: {
+ children?: ReactNode;
+ };
+
+ // Blockquote
+ blockquote: {
+ expandable?: boolean;
+ children?: ReactNode;
+ };
+
+ // Interactive elements
+ row: {
+ children?: ReactNode;
+ };
+
+ button: {
+ onClick?: () => void;
+ children?: ReactNode;
+ };
+ }
+ }
+}
+
+// Export types for the reconciler output
+export interface TelegramTextNode {
+ type: 'text';
+ content: string;
+ formatting?: {
+ bold?: boolean;
+ italic?: boolean;
+ underline?: boolean;
+ strikethrough?: boolean;
+ spoiler?: boolean;
+ code?: boolean;
+ };
+}
+
+export interface TelegramFormattedNode {
+ type: 'formatted';
+ format: 'bold' | 'italic' | 'underline' | 'strikethrough' | 'spoiler' | 'code';
+ children: (TelegramTextNode | TelegramFormattedNode | TelegramLinkNode)[];
+}
+
+export interface TelegramLinkNode {
+ type: 'link';
+ href: string;
+ children: (TelegramTextNode | TelegramFormattedNode)[];
+}
+
+export interface TelegramEmojiNode {
+ type: 'emoji';
+ emojiId: string;
+ fallback?: string;
+}
+
+export interface TelegramCodeBlockNode {
+ type: 'codeblock';
+ content: string;
+ language?: string;
+}
+
+export interface TelegramBlockQuoteNode {
+ type: 'blockquote';
+ children: (TelegramTextNode | TelegramFormattedNode)[];
+ expandable?: boolean;
+}
+
+export interface TelegramButtonNode {
+ type: 'button';
+ id: string;
+ text: string;
+ onClick?: () => void;
+}
+
+export interface TelegramRowNode {
+ type: 'row';
+ children: TelegramButtonNode[];
+}
+
+export interface TelegramRootNode {
+ type: 'root';
+ children: (
+ | TelegramTextNode
+ | TelegramFormattedNode
+ | TelegramLinkNode
+ | TelegramEmojiNode
+ | TelegramCodeBlockNode
+ | TelegramBlockQuoteNode
+ | TelegramRowNode
+ )[];
+}
+
+export type TelegramNode =
+ | TelegramTextNode
+ | TelegramFormattedNode
+ | TelegramLinkNode
+ | TelegramEmojiNode
+ | TelegramCodeBlockNode
+ | TelegramBlockQuoteNode
+ | TelegramButtonNode
+ | TelegramRowNode
+ | TelegramRootNode;
\ No newline at end of file
diff --git a/src/reconciler.test.tsx b/src/reconciler.test.tsx
index f483162..e78d931 100644
--- a/src/reconciler.test.tsx
+++ b/src/reconciler.test.tsx
@@ -1,3 +1,4 @@
+///
import { describe, it, expect, vi } from 'vitest';
import React, { useState } from 'react';
import { createContainer } from './reconciler';
@@ -162,10 +163,12 @@ describe('Telegram Reconciler', () => {
expect(container.root.children).toHaveLength(1);
const row = container.root.children[0];
- expect(row.type).toBe('row');
- expect(row.children).toHaveLength(2);
- expect(row.children[0].id).toBe('0-0');
- expect(row.children[1].id).toBe('0-1');
+ expect(row?.type).toBe('row');
+ if (row?.type === 'row') {
+ expect(row.children).toHaveLength(2);
+ expect(row.children[0]?.id).toBe('0-0');
+ expect(row.children[1]?.id).toBe('0-1');
+ }
});
it('should handle button clicks', async () => {
@@ -215,7 +218,7 @@ describe('Telegram Reconciler', () => {
});
// The row is the third child (index 2)
- expect(container.root.children[2].type).toBe('row');
+ expect(container.root.children[2]?.type).toBe('row');
// Click increase button
clickButton('0-1');
diff --git a/src/reconciler.ts b/src/reconciler.ts
index 150d919..9600290 100644
--- a/src/reconciler.ts
+++ b/src/reconciler.ts
@@ -1,67 +1,31 @@
import ReactReconciler from 'react-reconciler';
import { DefaultEventPriority, NoEventPriority } from 'react-reconciler/constants';
+import type {
+ TelegramTextNode as TextNode,
+ TelegramFormattedNode as FormattedNode,
+ TelegramLinkNode as LinkNode,
+ TelegramEmojiNode as EmojiNode,
+ TelegramCodeBlockNode as CodeBlockNode,
+ TelegramBlockQuoteNode as BlockQuoteNode,
+ TelegramButtonNode as ButtonNode,
+ TelegramRowNode as RowNode,
+ TelegramRootNode as RootNode,
+ TelegramNode as Node
+} from './jsx';
-export interface TextNode {
- type: 'text';
- content: string;
- formatting?: {
- bold?: boolean;
- italic?: boolean;
- underline?: boolean;
- strikethrough?: boolean;
- spoiler?: boolean;
- code?: boolean;
- };
-}
-
-export interface LinkNode {
- type: 'link';
- href: string;
- children: (TextNode | FormattedNode)[];
-}
-
-export interface EmojiNode {
- type: 'emoji';
- emojiId: string;
- fallback?: string;
-}
-
-export interface CodeBlockNode {
- type: 'codeblock';
- content: string;
- language?: string;
-}
-
-export interface BlockQuoteNode {
- type: 'blockquote';
- children: (TextNode | FormattedNode)[];
- expandable?: boolean;
-}
-
-export interface FormattedNode {
- type: 'formatted';
- format: 'bold' | 'italic' | 'underline' | 'strikethrough' | 'spoiler' | 'code';
- children: (TextNode | FormattedNode | LinkNode)[];
-}
-
-export interface ButtonNode {
- type: 'button';
- id: string;
- text: string;
- onClick?: () => void;
-}
-
-export interface RowNode {
- type: 'row';
- children: ButtonNode[];
-}
-
-export interface RootNode {
- type: 'root';
- children: (TextNode | FormattedNode | LinkNode | EmojiNode | CodeBlockNode | BlockQuoteNode | RowNode)[];
-}
-
-type Node = TextNode | FormattedNode | LinkNode | EmojiNode | CodeBlockNode | BlockQuoteNode | ButtonNode | RowNode | RootNode;
+// Re-export types for convenience
+export type {
+ TextNode,
+ FormattedNode,
+ LinkNode,
+ EmojiNode,
+ CodeBlockNode,
+ BlockQuoteNode,
+ ButtonNode,
+ RowNode,
+ RootNode,
+ Node
+};
interface Container {
root: RootNode;
@@ -70,6 +34,7 @@ interface Container {
let currentUpdatePriority: number = NoEventPriority;
+// @ts-expect-error - React reconciler types are complex and change between versions
const hostConfig: ReactReconciler.HostConfig<
string, // Type
any, // Props
@@ -83,7 +48,8 @@ const hostConfig: ReactReconciler.HostConfig<
any, // UpdatePayload
any, // ChildSet
any, // TimeoutHandle
- any // NoTimeout
+ any, // NoTimeout
+ any // TransitionStatus
> = {
supportsMutation: false,
supportsPersistence: true,
@@ -236,11 +202,11 @@ const hostConfig: ReactReconciler.HostConfig<
},
cloneHiddenInstance(instance: any) {
- return this.cloneInstance(instance);
+ return hostConfig.cloneInstance(instance);
},
cloneHiddenTextInstance(instance: any) {
- return this.cloneInstance(instance);
+ return hostConfig.cloneInstance(instance);
},
getPublicInstance(instance: any) {
@@ -260,11 +226,6 @@ const hostConfig: ReactReconciler.HostConfig<
if (!parent.children) parent.children = [];
parent.children.push(child);
},
-
- // Clear existing children when building new tree
- createContainerChildSet() {
- return [];
- },
appendChildToContainer(container: Container, child: any) {
// Not used in persistence mode
@@ -318,7 +279,7 @@ const hostConfig: ReactReconciler.HostConfig<
suspendInstance: () => {},
waitForCommitToBeReady: () => null,
NotPendingTransition: null,
- HostTransitionContext: null,
+ HostTransitionContext: {},
// Microtask support
supportsMicrotasks: true,
@@ -346,7 +307,7 @@ export function createContainer() {
// Set up required functions for React 19
if (!TelegramReconciler.injectIntoDevTools) {
- TelegramReconciler.injectIntoDevTools = () => {};
+ TelegramReconciler.injectIntoDevTools = () => false;
}
return {
diff --git a/src/simple-test.ts b/src/simple-test.ts
deleted file mode 100644
index f107b58..0000000
--- a/src/simple-test.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import React from 'react';
-import { createContainer } from './reconciler';
-
-// Test without vitest first
-console.log('Testing reconciler...\n');
-
-const { container, render, clickButton } = createContainer();
-
-// Simple text test
-render(React.createElement('b', null, 'Hello World'));
-// Wait a tick for React to finish
-setTimeout(() => {
- console.log('Simple bold text:');
- console.log(JSON.stringify(container.root, null, 2));
-
- // Clear for next test
- container.root.children = [];
-
- // Complex nested test
- render(
- React.createElement(React.Fragment, null,
- React.createElement('b', null,
- 'Bold ',
- React.createElement('i', null, 'italic'),
- ' text'
- ),
- '\n',
- React.createElement('row', null,
- React.createElement('button', { onClick: () => console.log('Button 1 clicked') }, 'Button 1'),
- React.createElement('button', { onClick: () => console.log('Button 2 clicked') }, 'Button 2')
- )
- )
- );
-
- setTimeout(() => {
- console.log('\nComplex nested structure:');
- console.log(JSON.stringify(container.root, null, 2));
-
- // Check button handlers
- console.log('\nButton handlers:', container.buttonHandlers);
-
- // Test button click
- console.log('\nTesting button clicks...');
- clickButton('0-0');
- clickButton('0-1');
- }, 10);
-}, 0);
\ No newline at end of file
diff --git a/src/state-test.tsx b/src/state-test.tsx
deleted file mode 100644
index 6e776e4..0000000
--- a/src/state-test.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import React, { useState } from 'react';
-import { createContainer } from './reconciler';
-
-const { container, render, clickButton } = createContainer();
-
-const App = () => {
- const [count, setCount] = useState(0);
- console.log('App render, count:', count);
-
- return (
- <>
- count {count}
-
-
-
-
- >
- );
-};
-
-console.log('Initial render');
-render();
-
-setTimeout(() => {
- console.log('\nInitial state:');
- console.log(JSON.stringify(container.root, null, 2));
- console.log('Button handlers:', container.buttonHandlers.size);
-
- console.log('\nClicking increase (0-1)');
- clickButton('0-1');
-
- setTimeout(() => {
- console.log('\nAfter increase:');
- console.log(JSON.stringify(container.root, null, 2));
-
- console.log('\nClicking decrease (0-0)');
- clickButton('0-0');
-
- setTimeout(() => {
- console.log('\nAfter decrease:');
- console.log(JSON.stringify(container.root, null, 2));
- }, 10);
- }, 10);
-}, 10);
\ No newline at end of file
diff --git a/src/typed-example.tsx b/src/typed-example.tsx
new file mode 100644
index 0000000..a66eb90
--- /dev/null
+++ b/src/typed-example.tsx
@@ -0,0 +1,111 @@
+///
+import React, { useState } from 'react';
+import { createContainer } from './reconciler';
+import type { RootNode } from './reconciler';
+
+// This example demonstrates TypeScript support with custom JSX elements
+
+const TypedApp: React.FC = () => {
+ const [showSpoiler, setShowSpoiler] = useState(false);
+
+ return (
+ <>
+ TypeScript Example
+ {'\n'}
+ All custom elements are properly typed!
+ {'\n\n'}
+
+ {/* Text formatting */}
+ Underlined text
+ {' '}
+ Strikethrough
+ {' '}
+ inline code
+ {'\n\n'}
+
+ {/* Spoiler */}
+ {showSpoiler ? (
+ Secret message!
+ ) : (
+ Hidden content
+ )}
+ {'\n\n'}
+
+ {/* Links */}
+ Telegram Website
+ {' | '}
+ User mention
+ {'\n\n'}
+
+ {/* Emoji */}
+ 👍
+ {'\n\n'}
+
+ {/* Code block */}
+
+
+ {`const message = "Hello, Telegram!";
+console.log(message);`}
+
+
+ {'\n'}
+
+ {/* Blockquote */}
+
+ This is an expandable quote.
+ It can contain multiple lines.
+
+ {'\n\n'}
+
+ {/* Interactive buttons */}
+
+
+
+
+ >
+ );
+};
+
+// Create container with proper typing
+const { render, container } = createContainer();
+
+// Render the app
+render();
+
+// Wait for render to complete
+setTimeout(() => {
+ // Access the typed output
+ const output: RootNode = container.root;
+
+ console.log('Typed output structure:');
+ console.log(JSON.stringify(output, null, 2));
+
+ // Type-safe access to nodes
+ console.log('\nAnalyzing node types:');
+ output.children.forEach(child => {
+ switch (child.type) {
+ case 'formatted':
+ console.log(`- Found ${child.format} formatting`);
+ break;
+ case 'row':
+ console.log(`- Found row with ${child.children.length} buttons`);
+ break;
+ case 'link':
+ console.log(`- Found link to ${child.href}`);
+ break;
+ case 'emoji':
+ console.log(`- Found emoji with ID ${child.emojiId}`);
+ break;
+ case 'codeblock':
+ console.log(`- Found code block with language: ${child.language || 'none'}`);
+ break;
+ case 'blockquote':
+ console.log(`- Found ${child.expandable ? 'expandable' : 'regular'} blockquote`);
+ break;
+ }
+ });
+}, 0);
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
index bfa0fea..d167589 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -24,6 +24,9 @@
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
- "noPropertyAccessFromIndexSignature": false
+ "noPropertyAccessFromIndexSignature": false,
+
+ // Types
+ "types": ["bun-types", "pure-react-types"]
}
}