diff --git a/.gitignore b/.gitignore index 2ab142a..d227280 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ node_modules/ dist/ .vercel .output -.vinxi \ No newline at end of file +.vinxi +.cache \ No newline at end of file diff --git a/package.json b/package.json index 35928b8..cb5c7c5 100644 --- a/package.json +++ b/package.json @@ -22,11 +22,17 @@ "@tanstack/react-router": "^1.58.9", "@tanstack/router-devtools": "^1.58.9", "@unocss/reset": "^0.62.4", + "cheerio": "^1.0.0", "clsx": "^2.1.1", + "dayjs": "^1.11.13", + "fast-xml-parser": "^4.5.0", "favicons-scraper": "^1.3.2", + "flat-cache": "^6.1.0", "h3": "^1.12.0", + "iconv-lite": "^0.6.3", "jotai": "^2.10.0", "node-fetch": "^3.3.2", + "ofetch": "^1.4.0", "overlayscrollbars": "^2.10.0", "overlayscrollbars-react": "^0.5.6", "react": "^18.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a98f9b1..cae0642 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,21 +32,39 @@ importers: '@unocss/reset': specifier: ^0.62.4 version: 0.62.4 + cheerio: + specifier: ^1.0.0 + version: 1.0.0 clsx: specifier: ^2.1.1 version: 2.1.1 + dayjs: + specifier: ^1.11.13 + version: 1.11.13 + fast-xml-parser: + specifier: ^4.5.0 + version: 4.5.0 favicons-scraper: specifier: ^1.3.2 version: 1.3.2 + flat-cache: + specifier: ^6.1.0 + version: 6.1.0 h3: specifier: ^1.12.0 version: 1.12.0 + iconv-lite: + specifier: ^0.6.3 + version: 0.6.3 jotai: specifier: ^2.10.0 version: 2.10.0(@types/react@18.3.9)(react@18.3.1) node-fetch: specifier: ^3.3.2 version: 3.3.2 + ofetch: + specifier: ^1.4.0 + version: 1.4.0 overlayscrollbars: specifier: ^2.10.0 version: 2.10.0 @@ -104,7 +122,7 @@ importers: version: 0.4.12(eslint@9.11.1(jiti@1.21.6)) nitropack: specifier: ^2.9.7 - version: 2.9.7(magicast@0.3.5) + version: 2.9.7(encoding@0.1.13)(magicast@0.3.5) tsx: specifier: ^4.19.1 version: 4.19.1 @@ -122,7 +140,7 @@ importers: version: 5.4.8(@types/node@22.6.1)(terser@5.33.0) vite-plugin-with-nitro: specifier: 0.0.0-beta.4 - version: 0.0.0-beta.4(magicast@0.3.5)(vite@5.4.8(@types/node@22.6.1)(terser@5.33.0)) + version: 0.0.0-beta.4(encoding@0.1.13)(magicast@0.3.5)(vite@5.4.8(@types/node@22.6.1)(terser@5.33.0)) vite-tsconfig-paths: specifier: ^5.0.1 version: 5.0.1(typescript@5.6.2)(vite@5.4.8(@types/node@22.6.1)(terser@5.33.0)) @@ -959,6 +977,9 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@keyv/serialize@1.0.1': + resolution: {integrity: sha512-kKXeynfORDGPUEEl2PvTExM2zs+IldC6ZD8jPcfvI351MDNtfMlw9V9s4XZXuJNDK2qR5gbEKxRyoYx3quHUVQ==} + '@mapbox/node-pre-gyp@1.0.11': resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} hasBin: true @@ -1887,6 +1908,9 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + cacheable@1.8.0: + resolution: {integrity: sha512-ewQhhLQ7aUO95MALxgQ/04M8Eh97uCfinTK1pfktsCq+mULf4gNFAg1jSkLi5SDIw/2Md6K6Np3yWjWtYEzvqw==} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -1923,6 +1947,13 @@ packages: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} + cheerio-select@2.1.0: + resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} + + cheerio@1.0.0: + resolution: {integrity: sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==} + engines: {node: '>=18.17'} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -2048,6 +2079,9 @@ packages: css-in-js-utils@3.1.0: resolution: {integrity: sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==} + css-select@5.1.0: + resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + css-tree@1.1.3: resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==} engines: {node: '>=8.0.0'} @@ -2056,6 +2090,10 @@ packages: resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + css-what@6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -2068,6 +2106,9 @@ packages: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} + dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + db0@0.1.4: resolution: {integrity: sha512-Ft6eCwONYxlwLjBXSJxw0t0RYtA5gW9mq8JfBXn9TtC0nDPlqePAhpv9v4g9aONBi6JI1OXHTKKkUYGd+BOrCA==} peerDependencies: @@ -2156,6 +2197,19 @@ packages: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.1.0: + resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + dot-prop@8.0.2: resolution: {integrity: sha512-xaBe6ZT4DHPkg0k4Ytbvn5xoxgpG0jOS1dYxSOwAHPuNLjP3/OzN0gH55SrLqpx8cBfSaVt91lXYkApjb+nYdQ==} engines: {node: '>=16'} @@ -2190,6 +2244,12 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} + encoding-sniffer@0.2.0: + resolution: {integrity: sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==} + + encoding@0.1.13: + resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==} + enhanced-resolve@5.17.1: resolution: {integrity: sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==} engines: {node: '>=10.13.0'} @@ -2549,6 +2609,10 @@ packages: fast-shallow-equal@1.0.0: resolution: {integrity: sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==} + fast-xml-parser@4.5.0: + resolution: {integrity: sha512-/PlTQCI96+fZMAOLMZK4CWG1ItCbfZ/0jx7UIJFChPNrx7tcEgerUgWbeieCM9MfHInUDyK8DWYZ+YrywDJuTg==} + hasBin: true + fastest-stable-stringify@2.0.2: resolution: {integrity: sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==} @@ -2597,6 +2661,9 @@ packages: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} + flat-cache@6.1.0: + resolution: {integrity: sha512-txr9o9zCbXvMOWZ1wZAID4U+CnYxdkSFqxxvd8Bs/a0ZidSs8V8diyv0CviKkPYEWHtbFJloF/QQO2WnkarnlA==} + flatted@3.3.1: resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} @@ -2748,9 +2815,16 @@ packages: hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + hookified@1.2.0: + resolution: {integrity: sha512-32q0JrtZRAdCGeEa8IcBz4iABsrP4UiDrbFj/WADL1VyNHRvHdGs8MrYvaIZWMzbff1xdS3Tw8kBt6YvcSL7jQ==} + engines: {node: '>=20'} + hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + htmlparser2@9.1.0: + resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} + http-errors@2.0.0: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} @@ -2773,6 +2847,10 @@ packages: hyphenate-style-name@1.1.0: resolution: {integrity: sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==} + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -2997,6 +3075,9 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + keyv@5.0.3: + resolution: {integrity: sha512-WmefGWaWkWiWDkIasfHxpWmM1lych/LPtRmNj8jnIQVGLsAgFw73Vg9utZ7ss97/JwRlERABb/fSejTPY4hlZQ==} + klona@2.0.6: resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} engines: {node: '>= 8'} @@ -3351,6 +3432,15 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parse5-htmlparser2-tree-adapter@7.0.0: + resolution: {integrity: sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==} + + parse5-parser-stream@7.1.2: + resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==} + + parse5@7.1.2: + resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -3602,6 +3692,9 @@ packages: safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} @@ -3800,6 +3893,9 @@ packages: strip-literal@2.1.0: resolution: {integrity: sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==} + strnum@1.0.5: + resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} + stylis@4.3.4: resolution: {integrity: sha512-osIBl6BGUmSfDkyH2mB7EFvCJntXDrLhKjHTRj/rK6xLH0yuPrHULDRQzKokSOD4VoorhtKpfcfW1GAntu8now==} @@ -3999,6 +4095,10 @@ packages: resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} engines: {node: '>=14.0'} + undici@6.19.8: + resolution: {integrity: sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==} + engines: {node: '>=18.17'} + unenv@1.10.0: resolution: {integrity: sha512-wY5bskBQFL9n3Eca5XnhH6KbUo/tfvkwm9OpcdCvLaeA7piBNbavbOKJySEwQ1V0RH6HvNlSAFRTpvTqgKRQXQ==} @@ -4205,6 +4305,14 @@ packages: webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -4963,12 +5071,16 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 - '@mapbox/node-pre-gyp@1.0.11': + '@keyv/serialize@1.0.1': + dependencies: + buffer: 6.0.3 + + '@mapbox/node-pre-gyp@1.0.11(encoding@0.1.13)': dependencies: detect-libc: 2.0.3 https-proxy-agent: 5.0.1 make-dir: 3.1.0 - node-fetch: 2.7.0 + node-fetch: 2.7.0(encoding@0.1.13) nopt: 5.0.0 npmlog: 5.0.1 rimraf: 3.0.2 @@ -5686,9 +5798,9 @@ snapshots: - rollup - supports-color - '@vercel/nft@0.26.5': + '@vercel/nft@0.26.5(encoding@0.1.13)': dependencies: - '@mapbox/node-pre-gyp': 1.0.11 + '@mapbox/node-pre-gyp': 1.0.11(encoding@0.1.13) '@rollup/pluginutils': 4.2.1 acorn: 8.12.1 acorn-import-attributes: 1.9.5(acorn@8.12.1) @@ -5967,6 +6079,11 @@ snapshots: cac@6.7.14: {} + cacheable@1.8.0: + dependencies: + hookified: 1.2.0 + keyv: 5.0.3 + callsites@3.1.0: {} caniuse-lite@1.0.30001663: {} @@ -6000,6 +6117,29 @@ snapshots: check-error@2.1.1: {} + cheerio-select@2.1.0: + dependencies: + boolbase: 1.0.0 + css-select: 5.1.0 + css-what: 6.1.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + + cheerio@1.0.0: + dependencies: + cheerio-select: 2.1.0 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + domutils: 3.1.0 + encoding-sniffer: 0.2.0 + htmlparser2: 9.1.0 + parse5: 7.1.2 + parse5-htmlparser2-tree-adapter: 7.0.0 + parse5-parser-stream: 7.1.2 + undici: 6.19.8 + whatwg-mimetype: 4.0.0 + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -6113,6 +6253,14 @@ snapshots: dependencies: hyphenate-style-name: 1.1.0 + css-select@5.1.0: + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 5.0.3 + domutils: 3.1.0 + nth-check: 2.1.1 + css-tree@1.1.3: dependencies: mdn-data: 2.0.14 @@ -6123,12 +6271,16 @@ snapshots: mdn-data: 2.0.30 source-map-js: 1.2.1 + css-what@6.1.0: {} + cssesc@3.0.0: {} csstype@3.1.3: {} data-uri-to-buffer@4.0.1: {} + dayjs@1.11.13: {} + db0@0.1.4: {} debug@2.6.9: @@ -6171,6 +6323,24 @@ snapshots: dependencies: esutils: 2.0.3 + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.1.0: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + dot-prop@8.0.2: dependencies: type-fest: 3.13.1 @@ -6193,6 +6363,16 @@ snapshots: encodeurl@2.0.0: {} + encoding-sniffer@0.2.0: + dependencies: + iconv-lite: 0.6.3 + whatwg-encoding: 3.1.1 + + encoding@0.1.13: + dependencies: + iconv-lite: 0.6.3 + optional: true + enhanced-resolve@5.17.1: dependencies: graceful-fs: 4.2.11 @@ -6780,6 +6960,10 @@ snapshots: fast-shallow-equal@1.0.0: {} + fast-xml-parser@4.5.0: + dependencies: + strnum: 1.0.5 + fastest-stable-stringify@2.0.2: {} fastq@1.17.1: @@ -6824,6 +7008,12 @@ snapshots: flatted: 3.3.1 keyv: 4.5.4 + flat-cache@6.1.0: + dependencies: + cacheable: 1.8.0 + flatted: 3.3.1 + hookified: 1.2.0 + flatted@3.3.1: {} foreground-child@3.3.0: @@ -6993,8 +7183,17 @@ snapshots: hookable@5.5.3: {} + hookified@1.2.0: {} + hosted-git-info@2.8.9: {} + htmlparser2@9.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + entities: 4.5.0 + http-errors@2.0.0: dependencies: depd: 2.0.0 @@ -7018,6 +7217,10 @@ snapshots: hyphenate-style-name@1.1.0: {} + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -7216,6 +7419,10 @@ snapshots: dependencies: json-buffer: 3.0.1 + keyv@5.0.3: + dependencies: + '@keyv/serialize': 1.0.1 + klona@2.0.6: {} knitwork@1.1.0: {} @@ -7415,7 +7622,7 @@ snapshots: natural-compare@1.4.0: {} - nitropack@2.9.7(magicast@0.3.5): + nitropack@2.9.7(encoding@0.1.13)(magicast@0.3.5): dependencies: '@cloudflare/kv-asset-handler': 0.3.4 '@netlify/functions': 2.8.1 @@ -7428,7 +7635,7 @@ snapshots: '@rollup/plugin-terser': 0.4.4(rollup@4.22.4) '@rollup/pluginutils': 5.1.2(rollup@4.22.4) '@types/http-proxy': 1.17.15 - '@vercel/nft': 0.26.5 + '@vercel/nft': 0.26.5(encoding@0.1.13) archiver: 7.0.1 c12: 1.11.2(magicast@0.3.5) chalk: 5.3.0 @@ -7511,9 +7718,11 @@ snapshots: node-fetch-native@1.6.4: {} - node-fetch@2.7.0: + node-fetch@2.7.0(encoding@0.1.13): dependencies: whatwg-url: 5.0.0 + optionalDependencies: + encoding: 0.1.13 node-fetch@3.3.2: dependencies: @@ -7666,6 +7875,19 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parse5-htmlparser2-tree-adapter@7.0.0: + dependencies: + domhandler: 5.0.3 + parse5: 7.1.2 + + parse5-parser-stream@7.1.2: + dependencies: + parse5: 7.1.2 + + parse5@7.1.2: + dependencies: + entities: 4.5.0 + parseurl@1.3.3: {} path-exists@4.0.0: {} @@ -7918,6 +8140,8 @@ snapshots: safe-buffer@5.2.1: {} + safer-buffer@2.1.2: {} + scheduler@0.23.2: dependencies: loose-envify: 1.4.0 @@ -8118,6 +8342,8 @@ snapshots: dependencies: js-tokens: 9.0.0 + strnum@1.0.5: {} + stylis@4.3.4: {} supports-color@5.5.0: @@ -8290,6 +8516,8 @@ snapshots: dependencies: '@fastify/busboy': 2.1.1 + undici@6.19.8: {} + unenv@1.10.0: dependencies: consola: 3.2.3 @@ -8432,13 +8660,13 @@ snapshots: - supports-color - terser - vite-plugin-with-nitro@0.0.0-beta.4(magicast@0.3.5)(vite@5.4.8(@types/node@22.6.1)(terser@5.33.0)): + vite-plugin-with-nitro@0.0.0-beta.4(encoding@0.1.13)(magicast@0.3.5)(vite@5.4.8(@types/node@22.6.1)(terser@5.33.0)): dependencies: esbuild: 0.24.0 fast-glob: 3.3.2 front-matter: 4.0.2 h3: 1.12.0 - nitropack: 2.9.7(magicast@0.3.5) + nitropack: 2.9.7(encoding@0.1.13)(magicast@0.3.5) vite: 5.4.8(@types/node@22.6.1)(terser@5.33.0) xmlbuilder2: 3.1.1 transitivePeerDependencies: @@ -8538,6 +8766,12 @@ snapshots: webpack-virtual-modules@0.6.2: {} + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 diff --git a/server/cache.ts b/server/cache.ts new file mode 100644 index 0000000..629b1d0 --- /dev/null +++ b/server/cache.ts @@ -0,0 +1,9 @@ +import { FlatCache } from "flat-cache" + +// init +export const cache = new FlatCache({ + ttl: 60 * 60 * 1000, // 1 hour + lruSize: 10000, // 10,000 items + expirationInterval: 5 * 1000 * 60, // 5 minutes + persistInterval: 5 * 1000 * 60, // 5 minutes +}) diff --git a/server/routes/[id].ts b/server/routes/[id].ts index 306e555..1f7f8ad 100644 --- a/server/routes/[id].ts +++ b/server/routes/[id].ts @@ -1,10 +1,24 @@ -import { defineEventHandler, getQuery, getRouterParam, sendProxy } from "h3" +import { defineEventHandler, getRouterParam } from "h3" +import { fallback, sources } from "#/sources" +// import { cache } from "#/cache" export default defineEventHandler(async (event) => { - const id = getRouterParam(event, "id") - const { latest } = getQuery(event) - // https://api-hot.efefee.cn/weibo?cache=false - // https://smzdk.top/api/${id}/new - if (latest !== undefined) return await sendProxy(event, `https://api-hot.efefee.cn/${id}?cache=false`) - return await sendProxy(event, `https://api-hot.efefee.cn/${id}?cache=true`) + const id = getRouterParam(event, "id") as keyof typeof sources + // const { latest } = getQuery(event) + // console.log(id, latest) + if (!id) throw new Error("Invalid source id") + // if (!latest) { + // const _ = cache.get(id) + // if (_) return _ + // } + + if (!sources[id]) { + const _ = await fallback(id) + // cache.set(id, _) + return _ + } else { + const _ = await sources[id]() + // cache.set(id, _) + return _ + } }) diff --git a/server/sources/fallback.ts b/server/sources/fallback.ts new file mode 100644 index 0000000..491db2d --- /dev/null +++ b/server/sources/fallback.ts @@ -0,0 +1,41 @@ +import type { OResponse } from "@shared/types" +import { $fetch } from "ofetch" + +export interface Res { + code: number + message: string + name: string + title: string + subtitle: string + total: number + updateTime: string + data: { + title: string + desc: string + time?: string + url: string + mobileUrl: string + }[] +} + +export async function fallback(id: string): Promise { + const res: Res = await $fetch(`https://smzdk.top/api/${id}/new`) + if (res.code !== 200 || !res.data) throw new Error(res.message) + return { + status: "success", + data: { + name: res.title, + type: res.subtitle, + updateTime: res.updateTime, + items: res.data.map(item => ({ + extra: { + date: item.time, + }, + id: item.url, + title: item.title, + url: item.url, + mobileUrl: item.mobileUrl, + })), + }, + } +} diff --git a/server/sources/index.ts b/server/sources/index.ts index e69de29..fedb83d 100644 --- a/server/sources/index.ts +++ b/server/sources/index.ts @@ -0,0 +1,11 @@ +import { peopledaily } from "./peopledaily" +import { weibo } from "./weibo" +import { zaobao } from "./zaobao" + +export { fallback } from "./fallback" + +export const sources = { + peopledaily, + weibo, + zaobao: () => zaobao("中国聚焦"), +} diff --git a/server/sources/peopledaily.ts b/server/sources/peopledaily.ts new file mode 100644 index 0000000..ff9ce4c --- /dev/null +++ b/server/sources/peopledaily.ts @@ -0,0 +1,20 @@ +import type { OResponse, RSS2JSON } from "@shared/types" +import { rss2json } from "#/utils/rss2json" + +export async function peopledaily(): Promise { + const source = await rss2json("https://feedx.net/rss/people.xml") + if (!source?.items.length) throw new Error("Cannot fetch data") + return { + status: "success", + data: { + name: "人民日报", + type: "报纸", + updateTime: Date.now(), + items: source.items.slice(0, 30).map((item: RSS2JSON) => ({ + title: item.title, + url: item.link, + id: item.link, + })), + }, + } +} diff --git a/server/sources/weibo.ts b/server/sources/weibo.ts index e69de29..7a0fec2 100644 --- a/server/sources/weibo.ts +++ b/server/sources/weibo.ts @@ -0,0 +1,54 @@ +import type { OResponse } from "@shared/types" +import { $fetch } from "ofetch" + +interface Res { + ok: number // 1 is ok + data: { + realtime: + { + num: number // 看上去是个 id + emoticon: string + icon?: string // 热,新 icon url + icon_width: number + icon_height: number + note: string + small_icon_desc: string + icon_desc?: string // 如果是 荐 ,就是广告 + topic_flag: number + icon_desc_color: string + flag: number + word_scheme: string + small_icon_desc_color: string + realpos: number + label_name: string + word: string // 热搜词 + rank: number + }[] + } +} + +export async function weibo(): Promise { + const url = "https://weibo.com/ajax/side/hotSearch" + const res: Res = await $fetch(url) + if (!res.ok || res.data.realtime.length === 0) throw new Error("Cannot fetch data") + return { + status: "success", + data: { + name: "微博热搜", + updateTime: Date.now(), + type: "热搜", + items: res.data.realtime.filter(k => !k.icon_desc || k.icon_desc !== "荐").map((k) => { + const keyword = k.word_scheme ? k.word_scheme : `#${k.word}#` + return { + id: k.num, + title: k.word, + extra: { + icon: k.icon, + }, + url: `https://s.weibo.com/weibo?q=${encodeURIComponent(keyword)}`, + mobileUrl: `https://m.weibo.cn/search?containerid=231522type%3D1%26q%3D${encodeURIComponent(keyword)}&_T_WM=16922097837&v_p=42`, + } + }), + }, + } +} diff --git a/server/sources/zaobao.ts b/server/sources/zaobao.ts new file mode 100644 index 0000000..69591ae --- /dev/null +++ b/server/sources/zaobao.ts @@ -0,0 +1,54 @@ +import { Buffer } from "node:buffer" +import { $fetch } from "ofetch" +import * as cheerio from "cheerio" +import iconv from "iconv-lite" +import type { NewsItem, OResponse } from "@shared/types" +import { tranformToUTC } from "#/utils/date" + +const columns = [ + "人物记事", + "观点评论", + "中国聚焦", + "香港澳门", + "台湾新闻", + "国际时政", + "国际军事", + "国际视野", +] as const +// type: "中国聚焦" | "人物记事" | "观点评论" +export async function zaobao(type: typeof columns[number] = "中国聚焦"): Promise { + const response = await $fetch("https://www.kzaobao.com/top.html", { + responseType: "arrayBuffer", + }) + const base = "https://www.kzaobao.com" + const utf8String = iconv.decode(Buffer.from(response), "gb2312") + const $ = cheerio.load(utf8String) + const $main = $(`#cd0${columns.indexOf(type) + 1}`) + const news: NewsItem[] = [] + $main.find("tr").each((_, el) => { + const a = $(el).find("h3>a") + // https://www.kzaobao.com/shiju/20241002/170659.html + const url = a.attr("href") + const title = a.text() + if (url && title) { + const date = $(el).find("td:nth-child(3)").text() + news.push({ + url: base + url, + title, + id: url, + extra: { + date: date && tranformToUTC(date), + }, + }) + } + }) + return { + status: "success", + data: { + name: `联合早报`, + type, + updateTime: Date.now(), + items: news, + }, + } +} diff --git a/server/utils/date.ts b/server/utils/date.ts new file mode 100644 index 0000000..b010fdf --- /dev/null +++ b/server/utils/date.ts @@ -0,0 +1,14 @@ +import dayjs from "dayjs" +import utcPlugin from "dayjs/plugin/utc" +import timezonePlugin from "dayjs/plugin/timezone" + +dayjs.extend(utcPlugin) +dayjs.extend(timezonePlugin) + +/** + * 传入任意时区的时间(不携带时区),转换为 UTC 时间 + */ +export function tranformToUTC(date: string, format?: string, timezone: string = "Asia/Shanghai"): number { + if (!format) return dayjs.tz(date, timezone).valueOf() + return dayjs.tz(date, format, timezone).valueOf() +} diff --git a/server/utils/index.ts b/server/utils/index.ts new file mode 100644 index 0000000..5ff5138 --- /dev/null +++ b/server/utils/index.ts @@ -0,0 +1 @@ +import type { SourceInfo } from "@shared/types" diff --git a/server/utils/rss2json.ts b/server/utils/rss2json.ts new file mode 100644 index 0000000..d598345 --- /dev/null +++ b/server/utils/rss2json.ts @@ -0,0 +1,81 @@ +import { XMLParser } from "fast-xml-parser" +import { $fetch } from "ofetch" + +export async function rss2json(url: string) { + if (!/^https?:\/\/[^\s$.?#].\S*/i.test(url)) return null + + const data = await $fetch(url) + + const xml = new XMLParser({ + attributeNamePrefix: "", + textNodeName: "$text", + ignoreAttributes: false, + }) + + const result = xml.parse(data as string) + + let channel = result.rss && result.rss.channel ? result.rss.channel : result.feed + if (Array.isArray(channel)) channel = channel[0] + + const rss = { + title: channel.title ?? "", + description: channel.description ?? "", + link: channel.link && channel.link.href ? channel.link.href : channel.link, + image: channel.image ? channel.image.url : channel["itunes:image"] ? channel["itunes:image"].href : "", + category: channel.category || [], + items: [], + } + + let items = channel.item || channel.entry || [] + if (items && !Array.isArray(items)) items = [items] + + for (let i = 0; i < items.length; i++) { + const val = items[i] + const media = {} + + const obj = { + id: val.guid && val.guid.$text ? val.guid.$text : val.id, + title: val.title && val.title.$text ? val.title.$text : val.title, + description: val.summary && val.summary.$text ? val.summary.$text : val.description, + link: val.link && val.link.href ? val.link.href : val.link, + author: val.author && val.author.name ? val.author.name : val["dc:creator"], + published: val.created ? Date.parse(val.created) : val.pubDate ? Date.parse(val.pubDate) : Date.now(), + created: val.updated ? Date.parse(val.updated) : val.pubDate ? Date.parse(val.pubDate) : val.created ? Date.parse(val.created) : Date.now(), + category: val.category || [], + content: val.content && val.content.$text ? val.content.$text : val["content:encoded"], + enclosures: val.enclosure ? (Array.isArray(val.enclosure) ? val.enclosure : [val.enclosure]) : [], + }; + + ["content:encoded", "podcast:transcript", "itunes:summary", "itunes:author", "itunes:explicit", "itunes:duration", "itunes:season", "itunes:episode", "itunes:episodeType", "itunes:image"].forEach((s) => { + // @ts-expect-error TODO + if (val[s]) obj[s.replace(":", "_")] = val[s] + }) + + if (val["media:thumbnail"]) { + Object.assign(media, { thumbnail: val["media:thumbnail"] }) + obj.enclosures.push(val["media:thumbnail"]) + } + + if (val["media:content"]) { + Object.assign(media, { thumbnail: val["media:content"] }) + obj.enclosures.push(val["media:content"]) + } + + if (val["media:group"]) { + if (val["media:group"]["media:title"]) obj.title = val["media:group"]["media:title"] + + if (val["media:group"]["media:description"]) obj.description = val["media:group"]["media:description"] + + if (val["media:group"]["media:thumbnail"]) obj.enclosures.push(val["media:group"]["media:thumbnail"].url) + + if (val["media:group"]["media:content"]) obj.enclosures.push(val["media:group"]["media:content"]) + } + + Object.assign(obj, { media }) + + // @ts-expect-error TODO + rss.items.push(obj) + } + + return rss +} diff --git a/shared/types.ts b/shared/types.ts index 53666db..86bfdfb 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -10,24 +10,34 @@ export interface Section { } export interface NewsItem { + id: string | number // unique title: string - cover?: string - author?: string - desc?: string url: string - timestamp?: number mobileUrl?: string + extra?: Record } // 路由数据 export interface SourceInfo { name: string - title: string type: string - description?: string - params?: Record - total: number - link?: string - updateTime: string - data: NewsItem[] + updateTime: number | string + items: NewsItem[] +} + +export type OResponse = { + status: "success" | "cache" + data: SourceInfo +} | { + status: "error" + message?: string +} + +export interface RSS2JSON { + id?: string + title: string + description: string + link: string + published: number + created: number } diff --git a/src/components/section/Card.tsx b/src/components/section/Card.tsx index ea15667..5bf6e52 100644 --- a/src/components/section/Card.tsx +++ b/src/components/section/Card.tsx @@ -1,4 +1,4 @@ -import type { SourceID, SourceInfo } from "@shared/types" +import type { OResponse, SourceID, SourceInfo } from "@shared/types" import { OverlayScrollbarsComponent } from "overlayscrollbars-react" import type { UseQueryResult } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query" @@ -6,7 +6,7 @@ import { relativeTime } from "@shared/utils" import clsx from "clsx" import { useInView } from "react-intersection-observer" import { useAtom } from "jotai" -import { forwardRef, useCallback, useImperativeHandle, useRef } from "react" +import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef } from "react" import { sources } from "@shared/data" import type { SyntheticListenerMap } from "@dnd-kit/core/dist/hooks/utilities" import { focusSourcesAtom, refetchSourceAtom } from "~/atoms" @@ -45,7 +45,7 @@ export const CardWrapper = forwardRef(({ id, isDragg
res.json()) + if (response.status === "error") { + throw new Error(response.message) + } else { + return response.data as SourceInfo + } }, // refetch 时显示原有的数据 placeholderData: prev => prev, @@ -125,7 +129,7 @@ export function NewsCard({ id, inView, isOverlay, handleListeners }: NewsCardPro className={clsx("i-ph:arrow-clockwise", query.isFetching && "animate-spin")} onClick={manualRefetch} /> -
@@ -154,7 +158,7 @@ function Num({ num }: { num: number }) { } function NewsList({ query }: Query) { - const items = query.data?.data + const items = query.data?.items if (items?.length) { return ( <> @@ -162,12 +166,12 @@ function NewsList({ query }: Query) {
- + {item.title} - {item.timestamp && ( + {item?.extra?.date && ( - {relativeTime(item.timestamp)} + {relativeTime(item.extra.date)} )} diff --git a/test/common.test.ts b/test/common.test.ts new file mode 100644 index 0000000..9516a30 --- /dev/null +++ b/test/common.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from "vitest" +import { tranformToUTC } from "#/utils/date" + +describe("transform Beijing time to UTC in different timezone", () => { + const a = "2024/10/3 12:26:16" + const b = 1727929576000 + it("in UTC", () => { + Object.assign(process.env, { TZ: "UTC" }) + const date = tranformToUTC(a) + expect(date).toBe(b) + }) + + it("in Beijing", () => { + Object.assign(process.env, { TZ: "Asia/Shanghai" }) + const date = tranformToUTC(a) + expect(date).toBe(b) + }) + + it("in New York", () => { + Object.assign(process.env, { TZ: "America/New_York" }) + const date = tranformToUTC(a) + expect(date).toBe(b) + }) +}) diff --git a/test/data.test.ts b/test/data.test.ts deleted file mode 100644 index 454ae50..0000000 --- a/test/data.test.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { metadata } from "@shared/data" -import { describe, expect, it } from "vitest" - -// 通过两次 diff 来找出数组的差异 -describe("data", () => { - it.for(Object.entries(metadata))(` "%s" source list shoule be unique`, ([, { sourceList }]) => { - if (sourceList) { - expect(new Set(sourceList).size).toBe(sourceList.length) - } - }) -}) diff --git a/test/rss2json.test.ts b/test/rss2json.test.ts new file mode 100644 index 0000000..7058483 --- /dev/null +++ b/test/rss2json.test.ts @@ -0,0 +1,9 @@ +import { it } from "vitest" +import { zaobao } from "#/sources/zaobao" + +it.skip("res", { + timeout: 10000, +}, async () => { + const res = await zaobao() + console.log(res) +}) diff --git a/tsconfig.node.json b/tsconfig.node.json index f52222d..91f4b46 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -1,5 +1,5 @@ { - "extends": ["./tsconfig.base.json", "./dist/.nitro/types/tsconfig.json"], + "extends": ["./tsconfig.base.json"], "compilerOptions": { "lib": ["ES2020"], "baseUrl": ".", diff --git a/vite.config.ts b/vite.config.ts index 55a1535..88d29d8 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -22,14 +22,11 @@ export default defineConfig({ srcDir: "server", alias: { "@shared": fileURLToPath(new URL("shared", import.meta.url)), + "#": fileURLToPath(new URL("server", import.meta.url)), }, runtimeConfig: { // apiPrefix: "", }, - typescript: { - generateTsConfig: true, - }, - minify: false, preset: process.env.VERCEL ? "vercel-edge" : "node-server", }), ],