A very simple bookmarking webapp bookmarker.finxol.deno.net/

feat: reuse api routes

finxol.io c69bdb16 ff2cd7ee

verified
+1022 -171
+2 -2
.zed/settings.json
··· 6 6 "!vtsls", 7 7 "!eslint", 8 8 "!typescript-language-server", 9 - "...", 10 - ], 9 + "..." 10 + ] 11 11 }
+22 -3
deno.json
··· 2 2 "tasks": { 3 3 "dev": "deno task dev:api & deno task dev:frontend", 4 4 "dev:frontend": "deno run -A npm:vite", 5 - "dev:api": "deno run --watch --allow-net --allow-read --allow-env server/index.ts", 5 + "dev:api": "deno run --watch --env-file --allow-net --allow-read --allow-env server/index.ts", 6 6 "build": "deno run -A npm:vite build", 7 7 "preview": "deno run -A npm:vite preview", 8 8 "serve": "DENO_ENV=production deno run --allow-net --allow-read --allow-env server/index.ts" 9 9 }, 10 10 "imports": { 11 + "@openauthjs/openauth": "npm:@openauthjs/openauth@^0.4.3", 11 12 "@std/assert": "jsr:@std/assert@1", 13 + "@std/encoding": "jsr:@std/encoding@^1.0.10", 12 14 "@std/http": "jsr:@std/http@1", 15 + "arktype": "npm:arktype@^2.1.29", 13 16 "hono": "npm:hono@^4", 17 + "node-html-parser": "npm:node-html-parser@^7.0.2", 18 + "ofetch": "npm:ofetch@^1.5.1", 14 19 "solid-js": "npm:solid-js@^1.9", 15 20 "solid-js/web": "npm:solid-js@^1.9/web", 21 + "tidy-url": "npm:tidy-url@^1.18.3", 16 22 "vite": "npm:vite@^8.0.0-beta.13", 17 23 "vite-plugin-solid": "npm:vite-plugin-solid@^2" 18 24 }, 25 + "unstable": ["kv"], 19 26 "compilerOptions": { 20 27 "jsx": "react-jsx", 21 28 "jsxImportSource": "solid-js", 22 - "lib": ["dom", "dom.iterable", "esnext", "deno.ns"], 29 + "lib": ["dom", "dom.iterable", "esnext", "deno.ns", "deno.unstable"], 23 30 "paths": { 24 31 "@/*": ["./src/*"], 25 32 "@api/*": ["./server/*"] 26 33 } 27 34 }, 28 - "nodeModulesDir": "auto" 35 + "nodeModulesDir": "auto", 36 + "fmt": { 37 + "useTabs": false, 38 + "lineWidth": 80, 39 + "indentWidth": 4, 40 + "semiColons": false, 41 + "singleQuote": false, 42 + "proseWrap": "always" 43 + }, 44 + "deploy": { 45 + "org": "finxol", 46 + "app": "bookmarker" 47 + } 29 48 }
+171 -1
deno.lock
··· 14 14 "jsr:@std/net@^1.0.6": "1.0.6", 15 15 "jsr:@std/path@^1.1.4": "1.1.4", 16 16 "jsr:@std/streams@^1.0.17": "1.0.17", 17 + "npm:@openauthjs/openauth@~0.4.3": "0.4.3_arctic@2.3.4_hono@4.9.8", 18 + "npm:arktype@^2.1.29": "2.1.29", 17 19 "npm:create-vite@latest": "8.2.0", 18 20 "npm:hono@4": "4.9.8", 21 + "npm:node-html-parser@^7.0.2": "7.0.2", 22 + "npm:ofetch@^1.5.1": "1.5.1", 19 23 "npm:solid-js@^1.9.0": "1.9.9_seroval@1.3.2", 24 + "npm:tidy-url@^1.18.3": "1.18.3", 20 25 "npm:vite-plugin-solid@2": "2.11.9_solid-js@1.9.9__seroval@1.3.2_vite@7.3.1__picomatch@4.0.3_@babel+core@7.29.0", 21 26 "npm:vite@*": "7.3.1_picomatch@4.0.3", 22 27 "npm:vite@^8.0.0-beta.13": "8.0.0-beta.13_picomatch@4.0.3" ··· 77 82 } 78 83 }, 79 84 "npm": { 85 + "@ark/schema@0.56.0": { 86 + "integrity": "sha512-ECg3hox/6Z/nLajxXqNhgPtNdHWC9zNsDyskwO28WinoFEnWow4IsERNz9AnXRhTZJnYIlAJ4uGn3nlLk65vZA==", 87 + "dependencies": [ 88 + "@ark/util" 89 + ] 90 + }, 91 + "@ark/util@0.56.0": { 92 + "integrity": "sha512-BghfRC8b9pNs3vBoDJhcta0/c1J1rsoS1+HgVUreMFPdhz/CRAKReAu57YEllNaSy98rWAdY1gE+gFup7OXpgA==" 93 + }, 80 94 "@babel/code-frame@7.29.0": { 81 95 "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", 82 96 "dependencies": [ ··· 397 411 "@tybys/wasm-util" 398 412 ] 399 413 }, 414 + "@openauthjs/openauth@0.4.3_arctic@2.3.4_hono@4.9.8": { 415 + "integrity": "sha512-RlnjqvHzqcbFVymEwhlUEuac4utA5h4nhSK/i2szZuQmxTIqbGUxZ+nM+avM+VV4Ing+/ZaNLKILoXS3yrkOOw==", 416 + "dependencies": [ 417 + "@standard-schema/spec", 418 + "arctic", 419 + "aws4fetch", 420 + "hono", 421 + "jose" 422 + ] 423 + }, 424 + "@oslojs/asn1@1.0.0": { 425 + "integrity": "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA==", 426 + "dependencies": [ 427 + "@oslojs/binary" 428 + ] 429 + }, 430 + "@oslojs/binary@1.0.0": { 431 + "integrity": "sha512-9RCU6OwXU6p67H4NODbuxv2S3eenuQ4/WFLrsq+K/k682xrznH5EVWA7N4VFk9VYVcbFtKqur5YQQZc0ySGhsQ==" 432 + }, 433 + "@oslojs/crypto@1.0.1": { 434 + "integrity": "sha512-7n08G8nWjAr/Yu3vu9zzrd0L9XnrJfpMioQcvCMxBIiF5orECHe5/3J0jmXRVvgfqMm/+4oxlQ+Sq39COYLcNQ==", 435 + "dependencies": [ 436 + "@oslojs/asn1", 437 + "@oslojs/binary" 438 + ] 439 + }, 440 + "@oslojs/encoding@0.4.1": { 441 + "integrity": "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q==" 442 + }, 443 + "@oslojs/encoding@1.1.0": { 444 + "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==" 445 + }, 446 + "@oslojs/jwt@0.2.0": { 447 + "integrity": "sha512-bLE7BtHrURedCn4Mco3ma9L4Y1GR2SMBuIvjWr7rmQ4/W/4Jy70TIAgZ+0nIlk0xHz1vNP8x8DCns45Sb2XRbg==", 448 + "dependencies": [ 449 + "@oslojs/encoding@0.4.1" 450 + ] 451 + }, 400 452 "@oxc-project/runtime@0.112.0": { 401 453 "integrity": "sha512-4vYtWXMnXM6EaweCxbJ6bISAhkNHeN33SihvuX3wrpqaSJA4ZEoW35i9mSvE74+GDf1yTeVE+aEHA+WBpjDk/g==" 402 454 }, ··· 598 650 "os": ["win32"], 599 651 "cpu": ["x64"] 600 652 }, 653 + "@standard-schema/spec@1.0.0-beta.3": { 654 + "integrity": "sha512-0ifF3BjA1E8SY9C+nUew8RefNOIq0cDlYALPty4rhUm8Rrl6tCM8hBT4bhGhx7I7iXD0uAgt50lgo8dD73ACMw==" 655 + }, 601 656 "@tybys/wasm-util@0.10.1": { 602 657 "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", 603 658 "dependencies": [ ··· 636 691 "@types/estree@1.0.8": { 637 692 "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" 638 693 }, 694 + "arctic@2.3.4": { 695 + "integrity": "sha512-+p30BOWsctZp+CVYCt7oAean/hWGW42sH5LAcRQX56ttEkFJWbzXBhmSpibbzwSJkRrotmsA+oAoJoVsU0f5xA==", 696 + "dependencies": [ 697 + "@oslojs/crypto", 698 + "@oslojs/encoding@1.1.0", 699 + "@oslojs/jwt" 700 + ] 701 + }, 702 + "arkregex@0.0.5": { 703 + "integrity": "sha512-ncYjBdLlh5/QnVsAA8De16Tc9EqmYM7y/WU9j+236KcyYNUXogpz3sC4ATIZYzzLxwI+0sEOaQLEmLmRleaEXw==", 704 + "dependencies": [ 705 + "@ark/util" 706 + ] 707 + }, 708 + "arktype@2.1.29": { 709 + "integrity": "sha512-jyfKk4xIOzvYNayqnD8ZJQqOwcrTOUbIU4293yrzAjA3O1dWh61j71ArMQ6tS/u4pD7vabSPe7nG3RCyoXW6RQ==", 710 + "dependencies": [ 711 + "@ark/schema", 712 + "@ark/util", 713 + "arkregex" 714 + ] 715 + }, 716 + "aws4fetch@1.0.20": { 717 + "integrity": "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g==" 718 + }, 639 719 "babel-plugin-jsx-dom-expressions@0.40.1_@babel+core@7.29.0": { 640 720 "integrity": "sha512-b4iHuirqK7RgaMzB2Lsl7MqrlDgQtVRSSazyrmx7wB3T759ggGjod5Rkok5MfHjQXhR7tRPmdwoeGPqBnW2KfA==", 641 721 "dependencies": [ ··· 663 743 "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", 664 744 "bin": true 665 745 }, 746 + "boolbase@1.0.0": { 747 + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" 748 + }, 666 749 "browserslist@4.28.1": { 667 750 "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", 668 751 "dependencies": [ ··· 684 767 "integrity": "sha512-77BlmjbmiaEaBc1xmBFZ5Izq7nSdFkrs037Jxegk93jv/3AkzNNLxypIbKBVMY7/lAMWynug4ERssvruqxxd1g==", 685 768 "bin": true 686 769 }, 770 + "css-select@5.2.2": { 771 + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", 772 + "dependencies": [ 773 + "boolbase", 774 + "css-what", 775 + "domhandler", 776 + "domutils", 777 + "nth-check" 778 + ] 779 + }, 780 + "css-what@6.2.2": { 781 + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==" 782 + }, 687 783 "csstype@3.2.3": { 688 784 "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==" 689 785 }, ··· 693 789 "ms" 694 790 ] 695 791 }, 792 + "destr@2.0.5": { 793 + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==" 794 + }, 696 795 "detect-libc@2.1.2": { 697 796 "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==" 698 797 }, 798 + "dom-serializer@2.0.0": { 799 + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", 800 + "dependencies": [ 801 + "domelementtype", 802 + "domhandler", 803 + "entities@4.5.0" 804 + ] 805 + }, 806 + "domelementtype@2.3.0": { 807 + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==" 808 + }, 809 + "domhandler@5.0.3": { 810 + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", 811 + "dependencies": [ 812 + "domelementtype" 813 + ] 814 + }, 815 + "domutils@3.2.2": { 816 + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", 817 + "dependencies": [ 818 + "dom-serializer", 819 + "domelementtype", 820 + "domhandler" 821 + ] 822 + }, 699 823 "electron-to-chromium@1.5.283": { 700 824 "integrity": "sha512-3vifjt1HgrGW/h76UEeny+adYApveS9dH2h3p57JYzBSXJIKUJAvtmIytDKjcSCt9xHfrNCFJ7gts6vkhuq++w==" 825 + }, 826 + "entities@4.5.0": { 827 + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" 701 828 }, 702 829 "entities@6.0.1": { 703 830 "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==" ··· 755 882 "gensync@1.0.0-beta.2": { 756 883 "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==" 757 884 }, 885 + "he@1.2.0": { 886 + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", 887 + "bin": true 888 + }, 758 889 "hono@4.9.8": { 759 890 "integrity": "sha512-JW8Bb4RFWD9iOKxg5PbUarBYGM99IcxFl2FPBo2gSJO11jjUDqlP1Bmfyqt8Z/dGhIQ63PMA9LdcLefXyIasyg==" 760 891 }, ··· 763 894 }, 764 895 "is-what@4.1.16": { 765 896 "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==" 897 + }, 898 + "jose@5.9.6": { 899 + "integrity": "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==" 766 900 }, 767 901 "js-tokens@4.0.0": { 768 902 "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" ··· 868 1002 "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", 869 1003 "bin": true 870 1004 }, 1005 + "node-fetch-native@1.6.7": { 1006 + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==" 1007 + }, 1008 + "node-html-parser@7.0.2": { 1009 + "integrity": "sha512-DxodLVh7a6JMkYzWyc8nBX9MaF4M0lLFYkJHlWOiu7+9/I6mwNK9u5TbAMC7qfqDJEPX9OIoWA2A9t4C2l1mUQ==", 1010 + "dependencies": [ 1011 + "css-select", 1012 + "he" 1013 + ] 1014 + }, 871 1015 "node-releases@2.0.27": { 872 1016 "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==" 873 1017 }, 1018 + "nth-check@2.1.1": { 1019 + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", 1020 + "dependencies": [ 1021 + "boolbase" 1022 + ] 1023 + }, 1024 + "ofetch@1.5.1": { 1025 + "integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==", 1026 + "dependencies": [ 1027 + "destr", 1028 + "node-fetch-native", 1029 + "ufo" 1030 + ] 1031 + }, 874 1032 "parse5@7.3.0": { 875 1033 "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", 876 1034 "dependencies": [ 877 - "entities" 1035 + "entities@6.0.1" 878 1036 ] 879 1037 }, 880 1038 "picocolors@1.1.1": { ··· 982 1140 "source-map-js@1.2.1": { 983 1141 "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==" 984 1142 }, 1143 + "tidy-url@1.18.3": { 1144 + "integrity": "sha512-NnbmullqE/3loiDoopNltfLb/v6EhX5GYNUVU4EuZjnDjh/NIBzy5dHnhvH3EfIAnZZZMf3cW0HgNw9E+easyg==" 1145 + }, 985 1146 "tinyglobby@0.2.15_picomatch@4.0.3": { 986 1147 "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", 987 1148 "dependencies": [ ··· 991 1152 }, 992 1153 "tslib@2.8.1": { 993 1154 "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" 1155 + }, 1156 + "ufo@1.6.3": { 1157 + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==" 994 1158 }, 995 1159 "update-browserslist-db@1.2.3_browserslist@4.28.1": { 996 1160 "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", ··· 1064 1228 "workspace": { 1065 1229 "dependencies": [ 1066 1230 "jsr:@std/assert@1", 1231 + "jsr:@std/encoding@^1.0.10", 1067 1232 "jsr:@std/http@1", 1233 + "npm:@openauthjs/openauth@~0.4.3", 1234 + "npm:arktype@^2.1.29", 1068 1235 "npm:hono@4", 1236 + "npm:node-html-parser@^7.0.2", 1237 + "npm:ofetch@^1.5.1", 1069 1238 "npm:solid-js@^1.9.0", 1239 + "npm:tidy-url@^1.18.3", 1070 1240 "npm:vite-plugin-solid@2", 1071 1241 "npm:vite@^8.0.0-beta.13" 1072 1242 ]
+11 -11
index.html
··· 1 - <!doctype html> 1 + <!DOCTYPE html> 2 2 <html lang="en"> 3 - <head> 4 - <meta charset="UTF-8" /> 5 - <link rel="icon" type="image/svg+xml" href="/vite.svg" /> 6 - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 - <title>Bookmarker</title> 8 - </head> 9 - <body> 10 - <div id="root"></div> 11 - <script type="module" src="/src/index.tsx"></script> 12 - </body> 3 + <head> 4 + <meta charset="UTF-8" /> 5 + <link rel="icon" type="image/svg+xml" href="/vite.svg" /> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 + <title>Bookmarker</title> 8 + </head> 9 + <body> 10 + <div id="root"></div> 11 + <script type="module" src="/src/index.tsx"></script> 12 + </body> 13 13 </html>
+37 -1
public/vite.svg
··· 1 - <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFBD4F"></stop><stop offset="100%" stop-color="#FF980E"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg> 1 + <svg 2 + xmlns="http://www.w3.org/2000/svg" 3 + xmlns:xlink="http://www.w3.org/1999/xlink" 4 + aria-hidden="true" 5 + role="img" 6 + class="iconify iconify--logos" 7 + width="31.88" 8 + height="32" 9 + preserveAspectRatio="xMidYMid meet" 10 + viewBox="0 0 256 257" 11 + > 12 + <defs><linearGradient 13 + id="IconifyId1813088fe1fbc01fb466" 14 + x1="-.828%" 15 + x2="57.636%" 16 + y1="7.652%" 17 + y2="78.411%" 18 + ><stop offset="0%" stop-color="#41D1FF"></stop><stop 19 + offset="100%" 20 + stop-color="#BD34FE" 21 + ></stop></linearGradient><linearGradient 22 + id="IconifyId1813088fe1fbc01fb467" 23 + x1="43.376%" 24 + x2="50.316%" 25 + y1="2.242%" 26 + y2="89.03%" 27 + ><stop offset="0%" stop-color="#FFBD4F"></stop><stop 28 + offset="100%" 29 + stop-color="#FF980E" 30 + ></stop></linearGradient></defs><path 31 + fill="url(#IconifyId1813088fe1fbc01fb466)" 32 + d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z" 33 + ></path><path 34 + fill="url(#IconifyId1813088fe1fbc01fb467)" 35 + d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z" 36 + ></path> 37 + </svg>
+58 -57
server/index.ts
··· 1 - import { Hono } from "hono"; 2 - import { cors } from "hono/cors"; 3 - import { logger } from "hono/logger"; 4 - import { serveStatic } from "hono/deno"; 5 - import { showRoutes } from "hono/dev"; 1 + import { Hono } from "hono" 2 + import { secureHeaders } from "hono/secure-headers" 3 + import { trimTrailingSlash } from "hono/trailing-slash" 4 + import { createMiddleware } from "hono/factory" 5 + import { cors } from "hono/cors" 6 + import { logger } from "hono/logger" 7 + import { serveStatic } from "hono/deno" 8 + import { showRoutes } from "hono/dev" 9 + import { unprotectedRoutes } from "./routes/public.ts" 10 + import { Variables } from "./utils/globals.ts" 11 + import bookmarks from "./routes/bookmarks.ts" 12 + import { isAuthenticated } from "./utils/auth.ts" 13 + 14 + const protectedRoutes = new Hono<Variables>() 15 + .use( 16 + createMiddleware(async (c, next) => { 17 + const subject = await isAuthenticated(c) 6 18 7 - const app = new Hono() 8 - // Middleware 9 - .use("*", logger()) 10 - .use("*", cors()) 11 - // API routes 12 - .get("/api/bookmarks", (c) => { 13 - // TODO: Replace with actual database query 14 - return c.json({ 15 - bookmarks: [ 16 - { 17 - id: 1, 18 - title: "Example", 19 - url: "https://example.com", 20 - createdAt: new Date().toISOString(), 21 - }, 22 - ], 23 - }); 24 - }) 25 - .post("/api/bookmarks", async (c) => { 26 - const body = await c.req.json(); 27 - // TODO: Save to database 28 - return c.json({ 29 - message: "Bookmark created", 30 - bookmark: { 31 - id: Date.now(), 32 - ...body, 33 - createdAt: new Date().toISOString(), 34 - }, 35 - }, 201); 36 - }) 37 - .get("/api/bookmarks/:id", (c) => { 38 - const id = c.req.param("id"); 39 - // TODO: Fetch from database 40 - return c.json({ 41 - bookmark: { 42 - id: Number(id), 43 - title: "Example", 44 - url: "https://example.com", 45 - createdAt: new Date().toISOString(), 46 - }, 47 - }); 48 - }) 49 - .delete("/api/bookmarks/:id", (c) => { 50 - const id = c.req.param("id"); 51 - // TODO: Delete from database 52 - return c.json({ message: `Bookmark ${id} deleted` }); 53 - }); 19 + if (!subject) { 20 + return c.json( 21 + { 22 + error: "Unauthorized", 23 + }, 24 + 401, 25 + ) 26 + } 27 + 28 + // Set the subject in the context 29 + c.set("user", subject.properties) 30 + await next() 31 + }), 32 + ) 33 + .get("/auth/me", async (ctx) => { 34 + const user = ctx.get("user") 35 + return await Promise.resolve(ctx.json(user)) 36 + }) 37 + .route("/bookmarks", bookmarks) 38 + 39 + const app = new Hono<Variables>() 40 + // Middleware 41 + .use("*", logger()) 42 + .use("*", cors()) 43 + .basePath("/api/v1") 44 + .use(trimTrailingSlash()) 45 + // TODO(@finxol): fix security headers 46 + .use( 47 + "*", 48 + secureHeaders({ 49 + xFrameOptions: false, 50 + xXssProtection: false, 51 + }), 52 + ) 53 + .route("/", unprotectedRoutes) 54 + .route("/", protectedRoutes) 54 55 55 56 if (Deno.env.get("DENO_DEPLOY")) { 56 - app.use("*", serveStatic({ root: "./dist/" })); 57 + app.use("*", serveStatic({ root: "./dist/" })) 57 58 } 58 59 59 - showRoutes(app, { verbose: true }); 60 + showRoutes(app, { verbose: true }) 60 61 61 62 // Start server when run directly 62 - const port = Number(Deno.env.get("API_PORT")) || 3000; 63 - Deno.serve({ port }, app.fetch); 63 + const port = Number(Deno.env.get("API_PORT")) || 3000 64 + Deno.serve({ port }, app.fetch) 64 65 65 - console.log(`API server running on http://localhost:${port}`); 66 + console.log(`API server running on http://localhost:${port}`)
+221
server/routes/bookmarks.ts
··· 1 + import { Hono } from "hono" 2 + import { kv } from "../utils/kv.ts" 3 + import { Bookmark, BookmarkSchema, URLSchema } from "../utils/bookmarks.ts" 4 + import { TidyURL } from "tidy-url" 5 + import { encodeHex } from "@std/encoding/hex" 6 + import { parse } from "node-html-parser" 7 + import type { Variables } from "../utils/globals.ts" 8 + import { getUserSub } from "../utils/auth.ts" 9 + import { tryCatch } from "../utils/utils.ts" 10 + import { ofetch } from "ofetch" 11 + import { type } from "arktype" 12 + 13 + async function getMeta( 14 + cleanUrl: string, 15 + ): Promise<{ title: string; description: string }> { 16 + const page = await tryCatch(ofetch(cleanUrl)) 17 + if (!page.success) { 18 + console.error("Error fetching URL:", page.error) 19 + return { 20 + title: cleanUrl.split("/").at(-1) || cleanUrl, 21 + description: cleanUrl, 22 + } 23 + } 24 + 25 + const parsedPage = parse(page.value) 26 + 27 + const title = (parsedPage.querySelector("title")?.textContent || 28 + parsedPage.querySelector("meta[property='og:title']")?.getAttribute( 29 + "content", 30 + ) || 31 + parsedPage.querySelector("meta[property='twitter:title']") 32 + ?.getAttribute( 33 + "content", 34 + ) || 35 + cleanUrl).trim() 36 + const description = 37 + (parsedPage.querySelector("meta[name='description']")?.getAttribute( 38 + "content", 39 + ) || 40 + parsedPage.querySelector("meta[property='og:description']") 41 + ?.getAttribute("content") || 42 + parsedPage.querySelector("meta[name='twitter:description']") 43 + ?.getAttribute("content") || 44 + "").trim() 45 + 46 + return { 47 + title, 48 + description, 49 + } 50 + } 51 + 52 + const app = new Hono<Variables>() 53 + .delete("/:id", async (c) => { 54 + const subject = getUserSub(c) 55 + 56 + if (!subject) { 57 + return c.json( 58 + { 59 + message: "User subject missing in context", 60 + }, 61 + 500, 62 + ) 63 + } 64 + 65 + const id = c.req.param("id") 66 + 67 + if (!id) { 68 + return c.json( 69 + { 70 + message: "ID parameter missing in request", 71 + }, 72 + 400, 73 + ) 74 + } 75 + 76 + const key = ["bookmarks", subject.id, id] 77 + await kv.delete(key) 78 + return c.text("Bookmark deleted!") 79 + }) 80 + .delete("/all", async (c) => { 81 + const subject = getUserSub(c) 82 + 83 + if (!subject) { 84 + return c.json( 85 + { 86 + message: "User subject missing in context", 87 + }, 88 + 500, 89 + ) 90 + } 91 + 92 + const it = kv.list({ prefix: ["bookmarks", subject.id] }) 93 + 94 + for await (const item of it) { 95 + await kv.delete(item.key) 96 + } 97 + return c.text("Bookmarks deleted!") 98 + }) 99 + .get("/all", async (c) => { 100 + const subject = getUserSub(c) 101 + 102 + if (!subject) { 103 + return c.json( 104 + { 105 + message: "User subject missing in context", 106 + }, 107 + 500, 108 + ) 109 + } 110 + 111 + const it = kv.list<Bookmark>({ prefix: ["bookmarks", subject.id] }) 112 + 113 + const bookmarks = [] 114 + for await (const item of it) { 115 + bookmarks.push({ 116 + id: item.key.at(-1), 117 + ...item.value, 118 + }) 119 + } 120 + 121 + return c.json(bookmarks) 122 + }) 123 + .post("/add", async (c) => { 124 + const subject = getUserSub(c) 125 + 126 + if (!subject) { 127 + return c.json( 128 + { 129 + message: "User subject missing in context", 130 + }, 131 + 500, 132 + ) 133 + } 134 + 135 + const urlParam = c.req.query("url") 136 + const urlBody = (await c.req.parseBody()).url 137 + const url = urlParam || urlBody 138 + if (!url) { 139 + return c.json({ error: "Missing URL parameter" }, 400) 140 + } 141 + 142 + const urlSchema = URLSchema(url) 143 + if (urlSchema instanceof type.errors) { 144 + return c.json({ error: "Invalid URL" }, 400) 145 + } 146 + 147 + const cleanUrl = TidyURL.clean(urlSchema).url 148 + 149 + const encoder = new TextEncoder() 150 + const data = encoder.encode(cleanUrl) 151 + const hashBuffer = await crypto.subtle.digest("SHA-256", data) 152 + const id = encodeHex(hashBuffer) 153 + 154 + const { title, description } = await getMeta(cleanUrl) 155 + 156 + console.log(title, description) 157 + 158 + const bookmark = BookmarkSchema({ 159 + title: title.length > 100 ? title.substring(0, 97) + "..." : title, 160 + description: description.length > 300 161 + ? description.substring(0, 297) + "..." 162 + : description, 163 + url: cleanUrl, 164 + updatedAt: new Date().toISOString(), 165 + }) 166 + 167 + if (bookmark instanceof type.errors) { 168 + console.error("Error parsing bookmark:", bookmark) 169 + return c.json({ error: "Invalid bookmark" }, 400) 170 + } 171 + 172 + await kv.set(["bookmarks", subject.id, id], bookmark) 173 + 174 + return c.json({ id }) 175 + }) 176 + .post("/import", async (c) => { 177 + const subject = getUserSub(c) 178 + 179 + if (!subject) { 180 + return c.json( 181 + { 182 + message: "User subject missing in context", 183 + }, 184 + 500, 185 + ) 186 + } 187 + 188 + const body = await c.req.json() 189 + const bookmarks = BookmarkSchema.array()(body.bookmarks) 190 + 191 + if (bookmarks instanceof type.errors) { 192 + console.error(bookmarks) 193 + return c.json({ error: "Error in Bookmark Schema" }, 400) 194 + } 195 + 196 + const promises = bookmarks.map(async (bookmark) => { 197 + const encoder = new TextEncoder() 198 + const data = encoder.encode(bookmark.url) 199 + const hashBuffer = await crypto.subtle.digest("SHA-256", data) 200 + const id = encodeHex(hashBuffer) 201 + 202 + const bookmarkData = BookmarkSchema({ 203 + ...bookmark, 204 + updatedAt: new Date().toISOString(), 205 + }) 206 + 207 + if (bookmarkData instanceof type.errors) { 208 + console.error("Error parsing bookmark:", bookmarkData.summary) 209 + return Promise.resolve({ error: "Invalid bookmark" }) 210 + } 211 + 212 + await kv.set(["bookmarks", subject.id, id], bookmarkData) 213 + 214 + return { id } 215 + }) 216 + 217 + const results = await Promise.allSettled(promises) 218 + return c.json(results) 219 + }) 220 + 221 + export default app
+151
server/routes/public.ts
··· 1 + import { Context, Hono } from "hono" 2 + import type { 3 + ExchangeError, 4 + ExchangeSuccess, 5 + } from "@openauthjs/openauth/client" 6 + import { client, setTokens } from "../utils/auth.ts" 7 + 8 + function getCallbackUrl(ctx: Context) { 9 + const callbackUrl = new URL("/api/v1/auth/callback", ctx.req.url) 10 + 11 + const redirectUri = ctx.req.query("redirect_uri") 12 + if (redirectUri) callbackUrl.searchParams.set("redirect_uri", redirectUri) 13 + 14 + return callbackUrl.toString() 15 + } 16 + 17 + export const unprotectedRoutes = new Hono() 18 + .get("/test", (ctx) => { 19 + return ctx.text("Hello, World!") 20 + }) 21 + .get("/auth/authorize", async (ctx) => { 22 + const { url } = await client.authorize(getCallbackUrl(ctx), "code") 23 + return ctx.redirect(url, 302) 24 + }) 25 + .get("/auth/callback", async (ctx) => { 26 + const url = new URL(ctx.req.url) 27 + const code = ctx.req.query("code") 28 + const error = ctx.req.query("error") 29 + const redirectUri = ctx.req.query("redirect_uri") 30 + 31 + // Determine if this is a mobile auth flow based on redirect_uri 32 + const isMobile = !!(redirectUri && 33 + (redirectUri.startsWith("bookmarkerapp://") || 34 + redirectUri.startsWith("exp://"))) 35 + 36 + if (error) { 37 + console.debug( 38 + `AUTH CALLBACK: Error in request - ${error}`, 39 + ) 40 + 41 + if (isMobile && redirectUri) { 42 + const errorUrl = new URL(redirectUri) 43 + errorUrl.searchParams.set("error", error) 44 + errorUrl.searchParams.set( 45 + "error_description", 46 + ctx.req.query("error_description") || 47 + "Authentication failed", 48 + ) 49 + return ctx.redirect(errorUrl.toString(), 302) 50 + } 51 + 52 + return ctx.json( 53 + { 54 + message: error, 55 + cause: ctx.req.query("error_description"), 56 + }, 57 + 500, 58 + ) 59 + } 60 + 61 + if (!code) { 62 + const errorMessage = "Missing code" 63 + 64 + if (isMobile && redirectUri) { 65 + const errorUrl = new URL(redirectUri) 66 + errorUrl.searchParams.set("error", "missing_code") 67 + errorUrl.searchParams.set("error_description", errorMessage) 68 + return ctx.redirect(errorUrl.toString(), 302) 69 + } 70 + 71 + return ctx.json( 72 + { 73 + message: errorMessage, 74 + }, 75 + 400, 76 + ) 77 + } 78 + 79 + let exchanged: ExchangeSuccess | ExchangeError 80 + try { 81 + exchanged = await client.exchange( 82 + code, 83 + getCallbackUrl(ctx), 84 + ) 85 + } catch (error) { 86 + console.error( 87 + `AUTH CALLBACK: Exception during code exchange:`, 88 + error, 89 + ) 90 + 91 + if (isMobile && redirectUri) { 92 + const errorUrl = new URL(redirectUri) 93 + errorUrl.searchParams.set("error", "exchange_failed") 94 + errorUrl.searchParams.set( 95 + "error_description", 96 + "Token exchange failed", 97 + ) 98 + return ctx.redirect(errorUrl.toString(), 302) 99 + } 100 + 101 + return ctx.json( 102 + { 103 + message: "Token exchange failed", 104 + cause: String(error), 105 + }, 106 + 500, 107 + ) 108 + } 109 + 110 + if (exchanged.err) { 111 + console.debug( 112 + "AUTH CALLBACK: Error exchanging code —", 113 + exchanged.err, 114 + ) 115 + 116 + if (isMobile && redirectUri) { 117 + const errorUrl = new URL(redirectUri) 118 + errorUrl.searchParams.set("error", "token_error") 119 + errorUrl.searchParams.set( 120 + "error_description", 121 + JSON.stringify(exchanged.err), 122 + ) 123 + return ctx.redirect(errorUrl.toString(), 302) 124 + } 125 + 126 + return ctx.json(exchanged.err, 400) 127 + } 128 + 129 + const next = redirectUri 130 + ? new URL(redirectUri) 131 + : new URL("/home", url.origin) 132 + 133 + if (isMobile && redirectUri) { 134 + // For mobile apps, redirect to the app with tokens as query parameters 135 + next.searchParams.set("access_token", exchanged.tokens.access) 136 + if (exchanged.tokens.refresh) { 137 + next.searchParams.set("refresh_token", exchanged.tokens.refresh) 138 + } 139 + if (exchanged.tokens.expiresIn) { 140 + next.searchParams.set( 141 + "expires_in", 142 + exchanged.tokens.expiresIn.toString(), 143 + ) 144 + } 145 + 146 + console.log("Redirecting mobile app to:", next.toString()) 147 + } 148 + 149 + setTokens(ctx, exchanged.tokens) 150 + return ctx.redirect(next.toString(), 302) 151 + })
+90
server/utils/auth.ts
··· 1 + import { createClient, type Tokens } from "@openauthjs/openauth/client" 2 + import { createSubjects } from "@openauthjs/openauth/subject" 3 + import type { Context } from "hono" 4 + import { deleteCookie, getCookie, setCookie } from "hono/cookie" 5 + import { user, type UserSubject } from "./globals.ts" 6 + 7 + const clientID = Deno.env.get("AUTH_CLIENT_ID") 8 + const issuerUrl = Deno.env.get("AUTH_ISSUER_URL") 9 + 10 + export const subjects = createSubjects({ 11 + user, 12 + }) 13 + 14 + if (!clientID) { 15 + throw new Error("AUTH_CLIENT_ID environment variable is not set") 16 + } 17 + 18 + if (!issuerUrl) { 19 + throw new Error("AUTH_ISSUER_URL environment variable is not set") 20 + } 21 + 22 + export const client = createClient({ 23 + clientID: clientID, 24 + issuer: issuerUrl, 25 + }) 26 + 27 + export function setTokens(ctx: Context, tokens: Tokens) { 28 + setCookie(ctx, "access_token", tokens.access, { 29 + httpOnly: true, 30 + sameSite: "lax", 31 + path: "/", 32 + maxAge: tokens.expiresIn, 33 + }) 34 + setCookie(ctx, "refresh_token", tokens.refresh, { 35 + httpOnly: true, 36 + sameSite: "lax", 37 + path: "/", 38 + maxAge: 60 * 60 * 24 * 400, // 400 days 39 + }) 40 + } 41 + 42 + /** 43 + * Check if the user is authenticated 44 + * @param ctx The request context 45 + * @returns true if the user is authenticated 46 + */ 47 + export async function isAuthenticated(ctx: Context) { 48 + const accessToken = getCookie(ctx, "access_token") 49 + const refreshToken = getCookie(ctx, "refresh_token") 50 + 51 + if (!accessToken) { 52 + return false 53 + } 54 + 55 + const verified = await client.verify(subjects, accessToken, { 56 + refresh: refreshToken, 57 + }) 58 + 59 + if (verified.err) { 60 + console.error("Error verifying token:", verified.err) 61 + deleteCookie(ctx, "access_token") 62 + deleteCookie(ctx, "refresh_token") 63 + 64 + console.log("Cookies", ctx.header) 65 + 66 + return false 67 + } 68 + if (verified.tokens) { 69 + setTokens(ctx, verified.tokens) 70 + } 71 + 72 + return verified.subject 73 + } 74 + 75 + export function getUserSub(c: Context): UserSubject | null { 76 + // Get the subject from the context 77 + const subject = c.get("user") as UserSubject 78 + 79 + // Middleware should prevent this, but good practice to check 80 + if (!subject?.id) { 81 + console.error( 82 + "User subject missing in context for", 83 + c.req.method, 84 + c.req.path, 85 + ) 86 + return null 87 + } 88 + 89 + return subject 90 + }
+12
server/utils/bookmarks.ts
··· 1 + import { type } from "arktype" 2 + 3 + export const URLSchema = type("string.url") 4 + 5 + export const BookmarkSchema = type({ 6 + title: "1 <= string <= 100", 7 + description: "string <= 500", 8 + url: "7 <= string <= 1500", 9 + updatedAt: "string.date", 10 + }) 11 + 12 + export type Bookmark = typeof BookmarkSchema.infer
+16
server/utils/globals.ts
··· 1 + import { type } from "arktype" 2 + 3 + export const user = type({ 4 + id: "string", 5 + email: "string.email", 6 + avatar: "string.url | null | undefined", 7 + name: "string", 8 + }) 9 + 10 + export type UserSubject = typeof user.infer 11 + 12 + export type Variables = { 13 + Variables: { 14 + user: UserSubject 15 + } 16 + }
+1
server/utils/kv.ts
··· 1 + export const kv = await Deno.openKv()
+57
server/utils/utils.ts
··· 1 + /** 2 + * Wraps a promise in a try/catch block and returns a Result object representing 3 + * either a successful value or an error. 4 + * 5 + * This utility function provides a more structured way to handle asynchronous operations 6 + * without using try/catch blocks throughout your codebase. It follows a pattern similar 7 + * to Rust's Result type, allowing for more predictable error handling. 8 + * 9 + * @template T - The type of the value returned by the promise on success 10 + * @template E - The type of the error object (defaults to Error) 11 + * 12 + * @param promise - The promise to wrap and execute 13 + * 14 + * @returns A Promise resolving to a discriminated union object with: 15 + * - On success: `{ success: true, value: T, error: null }` 16 + * - On failure: `{ success: false, value: null, error: E }` 17 + * 18 + * @example 19 + * // Success case 20 + * const successResult = await tryCatch(Promise.resolve('data')); 21 + * if (successResult.success) { 22 + * console.log(successResult.value); // 'data' 23 + * } 24 + * 25 + * @example 26 + * // Error case 27 + * const errorResult = await tryCatch(Promise.reject(new Error('Failed'))); 28 + * if (!errorResult.success) { 29 + * console.error(errorResult.error.message); // 'Failed' 30 + * } 31 + * 32 + * @example 33 + * // Using with custom error type 34 + * interface ApiError { code: number; message: string } 35 + * const apiCall = tryCatch<UserData, ApiError>(fetchUserData()); 36 + */ 37 + export async function tryCatch<T, E = Error>( 38 + promise: Promise<T>, 39 + ): Promise< 40 + | { 41 + success: true 42 + value: T 43 + error: null 44 + } 45 + | { 46 + success: false 47 + value: null 48 + error: E 49 + } 50 + > { 51 + try { 52 + const value = await promise 53 + return { success: true, value, error: null } 54 + } catch (error) { 55 + return { success: false, value: null, error: error as E } 56 + } 57 + }
+12 -12
src/App.css
··· 1 1 #root { 2 - max-width: 1280px; 3 - margin: 0 auto; 4 - padding: 2rem; 5 - text-align: center; 2 + max-width: 1280px; 3 + margin: 0 auto; 4 + padding: 2rem; 5 + text-align: center; 6 6 } 7 7 8 8 .logo { 9 - height: 6em; 10 - padding: 1.5em; 11 - will-change: filter; 12 - transition: filter 300ms; 9 + height: 6em; 10 + padding: 1.5em; 11 + will-change: filter; 12 + transition: filter 300ms; 13 13 } 14 14 .logo:hover { 15 - filter: drop-shadow(0 0 2em #646cffaa); 15 + filter: drop-shadow(0 0 2em #646cffaa); 16 16 } 17 17 .logo.solid:hover { 18 - filter: drop-shadow(0 0 2em #61dafbaa); 18 + filter: drop-shadow(0 0 2em #61dafbaa); 19 19 } 20 20 21 21 .card { 22 - padding: 2em; 22 + padding: 2em; 23 23 } 24 24 25 25 .read-the-docs { 26 - color: #888; 26 + color: #888; 27 27 }
+29 -26
src/App.tsx
··· 1 - import { createSignal } from "solid-js"; 2 - import solidLogo from "./assets/solid.svg"; 3 - import "./App.css"; 1 + import { createSignal } from "solid-js" 2 + import solidLogo from "./assets/solid.svg" 3 + import "./App.css" 4 4 5 5 function App() { 6 - const [count, setCount] = createSignal(0); 6 + const [count, setCount] = createSignal(0) 7 7 8 - return ( 9 - <> 10 - <div> 11 - <a href="https://solidjs.com" target="_blank"> 12 - <img src={solidLogo} class="logo solid" alt="Solid logo" /> 13 - </a> 14 - </div> 15 - <h1>Vite + Solid</h1> 16 - <div class="card"> 17 - <button type="button" onClick={() => setCount((count) => count + 1)}> 18 - count is {count()} 19 - </button> 20 - <p> 21 - Edit <code>src/App.tsx</code> and save to test HMR 22 - </p> 23 - </div> 24 - <p class="read-the-docs"> 25 - Click on the Vite and Solid logos to learn more 26 - </p> 27 - </> 28 - ); 8 + return ( 9 + <> 10 + <div> 11 + <a href="https://solidjs.com" target="_blank"> 12 + <img src={solidLogo} class="logo solid" alt="Solid logo" /> 13 + </a> 14 + </div> 15 + <h1>Vite + Solid</h1> 16 + <div class="card"> 17 + <button 18 + type="button" 19 + onClick={() => setCount((count) => count + 1)} 20 + > 21 + count is {count()} 22 + </button> 23 + <p> 24 + Edit <code>src/App.tsx</code> and save to test HMR 25 + </p> 26 + </div> 27 + <p class="read-the-docs"> 28 + Click on the Vite and Solid logos to learn more 29 + </p> 30 + </> 31 + ) 29 32 } 30 33 31 - export default App; 34 + export default App
+75 -1
src/assets/solid.svg
··· 1 - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 166 155.3"><defs><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="27.5" y1="3" x2="152" y2="63.5"><stop offset=".1" stop-color="#76b3e1"/><stop offset=".3" stop-color="#dcf2fd"/><stop offset="1" stop-color="#76b3e1"/></linearGradient><linearGradient id="b" gradientUnits="userSpaceOnUse" x1="95.8" y1="32.6" x2="74" y2="105.2"><stop offset="0" stop-color="#76b3e1"/><stop offset=".5" stop-color="#4377bb"/><stop offset="1" stop-color="#1f3b77"/></linearGradient><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="18.4" y1="64.2" x2="144.3" y2="149.8"><stop offset="0" stop-color="#315aa9"/><stop offset=".5" stop-color="#518ac8"/><stop offset="1" stop-color="#315aa9"/></linearGradient><linearGradient id="d" gradientUnits="userSpaceOnUse" x1="75.2" y1="74.5" x2="24.4" y2="260.8"><stop offset="0" stop-color="#4377bb"/><stop offset=".5" stop-color="#1a336b"/><stop offset="1" stop-color="#1a336b"/></linearGradient></defs><path d="M163 35S110-4 69 5l-3 1c-6 2-11 5-14 9l-2 3-15 26 26 5c11 7 25 10 38 7l46 9 18-30z" fill="#76b3e1"/><path d="M163 35S110-4 69 5l-3 1c-6 2-11 5-14 9l-2 3-15 26 26 5c11 7 25 10 38 7l46 9 18-30z" opacity=".3" fill="url(#a)"/><path d="m52 35-4 1c-17 5-22 21-13 35 10 13 31 20 48 15l62-21S92 26 52 35z" fill="#518ac8"/><path d="m52 35-4 1c-17 5-22 21-13 35 10 13 31 20 48 15l62-21S92 26 52 35z" opacity=".3" fill="url(#b)"/><path d="M134 80a45 45 0 0 0-48-15L24 85 4 120l112 19 20-36c4-7 3-15-2-23z" fill="url(#c)"/><path d="M114 115a45 45 0 0 0-48-15L4 120s53 40 94 30l3-1c17-5 23-21 13-34z" fill="url(#d)"/></svg> 1 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 166 155.3"> 2 + <defs><linearGradient 3 + id="a" 4 + gradientUnits="userSpaceOnUse" 5 + x1="27.5" 6 + y1="3" 7 + x2="152" 8 + y2="63.5" 9 + ><stop offset=".1" stop-color="#76b3e1" /><stop 10 + offset=".3" 11 + stop-color="#dcf2fd" 12 + /><stop 13 + offset="1" 14 + stop-color="#76b3e1" 15 + /></linearGradient><linearGradient 16 + id="b" 17 + gradientUnits="userSpaceOnUse" 18 + x1="95.8" 19 + y1="32.6" 20 + x2="74" 21 + y2="105.2" 22 + ><stop offset="0" stop-color="#76b3e1" /><stop 23 + offset=".5" 24 + stop-color="#4377bb" 25 + /><stop 26 + offset="1" 27 + stop-color="#1f3b77" 28 + /></linearGradient><linearGradient 29 + id="c" 30 + gradientUnits="userSpaceOnUse" 31 + x1="18.4" 32 + y1="64.2" 33 + x2="144.3" 34 + y2="149.8" 35 + ><stop offset="0" stop-color="#315aa9" /><stop 36 + offset=".5" 37 + stop-color="#518ac8" 38 + /><stop 39 + offset="1" 40 + stop-color="#315aa9" 41 + /></linearGradient><linearGradient 42 + id="d" 43 + gradientUnits="userSpaceOnUse" 44 + x1="75.2" 45 + y1="74.5" 46 + x2="24.4" 47 + y2="260.8" 48 + ><stop offset="0" stop-color="#4377bb" /><stop 49 + offset=".5" 50 + stop-color="#1a336b" 51 + /><stop 52 + offset="1" 53 + stop-color="#1a336b" 54 + /></linearGradient></defs><path 55 + d="M163 35S110-4 69 5l-3 1c-6 2-11 5-14 9l-2 3-15 26 26 5c11 7 25 10 38 7l46 9 18-30z" 56 + fill="#76b3e1" 57 + /><path 58 + d="M163 35S110-4 69 5l-3 1c-6 2-11 5-14 9l-2 3-15 26 26 5c11 7 25 10 38 7l46 9 18-30z" 59 + opacity=".3" 60 + fill="url(#a)" 61 + /><path 62 + d="m52 35-4 1c-17 5-22 21-13 35 10 13 31 20 48 15l62-21S92 26 52 35z" 63 + fill="#518ac8" 64 + /><path 65 + d="m52 35-4 1c-17 5-22 21-13 35 10 13 31 20 48 15l62-21S92 26 52 35z" 66 + opacity=".3" 67 + fill="url(#b)" 68 + /><path 69 + d="M134 80a45 45 0 0 0-48-15L24 85 4 120l112 19 20-36c4-7 3-15-2-23z" 70 + fill="url(#c)" 71 + /><path 72 + d="M114 115a45 45 0 0 0-48-15L4 120s53 40 94 30l3-1c17-5 23-21 13-34z" 73 + fill="url(#d)" 74 + /> 75 + </svg>
+39 -39
src/index.css
··· 1 1 :root { 2 - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 - line-height: 1.5; 4 - font-weight: 400; 2 + font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 + line-height: 1.5; 4 + font-weight: 400; 5 5 6 - color-scheme: light dark; 7 - color: rgba(255, 255, 255, 0.87); 8 - background-color: #242424; 6 + color-scheme: light dark; 7 + color: rgba(255, 255, 255, 0.87); 8 + background-color: #242424; 9 9 10 - font-synthesis: none; 11 - text-rendering: optimizeLegibility; 12 - -webkit-font-smoothing: antialiased; 13 - -moz-osx-font-smoothing: grayscale; 10 + font-synthesis: none; 11 + text-rendering: optimizeLegibility; 12 + -webkit-font-smoothing: antialiased; 13 + -moz-osx-font-smoothing: grayscale; 14 14 } 15 15 16 16 a { 17 - font-weight: 500; 18 - color: #646cff; 19 - text-decoration: inherit; 17 + font-weight: 500; 18 + color: #646cff; 19 + text-decoration: inherit; 20 20 } 21 21 a:hover { 22 - color: #535bf2; 22 + color: #535bf2; 23 23 } 24 24 25 25 body { 26 - margin: 0; 27 - display: flex; 28 - place-items: center; 29 - min-width: 320px; 30 - min-height: 100vh; 26 + margin: 0; 27 + display: flex; 28 + place-items: center; 29 + min-width: 320px; 30 + min-height: 100vh; 31 31 } 32 32 33 33 h1 { 34 - font-size: 3.2em; 35 - line-height: 1.1; 34 + font-size: 3.2em; 35 + line-height: 1.1; 36 36 } 37 37 38 38 button { 39 - border-radius: 8px; 40 - border: 1px solid transparent; 41 - padding: 0.6em 1.2em; 42 - font-size: 1em; 43 - font-weight: 500; 44 - font-family: inherit; 45 - background-color: #1a1a1a; 46 - cursor: pointer; 47 - transition: border-color 0.25s; 39 + border-radius: 8px; 40 + border: 1px solid transparent; 41 + padding: 0.6em 1.2em; 42 + font-size: 1em; 43 + font-weight: 500; 44 + font-family: inherit; 45 + background-color: #1a1a1a; 46 + cursor: pointer; 47 + transition: border-color 0.25s; 48 48 } 49 49 button:hover { 50 - border-color: #646cff; 50 + border-color: #646cff; 51 51 } 52 52 button:focus, 53 53 button:focus-visible { 54 - outline: 4px auto -webkit-focus-ring-color; 54 + outline: 4px auto -webkit-focus-ring-color; 55 55 } 56 56 57 57 @media (prefers-color-scheme: light) { 58 - :root { 59 - color: #213547; 60 - background-color: #ffffff; 61 - } 62 - button { 63 - background-color: #f9f9f9; 64 - } 58 + :root { 59 + color: #213547; 60 + background-color: #ffffff; 61 + } 62 + button { 63 + background-color: #f9f9f9; 64 + } 65 65 }
+4 -4
src/index.tsx
··· 1 1 /* @refresh reload */ 2 - import { render } from 'solid-js/web' 3 - import './index.css' 4 - import App from './App.tsx' 2 + import { render } from "solid-js/web" 3 + import "./index.css" 4 + import App from "./App.tsx" 5 5 6 - const root = document.getElementById('root') 6 + const root = document.getElementById("root") 7 7 8 8 render(() => <App />, root!)
+3 -3
tsconfig.json
··· 16 16 "jsxImportSource": "solid-js", 17 17 "paths": { 18 18 "@/*": ["./src/*"], 19 - "@api/*": ["./server/*"], 19 + "@api/*": ["./server/*"] 20 20 }, 21 21 22 22 /* Linting */ 23 23 "strict": true, 24 24 "noUnusedLocals": true, 25 25 "noUnusedParameters": true, 26 - "noFallthroughCasesInSwitch": true, 26 + "noFallthroughCasesInSwitch": true 27 27 }, 28 - "include": ["src"], 28 + "include": ["src"] 29 29 }
+11 -11
vite.config.ts
··· 1 - import { defineConfig } from "vite"; 2 - import solid from "vite-plugin-solid"; 1 + import { defineConfig } from "vite" 2 + import solid from "vite-plugin-solid" 3 3 4 4 export default defineConfig({ 5 - plugins: [solid()], 6 - server: { 7 - proxy: { 8 - "/api": { 9 - target: "http://localhost:3000", 10 - changeOrigin: true, 11 - }, 5 + plugins: [solid()], 6 + server: { 7 + proxy: { 8 + "/api": { 9 + target: "http://localhost:3000", 10 + changeOrigin: true, 11 + }, 12 + }, 12 13 }, 13 - }, 14 - }); 14 + })