diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 733152c4..3d35eaae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -359,6 +359,9 @@ importers: '@vitejs/plugin-react-swc': specifier: ^3.5.0 version: 3.6.0(vite@5.1.6) + autoprefixer: + specifier: ^10.4.18 + version: 10.4.18(postcss@8.4.37) clsx: specifier: ^2.1.0 version: 2.1.0 @@ -371,6 +374,9 @@ importers: lz-string: specifier: ^1.5.0 version: 1.5.0 + postcss: + specifier: ^8.4.37 + version: 8.4.37 qrcode: specifier: ^1.5.3 version: 1.5.3 @@ -383,6 +389,9 @@ importers: shiki: specifier: ^1.1.7 version: 1.1.7 + tailwindcss: + specifier: ^3.4.1 + version: 3.4.1 typescript: specifier: ^5.2.2 version: 5.4.2 @@ -402,7 +411,6 @@ packages: /@alloc/quick-lru@5.2.0: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} - dev: false /@ampproject/remapping@2.2.1: resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} @@ -4052,7 +4060,6 @@ packages: /arg@5.0.2: resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} - dev: false /argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -4158,6 +4165,22 @@ packages: postcss-value-parser: 4.2.0 dev: false + /autoprefixer@10.4.18(postcss@8.4.37): + resolution: {integrity: sha512-1DKbDfsr6KUElM6wg+0zRNkB/Q7WcKYAaK+pzXn+Xqmszm/5Xa9coeNdtP88Vi+dPzZnMjhge8GIV49ZQkDa+g==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + dependencies: + browserslist: 4.23.0 + caniuse-lite: 1.0.30001597 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.0.0 + postcss: 8.4.37 + postcss-value-parser: 4.2.0 + dev: true + /available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -4251,7 +4274,6 @@ packages: electron-to-chromium: 1.4.677 node-releases: 2.0.14 update-browserslist-db: 1.0.13(browserslist@4.23.0) - dev: false /buf@0.1.1: resolution: {integrity: sha512-mhZY7GswAAd9ZJpBCsf2WaH2WMZwvxgsXD6rozflWgerE6gz2bCT/FvAfzcBXw55R18Jf8Fjzte7bw5xhwVhFg==} @@ -4351,7 +4373,6 @@ packages: /camelcase-css@2.0.1: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} engines: {node: '>= 6'} - dev: false /camelcase-keys@6.2.2: resolution: {integrity: sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==} @@ -4377,7 +4398,6 @@ packages: /caniuse-lite@1.0.30001597: resolution: {integrity: sha512-7LjJvmQU6Sj7bL0j5b5WY/3n7utXUJvAe1lxhsHDbLmwX9mdL86Yjtr+5SRCyf8qME4M7pU2hswj0FpyBVCv9w==} - dev: false /capnp-ts@0.7.0: resolution: {integrity: sha512-XKxXAC3HVPv7r674zP0VC3RTXz+/JKhfyw94ljvF80yynK6VkTnqE3jMuN8b3dUVmmc43TjyxjW4KTsmB3c86g==} @@ -4581,7 +4601,6 @@ packages: /commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} - dev: false /compressible@2.0.18: resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} @@ -4696,7 +4715,6 @@ packages: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} hasBin: true - dev: false /csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} @@ -4881,7 +4899,6 @@ packages: /didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} - dev: false /diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} @@ -4910,7 +4927,6 @@ packages: /dlv@1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} - dev: false /dom-parser@1.1.5: resolution: {integrity: sha512-lCiFG48ZUzGXjKN0qhSkxD/i3ndyV6I37zQ3W2VFYLjF1ob8A+QgSsM7Ps2UT0d3LpJxLMmMHiJJ34z5hkKLiA==} @@ -4962,7 +4978,6 @@ packages: /electron-to-chromium@1.4.677: resolution: {integrity: sha512-erDa3CaDzwJOpyvfKhOiJjBVNnMM0qxHq47RheVVwsSQrgBA9ZSGV9kdaOfZDPXcHzhG7lBxhj6A7KvfLJBd6Q==} - dev: false /emoji-regex@10.3.0: resolution: {integrity: sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==} @@ -5659,7 +5674,6 @@ packages: /fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} - dev: false /fresh@0.5.2: resolution: {integrity: sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=} @@ -5855,7 +5869,6 @@ packages: engines: {node: '>=10.13.0'} dependencies: is-glob: 4.0.3 - dev: false /glob-to-regexp@0.4.1: resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} @@ -6501,7 +6514,6 @@ packages: /jiti@1.21.0: resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==} hasBin: true - dev: false /jose@5.2.2: resolution: {integrity: sha512-/WByRr4jDcsKlvMd1dRJnPfS1GVO3WuKyaurJ/vvXcOaUQO8rnNObCQMlv/5uCceVQIq5Q4WLF44ohsdiTohdg==} @@ -6588,12 +6600,10 @@ packages: /lilconfig@2.1.0: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} - dev: false /lilconfig@3.1.1: resolution: {integrity: sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==} engines: {node: '>=14'} - dev: false /linebreak@1.1.0: resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==} @@ -7570,7 +7580,6 @@ packages: any-promise: 1.3.0 object-assign: 4.1.1 thenify-all: 1.6.0 - dev: false /nanoid@3.3.7: resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} @@ -7669,7 +7678,6 @@ packages: /node-releases@2.0.14: resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} - dev: false /nopt@5.0.0: resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} @@ -7695,7 +7703,6 @@ packages: /normalize-range@0.1.2: resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} engines: {node: '>=0.10.0'} - dev: false /not@0.1.0: resolution: {integrity: sha512-5PDmaAsVfnWUgTUbJ3ERwn7u79Z0dYxN9ErxCpVJJqe2RK0PJ3z+iFUxuqjwtlDDegXvtWoxD/3Fzxox7tFGWA==} @@ -7736,7 +7743,6 @@ packages: /object-hash@3.0.0: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} engines: {node: '>= 6'} - dev: false /object-inspect@1.13.1: resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} @@ -8011,7 +8017,6 @@ packages: /pify@2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} - dev: false /pify@4.0.1: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} @@ -8021,7 +8026,6 @@ packages: /pirates@4.0.6: resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} engines: {node: '>= 6'} - dev: false /pkg-dir@4.2.0: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} @@ -8047,27 +8051,25 @@ packages: engines: {node: '>= 0.4'} dev: true - /postcss-import@15.1.0(postcss@8.4.35): + /postcss-import@15.1.0(postcss@8.4.37): resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} engines: {node: '>=14.0.0'} peerDependencies: postcss: ^8.0.0 dependencies: - postcss: 8.4.35 + postcss: 8.4.37 postcss-value-parser: 4.2.0 read-cache: 1.0.0 resolve: 1.22.8 - dev: false - /postcss-js@4.0.1(postcss@8.4.35): + /postcss-js@4.0.1(postcss@8.4.37): resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} engines: {node: ^12 || ^14 || >= 16} peerDependencies: postcss: ^8.4.21 dependencies: camelcase-css: 2.0.1 - postcss: 8.4.35 - dev: false + postcss: 8.4.37 /postcss-load-config@4.0.2(postcss@8.4.35): resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} @@ -8086,15 +8088,30 @@ packages: yaml: 2.3.4 dev: false - /postcss-nested@6.0.1(postcss@8.4.35): + /postcss-load-config@4.0.2(postcss@8.4.37): + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} + engines: {node: '>= 14'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + dependencies: + lilconfig: 3.1.1 + postcss: 8.4.37 + yaml: 2.3.4 + + /postcss-nested@6.0.1(postcss@8.4.37): resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==} engines: {node: '>=12.0'} peerDependencies: postcss: ^8.2.14 dependencies: - postcss: 8.4.35 + postcss: 8.4.37 postcss-selector-parser: 6.0.15 - dev: false /postcss-selector-parser@6.0.15: resolution: {integrity: sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==} @@ -8102,11 +8119,9 @@ packages: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 - dev: false /postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - dev: false /postcss@8.4.31: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} @@ -8124,6 +8139,15 @@ packages: nanoid: 3.3.7 picocolors: 1.0.0 source-map-js: 1.0.2 + dev: false + + /postcss@8.4.37: + resolution: {integrity: sha512-7iB/v/r7Woof0glKLH8b1SPHrsX7uhdO+Geb41QpF/+mWZHU3uxxSlN+UXGVit1PawOYDToO+AbZzhBzWRDwbQ==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.0 + source-map-js: 1.2.0 /preferred-pm@3.1.2: resolution: {integrity: sha512-nk7dKrcW8hfCZ4H6klWcdRknBOXWzNQByJ0oJyX97BOupsYD+FzLS4hflgEu/uPUEHZCuRfMxzCBsuWd7OzT8Q==} @@ -8372,7 +8396,6 @@ packages: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} dependencies: pify: 2.3.0 - dev: false /read-pkg-up@7.0.1: resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} @@ -8931,6 +8954,10 @@ packages: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} + /source-map-js@1.2.0: + resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} + engines: {node: '>=0.10.0'} + /source-map-support@0.5.21: resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} dependencies: @@ -9202,7 +9229,6 @@ packages: mz: 2.7.0 pirates: 4.0.6 ts-interface-checker: 0.1.13 - dev: false /supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} @@ -9242,17 +9268,16 @@ packages: normalize-path: 3.0.0 object-hash: 3.0.0 picocolors: 1.0.0 - postcss: 8.4.35 - postcss-import: 15.1.0(postcss@8.4.35) - postcss-js: 4.0.1(postcss@8.4.35) - postcss-load-config: 4.0.2(postcss@8.4.35) - postcss-nested: 6.0.1(postcss@8.4.35) + postcss: 8.4.37 + postcss-import: 15.1.0(postcss@8.4.37) + postcss-js: 4.0.1(postcss@8.4.37) + postcss-load-config: 4.0.2(postcss@8.4.37) + postcss-nested: 6.0.1(postcss@8.4.37) postcss-selector-parser: 6.0.15 resolve: 1.22.8 sucrase: 3.35.0 transitivePeerDependencies: - ts-node - dev: false /tar@4.4.18: resolution: {integrity: sha512-ZuOtqqmkV9RE1+4odd+MhBpibmCxNP6PJhH/h2OqNuotTX7/XHPZQJv2pKvWMplFH9SIZZhitehh6vBH6LO8Pg==} @@ -9298,13 +9323,11 @@ packages: engines: {node: '>=0.8'} dependencies: thenify: 3.3.1 - dev: false /thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} dependencies: any-promise: 1.3.0 - dev: false /time-span@4.0.0: resolution: {integrity: sha512-MyqZCTGLDZ77u4k+jqg4UlrzPTPZ49NDlaekU6uuFaJLzPIN1woaRXCbGeqOfxwc3Y37ZROGAJ614Rdv7Olt+g==} @@ -9386,7 +9409,6 @@ packages: /ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - dev: false /ts-morph@12.0.0: resolution: {integrity: sha512-VHC8XgU2fFW7yO1f/b3mxKDje1vmyzFXHWzOYmKEkCEwcLjDtbdLgBQviqj4ZwP4MJkQtRo6Ha2I29lq/B+VxA==} @@ -9710,7 +9732,6 @@ packages: browserslist: 4.23.0 escalade: 3.1.2 picocolors: 1.0.0 - dev: false /uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -9983,7 +10004,7 @@ packages: dependencies: '@types/node': 20.11.19 esbuild: 0.19.12 - postcss: 8.4.35 + postcss: 8.4.37 rollup: 4.12.0 optionalDependencies: fsevents: 2.3.3 @@ -10363,7 +10384,6 @@ packages: /yaml@2.3.4: resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==} engines: {node: '>= 14'} - dev: false /yargs-parser@18.1.3: resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} diff --git a/src/cli/app.ts b/src/cli/app.ts new file mode 100644 index 00000000..f8502b26 --- /dev/null +++ b/src/cli/app.ts @@ -0,0 +1,19 @@ +import { serveStatic } from '@hono/node-server/serve-static' +import { Hono } from 'hono' + +import { routes } from '../dev/devtools.js' +import { neynar } from '../hubs/neynar.js' + +export const app = new Hono() + +app.route( + '/', + routes({ + basePath: '', + hub: neynar({ apiKey: 'NEYNAR_FROG_FM' }), + publicPath: '.', + routes: [], + secret: undefined, + serveStatic, + }), +) diff --git a/src/cli/commands/dev.ts b/src/cli/commands/dev.ts index 8298bbbb..26bcc2f2 100644 --- a/src/cli/commands/dev.ts +++ b/src/cli/commands/dev.ts @@ -1,41 +1,43 @@ +import type { Hono } from 'hono' import { existsSync } from 'node:fs' -import { join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { dirname, relative, join, resolve } from 'node:path' import pc from 'picocolors' import { createLogger, createServer } from 'vite' import type { Frog } from '../../frog.js' import { version } from '../../version.js' import { findEntrypoint } from '../utils/findEntrypoint.js' -import { defaultOptions, devServer } from '../vite/dev.js' +import { devServer } from '../vite/dev.js' +import { printServerUrls, type ServerUrls } from '../utils/logger.js' type DevOptions = { - host?: boolean - port?: number - staticDir?: string + host?: boolean | undefined + port?: number | undefined + staticDir?: string | undefined + ui?: boolean | undefined } -export async function dev( - entry_: string | undefined, - options: DevOptions = {}, -) { - const { host, port, staticDir } = options - const entry = entry_ || (await findEntrypoint()) +export async function dev(path: string | undefined, options: DevOptions = {}) { + const { host, port, staticDir, ui } = options - const entry_resolved = resolve(join(process.cwd(), entry)) - if (!existsSync(entry_resolved)) - throw new Error(`entrypoint not found: ${entry_resolved}`) + const entryPath = path || (await findEntrypoint()) + let entry = resolve(join(process.cwd(), entryPath)) + let injectClientScript = true + const entryExists = existsSync(entry) + if (!entryExists || ui) { + entry = relative( + './', + resolve(dirname(fileURLToPath(import.meta.url)), '../app.ts'), + ) + injectClientScript = false + } const server = await createServer({ plugins: [ devServer({ - entry: entry_resolved, - exclude: [ - ...defaultOptions.exclude, - /.+\.(gif|jpe?g|tiff?|png|webp|bmp|woff|eot|woff2|ttf|otf|ico|txt)$/, - ], - // Note: we are not relying on the default export so we can be compatible with - // runtimes that rely on it (e.g. Vercel Serverless Functions). - export: 'app', + entry, + injectClientScript, }), ], publicDir: staticDir ?? 'public', @@ -46,13 +48,17 @@ export async function dev( }, }) - const module = await server.ssrLoadModule(entry_resolved) - const app = module.app as Frog | undefined - const basePath = app?.basePath || '/' + const module = (await server.ssrLoadModule(entry)) as { + app: Frog | Hono | undefined + } + const app = module.app + if (!app) { + await server.close() + throw new Error(`app export not found: ${entry}`) + } await server.listen() server.bindCLIShortcuts() - const url = `http://localhost:${server.config.server.port}` const logger = createLogger() logger.clearScreen('info') @@ -61,11 +67,46 @@ export async function dev( ` ${pc.green('[running]')} ${pc.bold('frog')}@${pc.dim(`v${version}`)}`, ) logger.info('') - const appUrl = `${url}${basePath}` - logger.info(` ${pc.green('➜')} ${pc.bold('Local')}: ${pc.cyan(appUrl)}`) - if (app?._dev) { - const devUrl = `${url}${app._dev}` - logger.info(` ${pc.green('➜')} ${pc.bold('Inspect')}: ${pc.cyan(devUrl)}`) + if (!entryExists) { + logger.info( + pc.yellow( + ` Using standalone devtools. No entry found at ${pc.bold(entryPath)}.`, + ), + ) + logger.info('') + } + + let devBasePath: string | false | undefined = false + let resolvedUrls: ServerUrls = { + local: server.resolvedUrls?.local ?? [], + network: server.resolvedUrls?.network ?? [], + dev: [], } + if ('version' in app && app.version === version) { + const basePath = app.basePath === '/' ? '' : app.basePath + devBasePath = app._dev ? app._dev.replace(/^\//, '') : undefined + resolvedUrls = { + local: (server.resolvedUrls?.local ?? []).map( + (url) => `${url}${basePath}`, + ), + network: (server.resolvedUrls?.network ?? []).map( + (url) => `${url}${basePath}`, + ), + dev: devBasePath + ? (server.resolvedUrls?.local ?? []).map( + (url) => `${url}${devBasePath}`, + ) + : [], + } + } + + printServerUrls( + resolvedUrls, + { + dev: devBasePath, + host: server.config.server.host, + }, + logger.info, + ) } diff --git a/src/cli/index.ts b/src/cli/index.ts index 9b9c1792..87e6b368 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -3,7 +3,7 @@ import { cac } from 'cac' import { version } from '../version.js' import { dev } from './commands/dev.js' -import { build as build_vercel } from './commands/vercel-build.js' +import { build } from './commands/vercel-build.js' export const cli = cac('frog') @@ -16,15 +16,18 @@ cli .option('-h, --host', 'Expose host URL') .option('-p, --port ', 'Port used by the server (default: 5173)') .option('-s, --staticDir [string]', 'Path to static files (default: public)') + .option('--ui', 'Run standalone devtools') .example((name) => `${name} dev --host`) .example((name) => `${name} dev --port 6969`) .action(dev) + cli .command( 'vercel-build', 'Builds an output conforming to the Vercel Build Output API.', ) - .action(build_vercel) + .example((name) => `${name} vercel-build`) + .action(build) cli.help() cli.version(version) diff --git a/src/cli/utils/logger.ts b/src/cli/utils/logger.ts new file mode 100644 index 00000000..0481fe7a --- /dev/null +++ b/src/cli/utils/logger.ts @@ -0,0 +1,41 @@ +// Forked from https://github.com/vitejs/vite/blob/1a3b1d73d7babdab6a52a5fb1ef193fd63666877/packages/vite/src/node/logger.ts#L161 +import type { Logger, ResolvedServerUrls } from 'vite' +import colors from 'picocolors' + +export type ServerUrls = ResolvedServerUrls & { dev: string[] } + +export function printServerUrls( + urls: ServerUrls, + options: { + dev: string | boolean | undefined + host: string | boolean | undefined + }, + info: Logger['info'], +): void { + const colorUrl = (url: string) => + colors.cyan(url.replace(/:(\d+)\//, (_, port) => `:${colors.bold(port)}/`)) + + for (const url of urls.local) { + info(` ${colors.green('➜')} ${colors.bold('Local')}: ${colorUrl(url)}`) + } + for (const url of urls.network) { + info(` ${colors.green('➜')} ${colors.bold('Network')}: ${colorUrl(url)}`) + } + for (const url of urls.dev) { + info(` ${colors.green('➜')} ${colors.bold('Inspect')}: ${colorUrl(url)}`) + } + + if (urls.dev.length === 0 && options.dev === undefined) + info( + colors.dim(` ${colors.green('➜')} ${colors.bold('Inspect')}: add `) + + colors.bold('devtools') + + colors.dim(' to app'), + ) + + if (urls.network.length === 0 && options.host === undefined) + info( + colors.dim(` ${colors.green('➜')} ${colors.bold('Network')}: use `) + + colors.bold('--host') + + colors.dim(' to expose'), + ) +} diff --git a/src/cli/vite/dev.ts b/src/cli/vite/dev.ts index f749fd84..97904f87 100644 --- a/src/cli/vite/dev.ts +++ b/src/cli/vite/dev.ts @@ -15,7 +15,9 @@ export type DevServerOptions = { export const defaultOptions = { entry: './src/index.ts', - export: 'default', + // Note: we are not relying on the default export so we can be compatible with + // runtimes that rely on it (e.g. Vercel Serverless Functions). + export: 'app', injectClientScript: true, exclude: [ /.*\.css$/, @@ -26,6 +28,8 @@ export const defaultOptions = { /^\/favicon\.ico$/, /^\/static\/.+/, /^\/node_modules\/.*/, + /// + /.+\.(gif|jpe?g|tiff?|png|webp|bmp|woff|eot|woff2|ttf|otf|ico|txt)$/, ], ignoreWatching: [/\.wrangler/], plugins: [], diff --git a/src/dev/api.ts b/src/dev/api.ts index 64627c91..8e0dae25 100644 --- a/src/dev/api.ts +++ b/src/dev/api.ts @@ -29,6 +29,8 @@ import { getUserDataByFid, postSignedKeyRequest, } from './utils/warpcast.js' +import type { inspectRoutes } from 'hono/dev' +import type { Hub } from '../types/hub.js' export type ApiRoutesOptions = { /** Custom app fid to auth with. */ @@ -37,17 +39,7 @@ export type ApiRoutesOptions = { appMnemonic?: string | undefined } -type Options = ApiRoutesOptions & { - hubApiUrl?: string | undefined - routes: Route[] - secret?: string | undefined -} - -type Route = { - path: string - method: string - isMiddleware: boolean -} +export type RouteData = ReturnType[number] export type User = { displayName?: string | undefined @@ -58,8 +50,14 @@ export type User = { username?: string | undefined } -export function apiRoutes(options: Options) { - const { appFid, appMnemonic, hubApiUrl, routes, secret } = options +export function apiRoutes( + options: ApiRoutesOptions & { + hub: Hub | undefined + routes: RouteData[] + secret: string | undefined + }, +) { + const { appFid, appMnemonic, hub, routes, secret } = options return new Hono<{ Variables: { @@ -219,8 +217,8 @@ export function apiRoutes(options: Options) { if (state === 'completed') { let user: User = { state, token, userFid: userFid as number } - if (hubApiUrl && userFid) { - const data = await getUserDataByFid(hubApiUrl, userFid) + if (hub && userFid) { + const data = await getUserDataByFid(hub, userFid) user = { ...user, ...data } } @@ -261,7 +259,7 @@ export type Bootstrap = { user: User | undefined } -export function getFrameUrls(origin: string, routes: Route[]) { +export function getFrameUrls(origin: string, routes: RouteData[]) { const frameUrls: string[] = [] for (const route of routes) { if (route.isMiddleware) continue diff --git a/src/dev/constants.ts b/src/dev/constants.ts index 98cbdeba..425a029f 100644 --- a/src/dev/constants.ts +++ b/src/dev/constants.ts @@ -16,3 +16,5 @@ export const defaultCookieOptions = { sameSite: 'Strict', secure: true, } as CookieOptions + +export const uiDistDir = '.frog' diff --git a/src/dev/devtools.tsx b/src/dev/devtools.tsx index 5cee0d60..ba161522 100644 --- a/src/dev/devtools.tsx +++ b/src/dev/devtools.tsx @@ -16,58 +16,16 @@ import { apiRoutes, getFrameUrls, getInitialData, + type RouteData, } from './api.js' import { isCloudflareWorkers } from './utils/env.js' import { getUserDataByFid } from './utils/warpcast.js' +import { uiDistDir } from './constants.js' +import { HTTPException } from 'hono/http-exception' +import type { Hub } from '../types/hub.js' export type DevtoolsOptions = - Pretty< - Pretty & { - /** - * The base path for devtools assets. - */ - assetsPath?: string - /** - * The base path for the devtools instance off the Frog instances `basePath`. - * - * @default '/dev' - */ - basePath?: string | undefined - /** - * Platform-dependent function to serve devtools' static files. - * - * @example - * import { serveStatic } from 'frog/serve-static' - * import { serveStatic } from 'hono/bun' - * import { serveStatic } from 'hono/cloudflare-workers' - * import { serveStatic } from '@hono/node-server/serve-static' - */ - serveStatic?: ServeStatic | undefined - /** - * Parameters to pass to the {@link serveStatic} function. - */ - serveStaticOptions?: - | Pretty[0]>> - | undefined - } - > - -type ServeStatic = - | typeof n_serveStatic - | typeof c_serveStatic - | typeof b_serveStatic - -let root: string | undefined -if (!isCloudflareWorkers()) { - const { dirname, relative, resolve } = await import('node:path') - const { fileURLToPath } = await import('node:url') - root = relative( - './', - resolve(dirname(fileURLToPath(import.meta.url)), '../ui'), - ) -} - -const uiDistDir = '.frog' + RoutesOptions /** * Built-in devtools with live preview, hot reload, time-travel debugging, and more. @@ -93,12 +51,9 @@ export function devtools< assetsPath, basePath = '/dev', serveStatic, - serveStaticOptions = { manifest: '' }, + serveStaticOptions, } = options ?? {} - const routes = inspectRoutes(frog.hono) - const hubApiUrl = frog.hub?.apiUrl || frog.hubApiUrl - let publicPath = '' if (assetsPath) publicPath = assetsPath === '/' ? '' : assetsPath else if (serveStatic) publicPath = `.${basePath}` @@ -109,26 +64,115 @@ export function devtools< const rootBasePath = frog.basePath === '/' ? '' : frog.basePath const devBasePath = `${rootBasePath}${basePath}` + const app = routes({ + appFid, + appMnemonic, + basePath: devBasePath, + hub: frog.hub || (frog.hubApiUrl ? { apiUrl: frog.hubApiUrl } : undefined), + publicPath, + routes: inspectRoutes(frog.hono), + secret: frog.secret, + serveStatic, + serveStaticOptions, + }) + + frog.hono.route(basePath, app) + frog._dev = devBasePath +} + +type RoutesOptions = Pretty< + Pretty & { + /** + * The base path for devtools assets. + */ + assetsPath?: string + /** + * The base path for the devtools instance off the Frog instances `basePath`. + * + * @default '/dev' + */ + basePath?: string | undefined + /** + * Platform-dependent function to serve devtools' static files. + * + * @example + * import { serveStatic } from 'frog/serve-static' + * import { serveStatic } from 'hono/bun' + * import { serveStatic } from 'hono/cloudflare-workers' + * import { serveStatic } from '@hono/node-server/serve-static' + */ + serveStatic?: serveStatic | ServeStatic | undefined + /** + * Parameters to pass to the {@link serveStatic} function. + */ + serveStaticOptions?: Parameters[0] | undefined + } +> + +type ServeStatic = + | typeof n_serveStatic + | typeof c_serveStatic + | typeof b_serveStatic + +let root: string | undefined +if (!isCloudflareWorkers()) { + const { dirname, relative, resolve } = await import('node:path') + const { fileURLToPath } = await import('node:url') + root = relative( + './', + resolve(dirname(fileURLToPath(import.meta.url)), '../ui'), + ) +} + +export function routes( + options: RoutesOptions & { + basePath: string + hub: Hub | undefined + publicPath: string + routes: RouteData[] + secret: string | undefined + }, +) { + const { + appFid, + appMnemonic, + basePath, + hub, + publicPath, + routes, + secret, + serveStatic, + serveStaticOptions, + } = options + const app = new Hono() + const assetsPath = publicPath.endsWith('/') + ? publicPath.replace(/\/$/, '') + : publicPath + app .get('/', async (c) => { const { origin } = new URL(c.req.url) - const baseUrl = `${origin}${devBasePath}` + const baseUrl = `${origin}${basePath}` let frameUrls: string[] = [] let initialData: Bootstrap['data'] = undefined - if (routes.length) { + const url = c.req.query('url') + if (url || routes.length) { frameUrls = getFrameUrls(origin, routes) let frameUrl = frameUrls[0] - const url = c.req.query('url') if (url) { const tmpUrl = `${origin}${url}` if (url.startsWith('/')) frameUrl = tmpUrl - else if (frameUrls.includes(url)) frameUrl = url + else frameUrl = url } - initialData = (await getInitialData(frameUrl)) as Bootstrap['data'] + try { + initialData = (await getInitialData(frameUrl)) as Bootstrap['data'] + } catch (error) { + if (error instanceof HTTPException) throw error + } } let user: User | undefined = undefined @@ -136,8 +180,8 @@ export function devtools< if (cookie) try { const parsed = JSON.parse(cookie) - if (parsed && hubApiUrl) { - const data = await getUserDataByFid(hubApiUrl, parsed.userFid) + if (parsed && hub) { + const data = await getUserDataByFid(hub, parsed.userFid) user = { state: 'completed', ...parsed, ...data } } } catch {} @@ -150,7 +194,7 @@ export function devtools< const title = initialData ? `frame: ${new URL(initialData.url).pathname}` - : 'frog' + : 'Frog Devtools' return c.html( <> @@ -171,18 +215,18 @@ export function devtools<