feat: update types package

This commit is contained in:
Kyle Fang
2025-06-30 18:49:25 +08:00
parent 7469aa0ebf
commit 435696b86e
12 changed files with 321 additions and 179 deletions

View File

@@ -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": []
}

View File

@@ -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=="],

View File

@@ -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": {

View File

@@ -1,3 +1,4 @@
/// <reference path="./jsx.d.ts" />
import React, { useState } from 'react';
import { createContainer } from './reconciler';

View File

@@ -8,5 +8,20 @@ export type {
BlockQuoteNode,
ButtonNode,
RowNode,
RootNode
} from './reconciler';
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';

140
src/jsx.d.ts vendored Normal file
View File

@@ -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;

View File

@@ -1,3 +1,4 @@
/// <reference path="./jsx.d.ts" />
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');

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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}
<row>
<button onClick={() => {
console.log('Decrease clicked');
setCount(p => p - 1);
}}>Decrease</button>
<button onClick={() => {
console.log('Increase clicked');
setCount(p => p + 1);
}}>Increase</button>
</row>
</>
);
};
console.log('Initial render');
render(<App />);
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);

111
src/typed-example.tsx Normal file
View File

@@ -0,0 +1,111 @@
/// <reference path="./jsx.d.ts" />
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 (
<>
<b>TypeScript Example</b>
{'\n'}
<i>All custom elements are properly typed!</i>
{'\n\n'}
{/* Text formatting */}
<u>Underlined text</u>
{' '}
<s>Strikethrough</s>
{' '}
<code>inline code</code>
{'\n\n'}
{/* Spoiler */}
{showSpoiler ? (
<tg-spoiler>Secret message!</tg-spoiler>
) : (
<span className="tg-spoiler">Hidden content</span>
)}
{'\n\n'}
{/* Links */}
<a href="https://telegram.org">Telegram Website</a>
{' | '}
<a href="tg://user?id=123456">User mention</a>
{'\n\n'}
{/* Emoji */}
<tg-emoji emojiId="5368324170671202286">👍</tg-emoji>
{'\n\n'}
{/* Code block */}
<pre>
<code className="language-typescript">
{`const message = "Hello, Telegram!";
console.log(message);`}
</code>
</pre>
{'\n'}
{/* Blockquote */}
<blockquote expandable>
This is an expandable quote.
It can contain multiple lines.
</blockquote>
{'\n\n'}
{/* Interactive buttons */}
<row>
<button onClick={() => setShowSpoiler(!showSpoiler)}>
{showSpoiler ? 'Hide' : 'Show'} Spoiler
</button>
<button onClick={() => console.log('Clicked!')}>
Log Message
</button>
</row>
</>
);
};
// Create container with proper typing
const { render, container } = createContainer();
// Render the app
render(<TypedApp />);
// 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);

View File

@@ -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"]
}
}