From 7469aa0ebfd5ff6a8d473f8cfeee47948cb46215 Mon Sep 17 00:00:00 2001 From: Kyle Fang Date: Mon, 30 Jun 2025 18:41:06 +0800 Subject: [PATCH] inital working version --- .claude/settings.local.json | 13 ++ bun.lock | 207 ++++++++++++++++++++ package.json | 9 +- src/example.tsx | 40 ++++ src/index.ts | 12 ++ src/reconciler.test.tsx | 239 +++++++++++++++++++++++ src/reconciler.ts | 366 ++++++++++++++++++++++++++++++++++++ src/simple-test.ts | 47 +++++ src/state-test.tsx | 50 +++++ vitest.config.ts | 7 + 10 files changed, 989 insertions(+), 1 deletion(-) create mode 100644 .claude/settings.local.json create mode 100644 src/example.tsx create mode 100644 src/index.ts create mode 100644 src/reconciler.test.tsx create mode 100644 src/reconciler.ts create mode 100644 src/simple-test.ts create mode 100644 src/state-test.tsx create mode 100644 vitest.config.ts diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..84028b3 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,13 @@ +{ + "permissions": { + "allow": [ + "Bash(bun add:*)", + "Bash(bun test:*)", + "Bash(bun run:*)", + "Bash(bun info:*)", + "Bash(npm view:*)", + "Bash(find:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/bun.lock b/bun.lock index ff5f0f7..9fb3d3f 100644 --- a/bun.lock +++ b/bun.lock @@ -3,8 +3,15 @@ "workspaces": { "": { "name": "react-telegram", + "dependencies": { + "react": "^19.1.0", + "react-reconciler": "^0.32.0", + }, "devDependencies": { "@types/bun": "latest", + "@types/react": "^19.1.8", + "@types/react-reconciler": "^0.32.0", + "vitest": "^3.2.4", }, "peerDependencies": { "typescript": "^5", @@ -12,14 +19,214 @@ }, }, "packages": { + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.5", "", { "os": "android", "cpu": "arm" }, "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.5", "", { "os": "android", "cpu": "arm64" }, "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.5", "", { "os": "android", "cpu": "x64" }, "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.5", "", { "os": "linux", "cpu": "arm" }, "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.5", "", { "os": "linux", "cpu": "x64" }, "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.5", "", { "os": "none", "cpu": "arm64" }, "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.5", "", { "os": "none", "cpu": "x64" }, "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.5", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.5", "", { "os": "win32", "cpu": "x64" }, "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.2", "", {}, "sha512-gKYheCylLIedI+CSZoDtGkFV9YEBxRRVcfCH7OfAqh4TyUyRjEE6WVE/aXDXX0p8BIe/QgLcaAoI0220KRRFgg=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.44.1", "", { "os": "android", "cpu": "arm" }, "sha512-JAcBr1+fgqx20m7Fwe1DxPUl/hPkee6jA6Pl7n1v2EFiktAHenTaXl5aIFjUIEsfn9w3HE4gK1lEgNGMzBDs1w=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.44.1", "", { "os": "android", "cpu": "arm64" }, "sha512-RurZetXqTu4p+G0ChbnkwBuAtwAbIwJkycw1n6GvlGlBuS4u5qlr5opix8cBAYFJgaY05TWtM+LaoFggUmbZEQ=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.44.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fM/xPesi7g2M7chk37LOnmnSTHLG/v2ggWqKj3CCA1rMA4mm5KVBT1fNoswbo1JhPuNNZrVwpTvlCVggv8A2zg=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.44.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-gDnWk57urJrkrHQ2WVx9TSVTH7lSlU7E3AFqiko+bgjlh78aJ88/3nycMax52VIVjIm3ObXnDL2H00e/xzoipw=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.44.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wnFQmJ/zPThM5zEGcnDcCJeYJgtSLjh1d//WuHzhf6zT3Md1BvvhJnWoy+HECKu2bMxaIcfWiu3bJgx6z4g2XA=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.44.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-uBmIxoJ4493YATvU2c0upGz87f99e3wop7TJgOA/bXMFd2SvKCI7xkxY/5k50bv7J6dw1SXT4MQBQSLn8Bb/Uw=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.44.1", "", { "os": "linux", "cpu": "arm" }, "sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.44.1", "", { "os": "linux", "cpu": "arm" }, "sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.44.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.44.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g=="], + + "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew=="], + + "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.44.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.44.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.44.1", "", { "os": "linux", "cpu": "x64" }, "sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.44.1", "", { "os": "linux", "cpu": "x64" }, "sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.44.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.44.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-JYA3qvCOLXSsnTR3oiyGws1Dm0YTuxAAeaYGVlGpUsHqloPcFjPg+X0Fj2qODGLNwQOAcCiQmHub/V007kiH5A=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.44.1", "", { "os": "win32", "cpu": "x64" }, "sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug=="], + "@types/bun": ["@types/bun@1.2.17", "", { "dependencies": { "bun-types": "1.2.17" } }, "sha512-l/BYs/JYt+cXA/0+wUhulYJB6a6p//GTPiJ7nV+QHa8iiId4HZmnu/3J/SowP5g0rTiERY2kfGKXEK5Ehltx4Q=="], + "@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/node": ["@types/node@24.0.7", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-YIEUUr4yf8q8oQoXPpSlnvKNVKDQlPMWrmOcgzoduo7kvA2UF0/BwJ/eMKFTiTtkNL17I0M6Xe2tvwFU7be6iw=="], + "@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="], + + "@types/react-reconciler": ["@types/react-reconciler@0.32.0", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-+WHarFkJevhH1s655qeeSEf/yxFST0dVRsmSqUgxG8mMOKqycgYBv2wVpyubBY7MX8KiX5FQ03rNIwrxfm7Bmw=="], + + "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], + + "@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], + + "@vitest/runner": ["@vitest/runner@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="], + + "@vitest/snapshot": ["@vitest/snapshot@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="], + + "@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], + + "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], + + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + "bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="], + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + + "chai": ["chai@5.2.0", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw=="], + + "check-error": ["check-error@2.1.1", "", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + + "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], + + "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + + "esbuild": ["esbuild@0.25.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.5", "@esbuild/android-arm": "0.25.5", "@esbuild/android-arm64": "0.25.5", "@esbuild/android-x64": "0.25.5", "@esbuild/darwin-arm64": "0.25.5", "@esbuild/darwin-x64": "0.25.5", "@esbuild/freebsd-arm64": "0.25.5", "@esbuild/freebsd-x64": "0.25.5", "@esbuild/linux-arm": "0.25.5", "@esbuild/linux-arm64": "0.25.5", "@esbuild/linux-ia32": "0.25.5", "@esbuild/linux-loong64": "0.25.5", "@esbuild/linux-mips64el": "0.25.5", "@esbuild/linux-ppc64": "0.25.5", "@esbuild/linux-riscv64": "0.25.5", "@esbuild/linux-s390x": "0.25.5", "@esbuild/linux-x64": "0.25.5", "@esbuild/netbsd-arm64": "0.25.5", "@esbuild/netbsd-x64": "0.25.5", "@esbuild/openbsd-arm64": "0.25.5", "@esbuild/openbsd-x64": "0.25.5", "@esbuild/sunos-x64": "0.25.5", "@esbuild/win32-arm64": "0.25.5", "@esbuild/win32-ia32": "0.25.5", "@esbuild/win32-x64": "0.25.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "expect-type": ["expect-type@1.2.1", "", {}, "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw=="], + + "fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], + + "loupe": ["loupe@3.1.4", "", {}, "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg=="], + + "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="], + + "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=="], + + "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=="], + + "rollup": ["rollup@4.44.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.44.1", "@rollup/rollup-android-arm64": "4.44.1", "@rollup/rollup-darwin-arm64": "4.44.1", "@rollup/rollup-darwin-x64": "4.44.1", "@rollup/rollup-freebsd-arm64": "4.44.1", "@rollup/rollup-freebsd-x64": "4.44.1", "@rollup/rollup-linux-arm-gnueabihf": "4.44.1", "@rollup/rollup-linux-arm-musleabihf": "4.44.1", "@rollup/rollup-linux-arm64-gnu": "4.44.1", "@rollup/rollup-linux-arm64-musl": "4.44.1", "@rollup/rollup-linux-loongarch64-gnu": "4.44.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.44.1", "@rollup/rollup-linux-riscv64-gnu": "4.44.1", "@rollup/rollup-linux-riscv64-musl": "4.44.1", "@rollup/rollup-linux-s390x-gnu": "4.44.1", "@rollup/rollup-linux-x64-gnu": "4.44.1", "@rollup/rollup-linux-x64-musl": "4.44.1", "@rollup/rollup-win32-arm64-msvc": "4.44.1", "@rollup/rollup-win32-ia32-msvc": "4.44.1", "@rollup/rollup-win32-x64-msvc": "4.44.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg=="], + + "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], + + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="], + + "strip-literal": ["strip-literal@3.0.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA=="], + + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + + "tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="], + + "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], + + "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], + + "tinyspy": ["tinyspy@4.0.3", "", {}, "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A=="], + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], "undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], + + "vite": ["vite@7.0.0", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", "picomatch": "^4.0.2", "postcss": "^8.5.6", "rollup": "^4.40.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-ixXJB1YRgDIw2OszKQS9WxGHKwLdCsbQNkpJN171udl6szi/rIySHL6/Os3s2+oE4P/FLD4dxg4mD7Wust+u5g=="], + + "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], + + "vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], } } diff --git a/package.json b/package.json index 74f6bc5..4992910 100644 --- a/package.json +++ b/package.json @@ -4,9 +4,16 @@ "type": "module", "private": true, "devDependencies": { - "@types/bun": "latest" + "@types/bun": "latest", + "@types/react": "^19.1.8", + "@types/react-reconciler": "^0.32.0", + "vitest": "^3.2.4" }, "peerDependencies": { "typescript": "^5" + }, + "dependencies": { + "react": "^19.1.0", + "react-reconciler": "^0.32.0" } } diff --git a/src/example.tsx b/src/example.tsx new file mode 100644 index 0000000..24335d1 --- /dev/null +++ b/src/example.tsx @@ -0,0 +1,40 @@ +import React, { useState } from 'react'; +import { createContainer } from './reconciler'; + +// Example usage +const App = () => { + const [count, setCount] = useState(0); + + return ( + <> + Welcome to Telegram React! + {'\n'} + Current count: {count} + {'\n'} + + + + + {'\n'} +
+ This is a custom React reconciler that renders to structured data + suitable for Telegram's message format. +
+ + ); +}; + +// Create container and render +const { render, clickButton } = createContainer(); + +console.log('Initial render:'); +render(); + +console.log('\nClicking increase button (1-1):'); +clickButton('1-1'); + +console.log('\nClicking increase button again:'); +clickButton('1-1'); + +console.log('\nClicking decrease button (1-0):'); +clickButton('1-0'); \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..5157f78 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,12 @@ +export { createContainer } from './reconciler'; +export type { + TextNode, + FormattedNode, + LinkNode, + EmojiNode, + CodeBlockNode, + BlockQuoteNode, + ButtonNode, + RowNode, + RootNode +} from './reconciler'; \ No newline at end of file diff --git a/src/reconciler.test.tsx b/src/reconciler.test.tsx new file mode 100644 index 0000000..f483162 --- /dev/null +++ b/src/reconciler.test.tsx @@ -0,0 +1,239 @@ +import { describe, it, expect, vi } from 'vitest'; +import React, { useState } from 'react'; +import { createContainer } from './reconciler'; + +describe('Telegram Reconciler', () => { + it('should render text formatting', async () => { + const { container, render } = createContainer(); + + render( + <> + bold + italic + underline + strikethrough + spoiler + + ); + + // Wait for React to finish rendering + await new Promise(resolve => setTimeout(resolve, 0)); + + expect(container.root.children).toHaveLength(5); + expect(container.root.children[0]).toEqual({ + type: 'formatted', + format: 'bold', + children: [{ type: 'text', content: 'bold' }] + }); + expect(container.root.children[1]).toEqual({ + type: 'formatted', + format: 'italic', + children: [{ type: 'text', content: 'italic' }] + }); + }); + + it('should render nested formatting', async () => { + const { container, render } = createContainer(); + + render( + + bold italic bold bold + + ); + + await new Promise(resolve => setTimeout(resolve, 0)); + + expect(container.root.children).toHaveLength(1); + expect(container.root.children[0]).toEqual({ + type: 'formatted', + format: 'bold', + children: [ + { type: 'text', content: 'bold ' }, + { + type: 'formatted', + format: 'italic', + children: [{ type: 'text', content: 'italic bold' }] + }, + { type: 'text', content: ' bold' } + ] + }); + }); + + it('should render links', async () => { + const { container, render } = createContainer(); + + render( + <> + inline URL + inline mention + + ); + + await new Promise(resolve => setTimeout(resolve, 0)); + + expect(container.root.children).toHaveLength(2); + expect(container.root.children[0]).toEqual({ + type: 'link', + href: 'http://www.example.com/', + children: [{ type: 'text', content: 'inline URL' }] + }); + }); + + it('should render emoji', async () => { + const { container, render } = createContainer(); + + render( + 👍 + ); + + await new Promise(resolve => setTimeout(resolve, 0)); + + expect(container.root.children).toHaveLength(1); + expect(container.root.children[0]).toEqual({ + type: 'emoji', + emojiId: '5368324170671202286', + fallback: '👍' + }); + }); + + it('should render code blocks', async () => { + const { container, render } = createContainer(); + + render( + <> + inline code +
pre-formatted code
+ + ); + + await new Promise(resolve => setTimeout(resolve, 0)); + + expect(container.root.children).toHaveLength(2); + expect(container.root.children[0]).toEqual({ + type: 'formatted', + format: 'code', + children: [{ type: 'text', content: 'inline code' }] + }); + expect(container.root.children[1]).toEqual({ + type: 'codeblock', + content: 'pre-formatted code', + language: undefined + }); + }); + + it('should render blockquotes', async () => { + const { container, render } = createContainer(); + + render( + <> +
Regular quote
+
Expandable quote
+ + ); + + await new Promise(resolve => setTimeout(resolve, 0)); + + expect(container.root.children).toHaveLength(2); + expect(container.root.children[0]).toEqual({ + type: 'blockquote', + children: [{ type: 'text', content: 'Regular quote' }], + expandable: undefined + }); + expect(container.root.children[1]).toEqual({ + type: 'blockquote', + children: [{ type: 'text', content: 'Expandable quote' }], + expandable: true + }); + }); + + it('should render buttons with IDs based on position', async () => { + const { container, render } = createContainer(); + const onClick1 = vi.fn(); + const onClick2 = vi.fn(); + + render( + + + + + ); + + await new Promise(resolve => setTimeout(resolve, 0)); + + 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'); + }); + + it('should handle button clicks', async () => { + const { render, clickButton } = createContainer(); + const onClick = vi.fn(); + + render( + + + + ); + + await new Promise(resolve => setTimeout(resolve, 0)); + + clickButton('0-0'); + expect(onClick).toHaveBeenCalledTimes(1); + }); + + it('should work with React state', async () => { + const { container, render, clickButton } = createContainer(); + + const App = () => { + const [count, setCount] = useState(0); + return ( + <> + count {count} + + + + + + ); + }; + + render(); + + await new Promise(resolve => setTimeout(resolve, 0)); + + // Initial state - React creates separate text nodes + expect(container.root.children[0]).toEqual({ + type: 'text', + content: 'count ' + }); + expect(container.root.children[1]).toEqual({ + type: 'text', + content: '0' + }); + + // The row is the third child (index 2) + expect(container.root.children[2].type).toBe('row'); + + // Click increase button + clickButton('0-1'); + await new Promise(resolve => setTimeout(resolve, 0)); + + // After re-render, the text should update + expect(container.root.children[1]).toEqual({ + type: 'text', + content: '1' + }); + + // Click decrease button + clickButton('0-0'); + await new Promise(resolve => setTimeout(resolve, 0)); + + expect(container.root.children[1]).toEqual({ + type: 'text', + content: '0' + }); + }); +}); \ No newline at end of file diff --git a/src/reconciler.ts b/src/reconciler.ts new file mode 100644 index 0000000..150d919 --- /dev/null +++ b/src/reconciler.ts @@ -0,0 +1,366 @@ +import ReactReconciler from 'react-reconciler'; +import { DefaultEventPriority, NoEventPriority } from 'react-reconciler/constants'; + +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; + +interface Container { + root: RootNode; + buttonHandlers: Map void>; +} + +let currentUpdatePriority: number = NoEventPriority; + +const hostConfig: ReactReconciler.HostConfig< + string, // Type + any, // Props + Container, // Container + Node, // Instance + TextNode, // TextInstance + any, // SuspenseInstance + any, // HydratableInstance + any, // PublicInstance + any, // HostContext + any, // UpdatePayload + any, // ChildSet + any, // TimeoutHandle + any // NoTimeout +> = { + supportsMutation: false, + supportsPersistence: true, + + createInstance(type: string, props: any) { + switch (type) { + case 'b': + case 'strong': + return { type: 'formatted', format: 'bold', children: [] }; + case 'i': + case 'em': + return { type: 'formatted', format: 'italic', children: [] }; + case 'u': + case 'ins': + return { type: 'formatted', format: 'underline', children: [] }; + case 's': + case 'strike': + case 'del': + return { type: 'formatted', format: 'strikethrough', children: [] }; + case 'span': + if (props.className === 'tg-spoiler') { + return { type: 'formatted', format: 'spoiler', children: [] }; + } + return { type: 'formatted', format: 'bold', children: [] }; // default + case 'tg-spoiler': + return { type: 'formatted', format: 'spoiler', children: [] }; + case 'a': + return { type: 'link', href: props.href || '', children: [] }; + case 'tg-emoji': + return { type: 'emoji', emojiId: props['emoji-id'] || props.emojiId || '', fallback: props.children }; + case 'code': + return { type: 'formatted', format: 'code', children: [] }; + case 'pre': + return { type: 'codeblock', content: '', language: undefined }; + case 'blockquote': + return { type: 'blockquote', children: [], expandable: props.expandable }; + case 'button': + const buttonText = typeof props.children === 'string' ? props.children : ''; + return { type: 'button', id: '', text: buttonText, onClick: props.onClick }; + case 'row': + return { type: 'row', children: [] }; + default: + return { type: 'formatted', format: 'bold', children: [] }; + } + }, + + createTextInstance(text: string) { + return { type: 'text', content: text }; + }, + + appendInitialChild(parent: any, child: any) { + if ('children' in parent && Array.isArray(parent.children)) { + parent.children.push(child); + } else if (parent.type === 'codeblock' && child.type === 'text') { + parent.content = child.content; + } else if (parent.type === 'codeblock' && child.type === 'formatted' && child.format === 'code') { + // Handle
...
+ if (child.children.length > 0 && child.children[0].type === 'text') { + parent.content = child.children[0].content; + // Extract language from props if available + const codeChild = child as any; + if (codeChild.props?.className?.startsWith('language-')) { + parent.language = codeChild.props.className.replace('language-', ''); + } + } + } + }, + + finalizeInitialChildren() { + return false; + }, + + prepareForCommit() { + return null; + }, + + resetAfterCommit(container: Container) { + // Assign button IDs after commit + let rowIndex = 0; + container.root.children.forEach((child: any) => { + if (child.type === 'row') { + child.children.forEach((button: ButtonNode, buttonIndex: number) => { + if (button.type === 'button') { + button.id = `${rowIndex}-${buttonIndex}`; + } + }); + rowIndex++; + } + }); + + // Store button handlers + container.buttonHandlers.clear(); + container.root.children.forEach((child: any) => { + if (child.type === 'row') { + child.children.forEach((button: ButtonNode) => { + if (button.onClick) { + container.buttonHandlers.set(button.id, button.onClick); + } + }); + } + }); + + // Don't log automatically + }, + + preparePortalMount() {}, + + getRootHostContext() { + return {}; + }, + + getChildHostContext() { + return {}; + }, + + shouldSetTextContent() { + return false; + }, + + // Persistence methods + cloneInstance(instance: any) { + // Deep clone but preserve functions + const clone = JSON.parse(JSON.stringify(instance)); + if (instance.onClick) { + clone.onClick = instance.onClick; + } + // Clear children for containers - they'll be rebuilt + if (clone.children) { + clone.children = []; + } + return clone; + }, + + createContainerChildSet() { + return []; + }, + + appendChildToContainerChildSet(childSet: any[], child: any) { + childSet.push(child); + }, + + finalizeContainerChildren(container: Container, newChildren: any[]) { + container.root.children = newChildren; + hostConfig.resetAfterCommit(container); + }, + + replaceContainerChildren(container: Container, newChildren: any[]) { + container.root.children = newChildren; + hostConfig.resetAfterCommit(container); + }, + + cloneHiddenInstance(instance: any) { + return this.cloneInstance(instance); + }, + + cloneHiddenTextInstance(instance: any) { + return this.cloneInstance(instance); + }, + + getPublicInstance(instance: any) { + return instance; + }, + + prepareUpdate() { + return null; + }, + + shouldDeprioritizeSubtree() { + return false; + }, + + // Persistence child building + appendChild(parent: any, child: any) { + 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 + }, + + // Stubs for mutation mode methods (not used in persistence) + insertBefore: () => {}, + insertInContainerBefore: () => {}, + removeChild: () => {}, + removeChildFromContainer: () => {}, + commitTextUpdate: () => {}, + commitMount: () => {}, + commitUpdate: () => {}, + clearContainer: () => {}, + + // Scheduling + scheduleTimeout: setTimeout, + cancelTimeout: clearTimeout, + noTimeout: -1, + isPrimaryRenderer: true, + warnsIfNotActing: true, + supportsHydration: false, + + // React 19 compatibility + getCurrentEventPriority: () => DefaultEventPriority, + getInstanceFromNode: () => null, + beforeActiveInstanceBlur: () => {}, + afterActiveInstanceBlur: () => {}, + prepareScopeUpdate: () => {}, + getInstanceFromScope: () => null, + detachDeletedInstance: () => {}, + + // Update priority management + setCurrentUpdatePriority: (newPriority: number) => { + currentUpdatePriority = newPriority; + }, + getCurrentUpdatePriority: () => currentUpdatePriority, + resolveUpdatePriority: () => + currentUpdatePriority !== NoEventPriority ? currentUpdatePriority : DefaultEventPriority, + + // Additional React 19 methods + resetFormInstance: () => {}, + shouldAttemptEagerTransition: () => false, + trackSchedulerEvent: () => {}, + resolveEventType: () => null, + resolveEventTimeStamp: () => -1.1, + requestPostPaintCallback: () => {}, + maySuspendCommit: () => false, + preloadInstance: () => true, + startSuspendingCommit: () => {}, + suspendInstance: () => {}, + waitForCommitToBeReady: () => null, + NotPendingTransition: null, + HostTransitionContext: null, + + // Microtask support + supportsMicrotasks: true, + scheduleMicrotask: queueMicrotask, +}; + +export const TelegramReconciler = ReactReconciler(hostConfig); + +export function createContainer() { + const container: Container = { + root: { type: 'root', children: [] }, + buttonHandlers: new Map(), + }; + + const reconcilerContainer = TelegramReconciler.createContainer( + container, + 1, // Use legacy mode for synchronous updates + null, + false, + null, + '', + () => {}, + null + ); + + // Set up required functions for React 19 + if (!TelegramReconciler.injectIntoDevTools) { + TelegramReconciler.injectIntoDevTools = () => {}; + } + + return { + container, + reconcilerContainer, + render: (element: React.ReactElement) => { + TelegramReconciler.updateContainer(element, reconcilerContainer, null, () => {}); + }, + getOutput: () => container.root, + clickButton: (buttonId: string) => { + const handler = container.buttonHandlers.get(buttonId); + if (handler) { + handler(); + } + }, + }; +} \ No newline at end of file diff --git a/src/simple-test.ts b/src/simple-test.ts new file mode 100644 index 0000000..f107b58 --- /dev/null +++ b/src/simple-test.ts @@ -0,0 +1,47 @@ +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 new file mode 100644 index 0000000..6e776e4 --- /dev/null +++ b/src/state-test.tsx @@ -0,0 +1,50 @@ +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/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..7054f48 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + }, +}); \ No newline at end of file