From 52759aa7f688708d48b36d34cec1fc990f7d3756 Mon Sep 17 00:00:00 2001 From: Kyle June Date: Sat, 3 Aug 2024 00:24:22 -0400 Subject: [PATCH 1/2] Update esbuild --- deno.jsonc | 2 +- deno.lock | 153 +++++++++++++++++++++++---------------------- example/deno.jsonc | 2 +- example/deno.lock | 2 +- 4 files changed, 82 insertions(+), 77 deletions(-) diff --git a/deno.jsonc b/deno.jsonc index 81412b2..36871c0 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -85,7 +85,7 @@ "@std/log": "jsr:@std/log@0", "@std/path": "jsr:@std/path@1", "@std/testing": "jsr:@std/testing@1", - "esbuild": "npm:esbuild@0.20", + "esbuild": "npm:esbuild@0.23", "react": "npm:react@18", "@types/react": "npm:@types/react@18", "react-dom": "npm:react-dom@18", diff --git a/deno.lock b/deno.lock index 908367d..9a60e16 100644 --- a/deno.lock +++ b/deno.lock @@ -43,7 +43,7 @@ "npm:@testing-library/react@16": "npm:@testing-library/react@16.0.0_@testing-library+dom@10.4.0_@types+react@18.3.3_react@18.3.1_react-dom@18.3.1__react@18.3.1", "npm:@types/node": "npm:@types/node@18.16.19", "npm:@types/react@18": "npm:@types/react@18.3.3", - "npm:esbuild@0.20": "npm:esbuild@0.20.2", + "npm:esbuild@0.23": "npm:esbuild@0.23.0", "npm:global-jsdom@24": "npm:global-jsdom@24.0.0_jsdom@24.1.1", "npm:path-to-regexp@6.2.1": "npm:path-to-regexp@6.2.1", "npm:react-dom@18": "npm:react-dom@18.3.1_react@18.3.1", @@ -263,96 +263,100 @@ "regenerator-runtime": "regenerator-runtime@0.14.1" } }, - "@esbuild/aix-ppc64@0.20.2": { - "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "@esbuild/aix-ppc64@0.23.0": { + "integrity": "sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==", "dependencies": {} }, - "@esbuild/android-arm64@0.20.2": { - "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "@esbuild/android-arm64@0.23.0": { + "integrity": "sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==", "dependencies": {} }, - "@esbuild/android-arm@0.20.2": { - "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "@esbuild/android-arm@0.23.0": { + "integrity": "sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==", "dependencies": {} }, - "@esbuild/android-x64@0.20.2": { - "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "@esbuild/android-x64@0.23.0": { + "integrity": "sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==", "dependencies": {} }, - "@esbuild/darwin-arm64@0.20.2": { - "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "@esbuild/darwin-arm64@0.23.0": { + "integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==", "dependencies": {} }, - "@esbuild/darwin-x64@0.20.2": { - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "@esbuild/darwin-x64@0.23.0": { + "integrity": "sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==", "dependencies": {} }, - "@esbuild/freebsd-arm64@0.20.2": { - "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "@esbuild/freebsd-arm64@0.23.0": { + "integrity": "sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==", "dependencies": {} }, - "@esbuild/freebsd-x64@0.20.2": { - "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "@esbuild/freebsd-x64@0.23.0": { + "integrity": "sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==", "dependencies": {} }, - "@esbuild/linux-arm64@0.20.2": { - "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "@esbuild/linux-arm64@0.23.0": { + "integrity": "sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==", "dependencies": {} }, - "@esbuild/linux-arm@0.20.2": { - "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "@esbuild/linux-arm@0.23.0": { + "integrity": "sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==", "dependencies": {} }, - "@esbuild/linux-ia32@0.20.2": { - "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "@esbuild/linux-ia32@0.23.0": { + "integrity": "sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==", "dependencies": {} }, - "@esbuild/linux-loong64@0.20.2": { - "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "@esbuild/linux-loong64@0.23.0": { + "integrity": "sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==", "dependencies": {} }, - "@esbuild/linux-mips64el@0.20.2": { - "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "@esbuild/linux-mips64el@0.23.0": { + "integrity": "sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==", "dependencies": {} }, - "@esbuild/linux-ppc64@0.20.2": { - "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "@esbuild/linux-ppc64@0.23.0": { + "integrity": "sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==", "dependencies": {} }, - "@esbuild/linux-riscv64@0.20.2": { - "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "@esbuild/linux-riscv64@0.23.0": { + "integrity": "sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==", "dependencies": {} }, - "@esbuild/linux-s390x@0.20.2": { - "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "@esbuild/linux-s390x@0.23.0": { + "integrity": "sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==", "dependencies": {} }, - "@esbuild/linux-x64@0.20.2": { - "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "@esbuild/linux-x64@0.23.0": { + "integrity": "sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==", "dependencies": {} }, - "@esbuild/netbsd-x64@0.20.2": { - "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "@esbuild/netbsd-x64@0.23.0": { + "integrity": "sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==", "dependencies": {} }, - "@esbuild/openbsd-x64@0.20.2": { - "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "@esbuild/openbsd-arm64@0.23.0": { + "integrity": "sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==", "dependencies": {} }, - "@esbuild/sunos-x64@0.20.2": { - "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "@esbuild/openbsd-x64@0.23.0": { + "integrity": "sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==", "dependencies": {} }, - "@esbuild/win32-arm64@0.20.2": { - "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "@esbuild/sunos-x64@0.23.0": { + "integrity": "sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==", "dependencies": {} }, - "@esbuild/win32-ia32@0.20.2": { - "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "@esbuild/win32-arm64@0.23.0": { + "integrity": "sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==", "dependencies": {} }, - "@esbuild/win32-x64@0.20.2": { - "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "@esbuild/win32-ia32@0.23.0": { + "integrity": "sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==", + "dependencies": {} + }, + "@esbuild/win32-x64@0.23.0": { + "integrity": "sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==", "dependencies": {} }, "@remix-run/router@1.17.0": { @@ -521,32 +525,33 @@ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dependencies": {} }, - "esbuild@0.20.2": { - "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", - "dependencies": { - "@esbuild/aix-ppc64": "@esbuild/aix-ppc64@0.20.2", - "@esbuild/android-arm": "@esbuild/android-arm@0.20.2", - "@esbuild/android-arm64": "@esbuild/android-arm64@0.20.2", - "@esbuild/android-x64": "@esbuild/android-x64@0.20.2", - "@esbuild/darwin-arm64": "@esbuild/darwin-arm64@0.20.2", - "@esbuild/darwin-x64": "@esbuild/darwin-x64@0.20.2", - "@esbuild/freebsd-arm64": "@esbuild/freebsd-arm64@0.20.2", - "@esbuild/freebsd-x64": "@esbuild/freebsd-x64@0.20.2", - "@esbuild/linux-arm": "@esbuild/linux-arm@0.20.2", - "@esbuild/linux-arm64": "@esbuild/linux-arm64@0.20.2", - "@esbuild/linux-ia32": "@esbuild/linux-ia32@0.20.2", - "@esbuild/linux-loong64": "@esbuild/linux-loong64@0.20.2", - "@esbuild/linux-mips64el": "@esbuild/linux-mips64el@0.20.2", - "@esbuild/linux-ppc64": "@esbuild/linux-ppc64@0.20.2", - "@esbuild/linux-riscv64": "@esbuild/linux-riscv64@0.20.2", - "@esbuild/linux-s390x": "@esbuild/linux-s390x@0.20.2", - "@esbuild/linux-x64": "@esbuild/linux-x64@0.20.2", - "@esbuild/netbsd-x64": "@esbuild/netbsd-x64@0.20.2", - "@esbuild/openbsd-x64": "@esbuild/openbsd-x64@0.20.2", - "@esbuild/sunos-x64": "@esbuild/sunos-x64@0.20.2", - "@esbuild/win32-arm64": "@esbuild/win32-arm64@0.20.2", - "@esbuild/win32-ia32": "@esbuild/win32-ia32@0.20.2", - "@esbuild/win32-x64": "@esbuild/win32-x64@0.20.2" + "esbuild@0.23.0": { + "integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==", + "dependencies": { + "@esbuild/aix-ppc64": "@esbuild/aix-ppc64@0.23.0", + "@esbuild/android-arm": "@esbuild/android-arm@0.23.0", + "@esbuild/android-arm64": "@esbuild/android-arm64@0.23.0", + "@esbuild/android-x64": "@esbuild/android-x64@0.23.0", + "@esbuild/darwin-arm64": "@esbuild/darwin-arm64@0.23.0", + "@esbuild/darwin-x64": "@esbuild/darwin-x64@0.23.0", + "@esbuild/freebsd-arm64": "@esbuild/freebsd-arm64@0.23.0", + "@esbuild/freebsd-x64": "@esbuild/freebsd-x64@0.23.0", + "@esbuild/linux-arm": "@esbuild/linux-arm@0.23.0", + "@esbuild/linux-arm64": "@esbuild/linux-arm64@0.23.0", + "@esbuild/linux-ia32": "@esbuild/linux-ia32@0.23.0", + "@esbuild/linux-loong64": "@esbuild/linux-loong64@0.23.0", + "@esbuild/linux-mips64el": "@esbuild/linux-mips64el@0.23.0", + "@esbuild/linux-ppc64": "@esbuild/linux-ppc64@0.23.0", + "@esbuild/linux-riscv64": "@esbuild/linux-riscv64@0.23.0", + "@esbuild/linux-s390x": "@esbuild/linux-s390x@0.23.0", + "@esbuild/linux-x64": "@esbuild/linux-x64@0.23.0", + "@esbuild/netbsd-x64": "@esbuild/netbsd-x64@0.23.0", + "@esbuild/openbsd-arm64": "@esbuild/openbsd-arm64@0.23.0", + "@esbuild/openbsd-x64": "@esbuild/openbsd-x64@0.23.0", + "@esbuild/sunos-x64": "@esbuild/sunos-x64@0.23.0", + "@esbuild/win32-arm64": "@esbuild/win32-arm64@0.23.0", + "@esbuild/win32-ia32": "@esbuild/win32-ia32@0.23.0", + "@esbuild/win32-x64": "@esbuild/win32-x64@0.23.0" } }, "escape-string-regexp@1.0.5": { @@ -907,7 +912,7 @@ "npm:@tanstack/query@5", "npm:@testing-library/react@16", "npm:@types/react@18", - "npm:esbuild@0.20", + "npm:esbuild@0.23", "npm:global-jsdom@24", "npm:react-dom@18", "npm:react-error-boundary@4", diff --git a/example/deno.jsonc b/example/deno.jsonc index b4afe5a..9e8bddb 100644 --- a/example/deno.jsonc +++ b/example/deno.jsonc @@ -57,7 +57,7 @@ "@std/log": "jsr:@std/log@0", "@std/path": "jsr:@std/path@1", "@std/testing": "jsr:@std/testing@1", - "esbuild": "npm:esbuild@0.20", + "esbuild": "npm:esbuild@0.23", "react": "npm:react@18", "@types/react": "npm:@types/react@18", "react-dom": "npm:react-dom@18", diff --git a/example/deno.lock b/example/deno.lock index 27426fd..e6b7ae5 100644 --- a/example/deno.lock +++ b/example/deno.lock @@ -301,7 +301,7 @@ "npm:@tanstack/query@5", "npm:@testing-library/react@16", "npm:@types/react@18", - "npm:esbuild@0.20", + "npm:esbuild@0.23", "npm:global-jsdom@24", "npm:react-dom@18", "npm:react-error-boundary@4", From c6a82fbdeaf000cee203f65f90dd25e6538dbf06 Mon Sep 17 00:00:00 2001 From: Kyle June Date: Fri, 30 Aug 2024 19:57:26 -0400 Subject: [PATCH 2/2] Remove global app state requirement and start making docs --- .github/codecov.yml | 2 +- .github/workflows/deploy.yml | 3 +- .gitignore | 3 + CONTRIBUTING.md | 6 +- LICENSE | 25 +- README.md | 365 +++----------- build.ts | 1 + client.tsx | 10 +- deno.jsonc | 25 +- deno.lock | 75 ++- docs/README.md | 23 + docs/ci-cd.md | 58 +++ docs/configuration.md | 56 +++ docs/development-tools.md | 21 + docs/error-handling.md | 19 + docs/forms.md | 16 + docs/getting-started.md | 692 ++++++++++++++++++++++++++ docs/http-middleware.md | 5 + docs/logging.md | 17 + docs/metadata.md | 56 +++ docs/routing.md | 816 +++++++++++++++++++++++++++++++ docs/state-management.md | 29 ++ docs/static-files.md | 6 + docs/styling.md | 20 + docs/testing.md | 56 +++ env.ts | 8 +- example/.gitignore | 3 + example/build.ts | 10 + example/deno.jsonc | 72 --- example/deno.lock | 314 ------------ example/dev.ts | 8 + example/log.ts | 7 +- example/main.ts | 3 +- example/models/posts.ts | 8 + example/routes/api/blog/posts.ts | 4 +- example/routes/blog/[id].ts | 4 +- example/routes/blog/index.ts | 4 +- example/routes/index.tsx | 15 +- example/routes/main.ts | 4 +- example/routes/main.tsx | 26 +- example/services/posts.tsx | 7 +- example/state.ts | 5 - mod.tsx | 6 +- server.tsx | 76 +-- test-utils.ts | 8 +- 45 files changed, 2121 insertions(+), 876 deletions(-) create mode 100644 docs/README.md create mode 100644 docs/ci-cd.md create mode 100644 docs/configuration.md create mode 100644 docs/development-tools.md create mode 100644 docs/error-handling.md create mode 100644 docs/forms.md create mode 100644 docs/getting-started.md create mode 100644 docs/http-middleware.md create mode 100644 docs/logging.md create mode 100644 docs/metadata.md create mode 100644 docs/routing.md create mode 100644 docs/state-management.md create mode 100644 docs/static-files.md create mode 100644 docs/styling.md create mode 100644 docs/testing.md create mode 100644 example/build.ts delete mode 100644 example/deno.jsonc delete mode 100644 example/deno.lock create mode 100644 example/dev.ts delete mode 100644 example/state.ts diff --git a/.github/codecov.yml b/.github/codecov.yml index 433ea1f..8761e60 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -7,4 +7,4 @@ coverage: default: informational: true ignore: - - "example/public/**/*" + - "example/public/build/**/*" diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index eac039d..6543cbd 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -5,7 +5,7 @@ on: app-directory: description: The directory containing the application to deploy type: string - default: '.' + default: '' entrypoint: description: The entrypoint file for running your application type: string @@ -29,7 +29,6 @@ jobs: with: deno-version: v1.x - name: Build - working-directory: ${{ inputs.app-directory }} run: deno task build-prod - name: Deploy to Deno Deploy uses: denoland/deployctl@v1 diff --git a/.gitignore b/.gitignore index 3cd37a6..b745d83 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,8 @@ # Coverage coverage +# Node modules +node_modules + # WIP test-utils.tsx diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 99b97f9..554a54c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,7 +1,7 @@ # Contributing -To contribute, create an issue or comment on an existing issue you would like to -work on. All code contributions require test coverage and must pass +To contribute, create an issue or comment on an existing issue that you would +like to work on. All code contributions require test coverage and must pass formatting/lint checks before being approved and merged. ## Prerequisites @@ -27,3 +27,5 @@ To run the application in development mode with live reloading, use This repository uses squash merging. If your branch is merged into main, you can get your branch back up to date with `deno task git-rebase`. Alternatively, you can delete your branch and create a new one off of the main branch. + +To learn more about working on this framework, see the [documentation](docs). diff --git a/LICENSE b/LICENSE index e717e64..a5af383 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,20 @@ MIT License -Copyright (c) 2023 Udibo +Copyright (c) 2024 Udibo -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index a106f86..201b724 100644 --- a/README.md +++ b/README.md @@ -6,319 +6,76 @@ [![codecov](https://codecov.io/gh/udibo/react-app/branch/main/graph/badge.svg?token=G5XCR01X8E)](https://codecov.io/gh/udibo/react-app) [![license](https://img.shields.io/github/license/udibo/react-app)](https://github.com/udibo/react-app/blob/main/LICENSE) -**NOTICE**: This is a WIP, not ready for use yet. - -A [React](https://reactjs.org/) Framework for [Deno](https://deno.land) that -makes it easy to create highly interactive applications that have server side -rendering with file based routing for both your UI and API. - -Apps are created using [React Router](https://reactrouter.com), -[React Helmet Async](https://www.npmjs.com/package/react-helmet-async), and -[Oak](https://deno.land/x/oak). +## Description + +Udibo React App is a [React](https://react.dev) framework for building +full-stack web applications with [Deno](https://deno.com). + +On the frontend, it uses [React Router](https://reactrouter.com) to handle +client side routing and +[React Helmet Async](https://www.npmjs.com/package/react-helmet-async) to manage +all of your changes to metadata for your website. Client side routing enables a +faster user experience by fetching the new data it needs for the next page when +navigating your site instead of having to fetch and re-render a whole new page. + +On the backend, it uses the [Oak](https://jsr.io/@oak/oak) middleware framework +for handling HTTP requests. If you are coming from using Node.js, the Oak +middleware framework is very similar to [Express](https://expressjs.com/) but +leverages async functions instead of callbacks to provide a better developer +experience. + +For bundling your user interface code for the browser, Udibo React App uses +[esbuild](https://esbuild.github.io/). It can generate bundles for your +application very quickly. Udibo React App's dev script enables automatic +rebuilds and reloads of your application as you make changes to it, which makes +it so you can see the effects of your changes quickly. + +Ontop of tying together those tools for the frontend and backend, Udibo React +App provides file based routing for both the user interface and the API. Routes +for the user interface will automatically be pre-rendered on the server by +default, making your application load quickly. + +Whether you're a beginner or an experienced developer, the Udibo React App +framework is a valuable resource for learning and building robust web +applications. It provides a solid foundation for creating scalable and +performant projects, enabling developers to deliver high-quality software +solutions. ## Features -- TypeScript out of the box -- File-system routing like [Next.js](https://nextjs.org), +- Supports TypeScript and JavaScript out of the box +- File based routing like [Next.js](https://nextjs.org), [Remix](https://remix.run/) and [Fresh](https://fresh.deno.dev) for both your application's UI and API -- Nested routes - Server side rendering - Easy to extend - Error boundaries that work both on the server and in the browser - Quick builds with hot reloading -- Can run on the edge with [Deno Deploy](https://deno.land/) - -## Usage - -You can look at the [examples](#examples) and documentation on JSR.io to learn -more about usage. - -### Examples - -**NOTICE**: The examples in separate repositories have not been updated yet. - -This repository contains one example for manually testing changes. To use it as -the base for a new project, you would need to update the `deno.jsonc` file to -use @udibo/react-app from the jsr.io. It's recommended to just fork one of the -many example repositories. The one linked below is the same as the one in the -example directory. - -- [Example](https://github.com/udibo/react-app-example): A basic example. - -The following examples are forks of the first example. They demonstate how easy -it is to extend Udibo React Apps. The README.md file in each of them describes -how it was done. - -- [Tailwindcss Example](https://github.com/udibo/react-app-example_tailwindcss): - A basic example using - [esbuild-plugin-postcss](https://github.com/deanc/esbuild-plugin-postcss) to - add Tailwindcss. -- [React Query Example](https://github.com/udibo/react-app-example-react-query): - A basic example using [React Query](https://tanstack.com/query/latest) for - asyncronous state management. - -### Tasks - -To run the tests, use `deno task test` or `deno task test-watch`. - -To check formatting and run lint, use `deno task check`. - -The following 2 commands can be used for creating builds. - -- `deno task build-dev`: Builds the application in development mode. -- `deno task build-prod`: Builds the application in production mode. - -A build must be generated before you can run an application. You can use the -following 2 commands to run the application. - -- `deno task run-dev`: Runs the application in development mode. -- `deno task run-prod`: Runs the application in production mode. - -To run the application in development mode with live reloading, use -`deno task dev`. - -When in development, identifiers are not minified and sourcemaps are generated -and linked. - -The commands ending in `-dev` and `-prod` set the `APP_ENV` and `NODE_ENV` -environment variables. The `NODE_ENV` environment variable is needed for react. -If you use the `deno task build` or `deno task run` tasks, you should make sure -that you set both of those environment variables. Those environment variables -are also needed if you deploy to Deno Deploy. - -### Deployment - -The GitHub workflows in this project can be used to run the tests and deploy -your project. You can look at the [examples](#examples) to see how it is done. - -If you don't plan on using Deno Deploy to host your App, you can base your own -deployment workflow on the deploy workflow in this repository. - -### Helmet - -[React Helmet Async](https://www.npmjs.com/package/react-helmet-async) is used -to manage all of your changes to the document head. You can add a Helmet tag to -any page that you would like to update the document head. - -- Supports all valid head tags: title, base, meta, link, script, noscript, and - style tags. -- Supports attributes for body, html and title tags. - -The following example can be found in the [main route](example/routes/main.tsx) -of the example in this repository. The Helmet in the main route of a directory -will apply to all routes within the directory. - -```tsx -import { Helmet } from "react-helmet-async"; - - - - -; -``` - -More examples of Helmet tag usage can be found in the -[React Helmet Reference Guide](https://github.com/nfl/react-helmet#reference-guide). - -### Routing - -Udibo React Apps have 2 types of routes, UI Routes and API Routes. UI Routes -that do not have an API Route with the same path will default to rendering the -application on the server. The naming convention is the same for both types of -routes. - -In each directory within the routes directory, the main and index files have a -special purpose. Neither the main or index file are required. - -For the UI routes: - -- The `index.tsx` or `index.jsx` file's react component will be used for - requests to the directory. -- The `main.tsx` or `main.tsx` file's react component will be used as a wrapper - around all routes in the directory. This can be useful for updating the head - for all routes in the directory. - -For the API routes: - -- The `index.ts` or `index.js` file will be used for requests to the directory. -- The `main.ts` or `main.js` file will be used before all routes in the - directory. This can be useful for adding middleware to all routes in the - directory. - -#### UI routes - -All tsx/jsx files in your routes directory will be treated as UI routes. They -should have a React component as their default export. - -It's recommended to use [React Router](https://reactrouter.com) components and -hooks for navigation within your app. Udibo React App uses Router Components to -connect all your routes together. - -Parameterized routes can be created by wrapping your parameter name in brackets. -For example, `/blog/[id].tsx` would handle requests like `/blog/123`, setting -the id paramter to `"123"`. The parameters can be accessed using the React -Router [useParams](https://reactrouter.com/en/main/hooks/use-params) hook. - -A wildcard route that will catch all requests that didn't have any other matches -can be created by naming a React file `[...].tsx`. The key for the parameter -will be "*" and can be accessed the same way as named parameters. - -#### API routes - -All ts/js files in your routes directory will be treated as API routes. They -should have an Oak router as their default export. - -If you create an API route that relates to a UI route, it should call -`state.app.render()` to render the app on the server. The render function will -render the application as a readable stream and respond to the client with it. - -Parameterized and wildcard routes work just like they do for UI routes. But the -parameters are stored on the context's params property. The key for wildcard -parameters will be "0". - -```ts -import { Router } from "@udibo/react-app/server"; - -import { AppState } from "../state.ts"; - -export default new Router() - .get("/", async (context) => { - const { state, params } = context; - await state.app.render(); - }); -``` - -#### Error handling - -There are 2 ways to add error handling to your UI routes. - -The easiest way is to add an ErrorFallback export to your UI Route. The related -API router will automatically have error boundary middleware added to it, that -will match the route path unless a different one is specified via a boundary -export. For example, the `/blog/[id]` route would have the error's boundary -identifier set to `"/blog/[id]"`. Then the AppErrorBoundary added around your -component will have a matching boundary identifier. - -Here is an example of an simple ErrorFallback. If you'd like to use it as is, -it's exported as DefaultErrorFallback. - -```ts -import { FallbackProps } from "@udibo/react-app"; - -// ... - -export function ErrorFallback( - { error, resetErrorBoundary }: FallbackProps, -) { - const reset = useAutoReset(resetErrorBoundary); - - return ( -
-

{error.message || "Something went wrong"}

- {isDevelopment() && error.stack ?
{error.stack}
: null} - -
- ); -} -``` - -If you'd like to nest an error boundary within your UI route component, you can -use AppErrorBoundary or withAppErrorBoundary. If you do it this way, you will -need to either export a boundary string from your route file or manually add -errorBoundary middleware to your router to ensure any errors in that route are -associated with the AppErrorBoundary you added. - -```tsx -export const boundary = "MyComponentErrorBoundary"; -``` - -```ts -const router = new Router() - .use(errorBoundary("MyComponentErrorBoundary")); -``` - -Then the related UI route component needs to either use `withAppErrorBoundary` -or `AppErrorBoundary` to be able to catch the error during rendering. The -boundary identifier must match the one on the server. - -```tsx -const MyComponentSafe = withAppErrorBoundary(MyComponent, { - FallbackComponent: DefaultErrorFallback, - boundary: "MyComponentErrorBoundary", -}); -``` - -```tsx - - -; -``` - -#### Ignore files - -You can have the build script ignore files in your routes directory by adding an -underscore prefix to their name. - -#### Build artifacts - -The only reserved file names are `_main.ts` and `_main.tsx` at the root of your -routes directory. Those files are generated during the build process. - -- `_main.ts`: Exports an Oak router that connects all the Oak router files - together. -- `_main.tsx`: Exports a React Router route object that connects all your React - component files together. - -### Disabling server side rendering - -All pages will be rendered server side by default. If you have a component you -don't want to render on the server, you can disable it by having it return the -fallback on the server. You can use `isServer()` to determine if the code is -running on the server or in the browser. In the example's blog, you could have -it only get the post when rendering in the browser by setting post to undefined -when on the server like shown below. - -```tsx -const post = isServer() ? undefined : getPost(id); -return post - ? ( - <> - - {post.title} - - -

{post.title}

-

{post.content}

- - ) - : ( - <> - - Loading... - -

Loading...

- - ); -``` - -The actual example currently does render the post on the server. - -### Server side rendering with data fetching - -To render a route that loads data on the server, you can add a matching Oak -router that will cache the information being fetched before rendering the -application. The example in this repository uses the application's initial state -to store the cached responses but that's not the only way to do it. It's -recommended that you use a library like -[React Query](https://tanstack.com/query/latest) to get your data. +- Can run on the edge with [Deno Deploy](https://deno.com/deploy) + +## Documentation + +The documentation for how to use all of the entrypoints for this framework's +package can be found on JSR +([@udibo/react-app](https://jsr.io/@udibo/react-app/doc)). + +In addition to that documentation for the code below is a list of guides for how +to use the framework. + +- [Getting Started](docs/getting-started.md) +- [Configuration](docs/configuration.md) +- [Development tools](docs/development-tools.md) +- [Routing](docs/routing.md) +- [HTTP Middleware](docs/http-middleware.md) +- [Static Files](docs/static-files.md) +- [Metadata](docs/metadata.md) +- [Styling](docs/styling.md) +- [State Management](docs/state-management.md) +- [Forms](docs/forms.md) +- [Error Handling](docs/error-handling.md) +- [Testing](docs/testing.md) +- [Logging](docs/logging.md) +- [CI/CD](docs/ci-cd.md) ## Contributing diff --git a/build.ts b/build.ts index 7b540de..6497db2 100644 --- a/build.ts +++ b/build.ts @@ -633,6 +633,7 @@ export async function build(options: BuildOptions = {}): Promise { if (!await exists(configPath)) { throw new Error("Could not find deno config file"); } + configPath = path.resolve(configPath); const outdir = path.join( publicUrl, diff --git a/client.tsx b/client.tsx index 99a4936..00e61b0 100644 --- a/client.tsx +++ b/client.tsx @@ -42,7 +42,7 @@ type AppOptions = HydrateOptions; * @param options - The configuration for rendering the application. */ function App< - AppState extends Record = Record< + SharedState extends Record = Record< string | number, unknown >, @@ -80,8 +80,8 @@ function App< globalThis.addEventListener("beforeunload", () => source.close()); } - const initialState = (window as AppWindow).app?.initialState ?? - {} as AppState; + const initialState = (window as AppWindow).app?.initialState ?? + {} as SharedState; const appErrorContext = { error }; return ( @@ -114,7 +114,7 @@ function App< * The route object is the default export from the generated `_main.tsx` file in your routes directory. */ export function hydrate< - AppState extends Record = Record< + SharedState extends Record = Record< string | number, unknown >, @@ -124,7 +124,7 @@ export function hydrate< startTransition(() => { hydrateRoot( document.getElementById("root"), - route={route} />, + route={route} />, ); }); diff --git a/deno.jsonc b/deno.jsonc index 36871c0..7eff323 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -1,6 +1,6 @@ { "name": "@udibo/react-app", - "version": "0.23.0", + "version": "0.24.0", "exports": { ".": "./mod.tsx", "./build": "./build.ts", @@ -29,19 +29,19 @@ }, "tasks": { // Builds the application. - "build": "cd ./example && deno task build", + "build": "cd ./example && deno run -A ./build.ts", // Builds the application in development mode. - "build-dev": "cd ./example && deno task build-dev", + "build-dev": "export APP_ENV=development NODE_ENV=development && deno task build", // Builds the application in production mode. - "build-prod": "cd ./example && deno task build-prod", + "build-prod": "export APP_ENV=production NODE_ENV=production && deno task build", // Builds and runs the application in development mode, with hot reloading. - "dev": "cd ./example && deno task dev", + "dev": "export APP_ENV=development NODE_ENV=development && cd ./example && deno run -A ./dev.ts", // Runs the application. Requires the application to be built first. - "run": "cd ./example && deno task run", + "run": "cd ./example && deno run -A ./main.ts", // Runs the application in development mode. Requires the application to be built first. - "run-dev": "cd ./example && deno task run-dev", + "run-dev": "export APP_ENV=development NODE_ENV=development && deno task run", // Runs the application in production mode. Requires the application to be built first. - "run-prod": "cd ./example && deno task run-prod", + "run-prod": "export APP_ENV=production NODE_ENV=production && deno task run", // Runs the tests. "test": "export APP_ENV=test NODE_ENV=development && deno test -A --trace-leaks", // Runs the tests in watch mode. @@ -57,6 +57,7 @@ "jsxImportSource": "react", "jsxImportSourceTypes": "@types/react" }, + "nodeModulesDir": true, "lint": { "exclude": [ "coverage", @@ -71,8 +72,8 @@ "imports": { "@udibo/http-error": "jsr:@udibo/http-error@0", "@udibo/react-app": "./mod.tsx", - "@udibo/react-app/build": "./build.tsx", - "@udibo/react-app/dev": "./dev.tsx", + "@udibo/react-app/build": "./build.ts", + "@udibo/react-app/dev": "./dev.ts", "@udibo/react-app/server": "./server.tsx", "@udibo/react-app/client": "./client.tsx", "@udibo/react-app/test-utils": "./test-utils.tsx", @@ -81,7 +82,7 @@ "@oak/oak": "jsr:@oak/oak@16", "@std/assert": "jsr:@std/assert@1", "@std/async": "jsr:@std/async@1", - "@std/fs": "jsr:@std/fs@0", + "@std/fs": "jsr:@std/fs@1", "@std/log": "jsr:@std/log@0", "@std/path": "jsr:@std/path@1", "@std/testing": "jsr:@std/testing@1", @@ -95,6 +96,6 @@ "serialize-javascript": "npm:serialize-javascript@6", "@testing-library/react": "npm:@testing-library/react@16", "global-jsdom": "npm:global-jsdom@24", - "@tanstack/query": "npm:@tanstack/query@5" + "@tanstack/react-query": "npm:@tanstack/react-query@5" } } diff --git a/deno.lock b/deno.lock index 9a60e16..11c5a42 100644 --- a/deno.lock +++ b/deno.lock @@ -21,7 +21,7 @@ "jsr:@std/encoding@1.0.0-rc.2": "jsr:@std/encoding@1.0.0-rc.2", "jsr:@std/encoding@^0.223.0": "jsr:@std/encoding@0.223.0", "jsr:@std/fmt@^1.0.0-rc.1": "jsr:@std/fmt@1.0.0", - "jsr:@std/fs@0": "jsr:@std/fs@0.229.3", + "jsr:@std/fs@1": "jsr:@std/fs@1.0.1", "jsr:@std/fs@^1.0.0-rc.5": "jsr:@std/fs@1.0.1", "jsr:@std/http@0": "jsr:@std/http@0.224.5", "jsr:@std/http@0.223": "jsr:@std/http@0.223.0", @@ -37,7 +37,7 @@ "jsr:@std/path@0.213": "jsr:@std/path@0.213.1", "jsr:@std/path@0.223": "jsr:@std/path@0.223.0", "jsr:@std/path@1": "jsr:@std/path@1.0.2", - "jsr:@std/path@1.0.0-rc.1": "jsr:@std/path@1.0.0-rc.1", + "jsr:@std/path@^1.0.2": "jsr:@std/path@1.0.2", "jsr:@std/testing@1": "jsr:@std/testing@1.0.0", "jsr:@udibo/http-error@0": "jsr:@udibo/http-error@0.8.2", "npm:@testing-library/react@16": "npm:@testing-library/react@16.0.0_@testing-library+dom@10.4.0_@types+react@18.3.3_react@18.3.1_react-dom@18.3.1__react@18.3.1", @@ -48,9 +48,12 @@ "npm:path-to-regexp@6.2.1": "npm:path-to-regexp@6.2.1", "npm:react-dom@18": "npm:react-dom@18.3.1_react@18.3.1", "npm:react-error-boundary@4": "npm:react-error-boundary@4.0.13_react@18.3.1", + "npm:react-error-boundary@4.0.13": "npm:react-error-boundary@4.0.13_react@18.3.1", "npm:react-helmet-async@2": "npm:react-helmet-async@2.0.5_react@18.3.1", - "npm:react-router-dom@6": "npm:react-router-dom@6.24.0_react@18.3.1_react-dom@18.3.1__react@18.3.1", + "npm:react-router-dom@6": "npm:react-router-dom@6.26.0_react@18.3.1_react-dom@18.3.1__react@18.3.1", + "npm:react-router-dom@6.26.0": "npm:react-router-dom@6.26.0_react@18.3.1_react-dom@18.3.1__react@18.3.1", "npm:react@18": "npm:react@18.3.1", + "npm:react@18.3.1": "npm:react@18.3.1", "npm:serialize-javascript@6": "npm:serialize-javascript@6.0.2" }, "jsr": { @@ -104,9 +107,6 @@ "jsr:@std/internal@^1.0.1" ] }, - "@std/async@1.0.1": { - "integrity": "3c7f6324a8a1b47ca657e5a349b511c9a6c2c0729e9d66b223c9ecaac0753ecb" - }, "@std/async@1.0.2": { "integrity": "36e7f0f922c843b45df546857d269f01ed4d0406aced2a6639eac325b2435e43" }, @@ -141,21 +141,12 @@ "@std/fmt@1.0.0": { "integrity": "8a95c9fdbb61559418ccbc0f536080cf43341655e1444f9d375a66886ceaaa3d" }, - "@std/fmt@1.0.0-rc.1": { - "integrity": "3dbbd12f1704c62b5bd33c9ff928c8df47cd5c75f6637201a8c21298b28e43bc" - }, - "@std/fs@0.229.3": { - "integrity": "783bca21f24da92e04c3893c9e79653227ab016c48e96b3078377ebd5222e6eb", + "@std/fs@1.0.1": { + "integrity": "d6914ca2c21abe591f733b31dbe6331e446815e513e2451b3b9e472daddfefcb", "dependencies": [ - "jsr:@std/path@1.0.0-rc.1" + "jsr:@std/path@^1.0.2" ] }, - "@std/fs@1.0.0": { - "integrity": "d72e4a125af7168d717a2ed1dca77a728b422b0d138fd20579e3fa41a77da943" - }, - "@std/fs@1.0.1": { - "integrity": "d6914ca2c21abe591f733b31dbe6331e446815e513e2451b3b9e472daddfefcb" - }, "@std/http@0.223.0": { "integrity": "15ab8a0c5a7e9d5be017a15b01600f20f66602ceec48b378939fa24fcec522aa", "dependencies": [ @@ -178,9 +169,6 @@ "jsr:@std/bytes@^0.223.0" ] }, - "@std/io@0.224.3": { - "integrity": "b402edeb99c6b3778d9ae3e9927bc9085b170b41e5a09bbb7064ab2ee394ae2f" - }, "@std/io@0.224.4": { "integrity": "bce1151765e4e70e376039fd72c71672b4d4aae363878a5ee3e58361b81197ec" }, @@ -220,9 +208,6 @@ "jsr:@std/assert@^0.223.0" ] }, - "@std/path@1.0.0-rc.1": { - "integrity": "b8c00ae2f19106a6bb7cbf1ab9be52aa70de1605daeb2dbdc4f87a7cbaf10ff6" - }, "@std/path@1.0.2": { "integrity": "a452174603f8c620bd278a380c596437a9eef50c891c64b85812f735245d9ec7" }, @@ -257,8 +242,8 @@ "picocolors": "picocolors@1.0.1" } }, - "@babel/runtime@7.24.7": { - "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", + "@babel/runtime@7.25.0": { + "integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==", "dependencies": { "regenerator-runtime": "regenerator-runtime@0.14.1" } @@ -359,15 +344,15 @@ "integrity": "sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==", "dependencies": {} }, - "@remix-run/router@1.17.0": { - "integrity": "sha512-2D6XaHEVvkCn682XBnipbJjgZUU7xjLtA4dGJRBVUKpEaDYOZMENZoZjAOSb7qirxt5RupjzZxz4fK2FO+EFPw==", + "@remix-run/router@1.19.0": { + "integrity": "sha512-zDICCLKEwbVYTS6TjYaWtHXxkdoUvD/QXvyVZjGCsWz5vyH7aFeONlPffPdW+Y/t6KT0MgXb2Mfjun9YpWN1dA==", "dependencies": {} }, "@testing-library/dom@10.4.0": { "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", "dependencies": { "@babel/code-frame": "@babel/code-frame@7.24.7", - "@babel/runtime": "@babel/runtime@7.24.7", + "@babel/runtime": "@babel/runtime@7.25.0", "@types/aria-query": "@types/aria-query@5.0.4", "aria-query": "aria-query@5.3.0", "chalk": "chalk@4.1.2", @@ -379,7 +364,7 @@ "@testing-library/react@16.0.0_@testing-library+dom@10.4.0_@types+react@18.3.3_react@18.3.1_react-dom@18.3.1__react@18.3.1": { "integrity": "sha512-guuxUKRWQ+FgNX0h0NS0FIq3Q3uLtWVpBzcLOggmfMoUpgBnzBzvLLd4fbm6yS8ydJd94cIfY4yP9qUQjM2KwQ==", "dependencies": { - "@babel/runtime": "@babel/runtime@7.24.7", + "@babel/runtime": "@babel/runtime@7.25.0", "@testing-library/dom": "@testing-library/dom@10.4.0", "@types/react": "@types/react@18.3.3", "react": "react@18.3.1", @@ -408,7 +393,7 @@ "agent-base@7.1.1": { "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", "dependencies": { - "debug": "debug@4.3.5" + "debug": "debug@4.3.6" } }, "ansi-regex@5.0.1": { @@ -499,8 +484,8 @@ "whatwg-url": "whatwg-url@14.0.0" } }, - "debug@4.3.5": { - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "debug@4.3.6": { + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "dependencies": { "ms": "ms@2.1.2" } @@ -590,14 +575,14 @@ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dependencies": { "agent-base": "agent-base@7.1.1", - "debug": "debug@4.3.5" + "debug": "debug@4.3.6" } }, "https-proxy-agent@7.0.5": { "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", "dependencies": { "agent-base": "agent-base@7.1.1", - "debug": "debug@4.3.5" + "debug": "debug@4.3.6" } }, "iconv-lite@0.6.3": { @@ -725,7 +710,7 @@ "react-error-boundary@4.0.13_react@18.3.1": { "integrity": "sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==", "dependencies": { - "@babel/runtime": "@babel/runtime@7.24.7", + "@babel/runtime": "@babel/runtime@7.25.0", "react": "react@18.3.1" } }, @@ -746,19 +731,19 @@ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dependencies": {} }, - "react-router-dom@6.24.0_react@18.3.1_react-dom@18.3.1__react@18.3.1": { - "integrity": "sha512-960sKuau6/yEwS8e+NVEidYQb1hNjAYM327gjEyXlc6r3Skf2vtwuJ2l7lssdegD2YjoKG5l8MsVyeTDlVeY8g==", + "react-router-dom@6.26.0_react@18.3.1_react-dom@18.3.1__react@18.3.1": { + "integrity": "sha512-RRGUIiDtLrkX3uYcFiCIxKFWMcWQGMojpYZfcstc63A1+sSnVgILGIm9gNUA6na3Fm1QuPGSBQH2EMbAZOnMsQ==", "dependencies": { - "@remix-run/router": "@remix-run/router@1.17.0", + "@remix-run/router": "@remix-run/router@1.19.0", "react": "react@18.3.1", "react-dom": "react-dom@18.3.1_react@18.3.1", - "react-router": "react-router@6.24.0_react@18.3.1" + "react-router": "react-router@6.26.0_react@18.3.1" } }, - "react-router@6.24.0_react@18.3.1": { - "integrity": "sha512-sQrgJ5bXk7vbcC4BxQxeNa5UmboFm35we1AFK0VvQaz9g0LzxEIuLOhHIoZ8rnu9BO21ishGeL9no1WB76W/eg==", + "react-router@6.26.0_react@18.3.1": { + "integrity": "sha512-wVQq0/iFYd3iZ9H2l3N3k4PL8EEHcb0XlU2Na8nEwmiXgIUElEH6gaJDtUQxJ+JFzmIXaQjfdpcGWaM6IoQGxg==", "dependencies": { - "@remix-run/router": "@remix-run/router@1.17.0", + "@remix-run/router": "@remix-run/router@1.19.0", "react": "react@18.3.1" } }, @@ -904,12 +889,12 @@ "jsr:@oak/oak@16", "jsr:@std/assert@1", "jsr:@std/async@1", - "jsr:@std/fs@0", + "jsr:@std/fs@1", "jsr:@std/log@0", "jsr:@std/path@1", "jsr:@std/testing@1", "jsr:@udibo/http-error@0", - "npm:@tanstack/query@5", + "npm:@tanstack/react-query@5", "npm:@testing-library/react@16", "npm:@types/react@18", "npm:esbuild@0.23", diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..a11a815 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,23 @@ +# Documentation + +The documentation for how to use all of the entrypoints for this framework's +package can be found on JSR +([@udibo/react-app](https://jsr.io/@udibo/react-app/doc)). + +In addition to that documentation for the code below is a list of guides for how +to use the framework. + +- [Getting Started](getting-started.md) +- [Configuration](configuration.md) +- [Development tools](development-tools.md) +- [Routing](routing.md) +- [HTTP Middleware](http-middleware.md) +- [Static Files](static-files.md) +- [Metadata](metadata.md) +- [Styling](styling.md) +- [State Management](state-management.md) +- [Forms](forms.md) +- [Error Handling](error-handling.md) +- [Testing](testing.md) +- [Logging](logging.md) +- [CI/CD](ci-cd.md) diff --git a/docs/ci-cd.md b/docs/ci-cd.md new file mode 100644 index 0000000..b413ba3 --- /dev/null +++ b/docs/ci-cd.md @@ -0,0 +1,58 @@ +# CI/CD + +Continuous integration and continuous deployment is a process for developing +apps faster, safer, and more efficiently. It is the automation of the manual, +repetitive, and error prone tasks involved in integrating and deploying changes. +The [ci](#ci) section covers building and testing your application. The +[CD](#cd) section covers automating deploying your changes. + +- [CI/CD](#cicd) + - [CI](#ci) + - [GitHub actions](#github-actions) + - [CD](#cd) + - [GitHub actions](#github-actions-1) + - [Deno Deploy](#deno-deploy) + - [AWS](#aws) + +## CI + +Continuous integration is all about building and testing your application to +ensure changes made will work correctly if merged into the codebase. It will +help you identify any conflicts or error caused by changes. This can be used to +show the results of testing a change. It also helps developers identify and +correct issues earlier. + +GitHub actions is one of the environments where this automation can exist. Other +code hosting platforms have similar offerings. While this document doesn't cover +all of them, you can develop it based on the steps described in this section. + +### GitHub actions + +TODO: Go over using our CI workflow and what our CI workflow example is doing. +Describe how they can take and modify our CI workflow if it doesn't meet their +needs. + +## CD + +Continuous deployment is about building and deploying your application after a +change has passed CI. It can involve deploying to both staging and production +environments. Automating deployment can help release software updates to +customers as soon as they have been validated. + +GitHub actions is one of the environments where this automation can exist. Other +code hosting platforms have similar offerings. While this document doesn't cover +all of them, you can develop it based on the steps described in this section. + +### GitHub actions + +TODO: Go over using our CD workflow and what our CD workflow example is doing. +Describe how they can take and modify our CD workflow if it doesn't meet their +needs. + +#### Deno Deploy + +TODO: Write instructions on this. Link back to Deno Deploy documentation. + +#### AWS + +TODO: Describe how to do this with docker. diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..d7fa772 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,56 @@ +# Configuration + +TODO: Make an outline then fill it in with details. + +- [Configuration](#configuration) + - [Tasks](#tasks) + - [Compiler options](#compiler-options) + - [Linting](#linting) + - [Formatting](#formatting) + - [Imports](#imports) + - [Application](#application) + - [Build](#build) + - [esbuild](#esbuild) + - [Development](#development) + - [Environment variables](#environment-variables) + +## Tasks + +To learn more about using the default tasks, see the +[tasks](getting-started.md#tasks) section in the getting started guide. + +If you need to customize your build options to be different from the default, +follow the instructions for adding the [build.ts](getting-started.md#buildts) +and [dev.ts](getting-started.md#devts) files in the getting started guide. + +You can add any tasks that you want to your configuration, for more information +on how to do so, see Deno's +[task runner](https://docs.deno.com/runtime/manual/tools/task_runner/) guide. + +If you remove or rename any of the default tasks and you make use of our GitHub +workflows, you may need to modify them to use different tasks. See the +[CI/CD](ci-cd.md) guide for more information. + +## Compiler options + +## Linting + +## Formatting + +## Imports + +## Application + +## Build + +TODO: Include link to esbuild plugins. Link to our styling guide for examples of +using common styling plugins for esbuild. + +### esbuild + +## Development + +## Environment variables + +TODO: Cover the basics of environment variables, with a focus on how to use +dotfiles for development, production, and test environment variables. diff --git a/docs/development-tools.md b/docs/development-tools.md new file mode 100644 index 0000000..d177d54 --- /dev/null +++ b/docs/development-tools.md @@ -0,0 +1,21 @@ +# Development tools + +TODO: Make an outline then fill it in with details. + +- [Development tools](#development-tools) + - [VS Code](#vs-code) + - [Configuration](#configuration) + - [Debugging](#debugging) + - [Docker](#docker) + +## VS Code + +### Configuration + +TODO: Cover how to configure VS Code. + +### Debugging + +TODO: Explain how to use the debugger + +## Docker diff --git a/docs/error-handling.md b/docs/error-handling.md new file mode 100644 index 0000000..d560616 --- /dev/null +++ b/docs/error-handling.md @@ -0,0 +1,19 @@ +# Error handling + +- [Error handling](#error-handling) + - [UI](#ui) + - [API](#api) + +## UI + +For UI routes, error handling typically involves using the included error +handling components and hooks. This allows you to catch and display errors in a +user-friendly manner, preventing the entire application from crashing due to a +single component error. + +## API + +In API routes, error handling involves catching and properly formatting errors, +setting appropriate HTTP status codes, and potentially logging errors for +debugging purposes. The framework provides utilities to streamline this process +and ensure consistent error responses across your API. diff --git a/docs/forms.md b/docs/forms.md new file mode 100644 index 0000000..dce8a13 --- /dev/null +++ b/docs/forms.md @@ -0,0 +1,16 @@ +# Forms + +TODO: Make an outline then fill it in with details. + +- [Forms](#forms) + - [Basic](#basic) + - [React Hook Forms](#react-hook-forms) + +## Basic + +TODO: Cover using basic HTML forms and handling them with oak routes. + +## React Hook Forms + +TODO: Explain the basics for how to use React Hook Forms. Link to their +documentation. diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..9359d55 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,692 @@ +# Getting Started + +This guide covers the basics of setting up, developing, and deploying an +application using the Udibo React App framework. + +- [Getting Started](#getting-started) + - [Setup](#setup) + - [Copy example project](#copy-example-project) + - [Manually create all the files](#manually-create-all-the-files) + - [Required files](#required-files) + - [deno.jsonc](#denojsonc) + - [main.ts](#maints) + - [log.ts](#logts) + - [routes/main.ts](#routesmaints) + - [routes/main.tsx](#routesmaintsx) + - [routes/index.tsx](#routesindextsx) + - [Optional files](#optional-files) + - [build.ts](#buildts) + - [dev.ts](#devts) + - [.gitignore](#gitignore) + - [test-utils.tsx](#test-utilstsx) + - [.github/\*](#github) + - [.github/workflows/main.yml](#githubworkflowsmainyml) + - [.github/codecov.yml](#githubcodecovyml) + - [.vscode/\*](#vscode) + - [.vscode/settings.json](#vscodesettingsjson) + - [Tasks](#tasks) + - [Routing](#routing) + - [Error handling](#error-handling) + - [Metadata](#metadata) + - [Logging](#logging) + - [Testing](#testing) + - [Deployment](#deployment) + +## Setup + +There are 2 options for how to get started, you can either +[copy the example project](#copy-example-project) or +[manually create all the required files](#manually-create-all-the-required-files). + +### Copy example project + +For copying the example, you can use git clone or download it from the GitHub +repo. Below is a link to the example. + +https://github.com/udibo/react-app-example + +If you go to that link and click the Code button, the dropdown includes the +option to download it or the url for cloning it with git. + +If you have git installed, you can run the following command to clone it. + +```sh +git clone https://github.com/udibo/react-app-example.git +``` + +For more information on cloning a repository, see +[this guide](https://docs.github.com/en/repositories/creating-and-managing-repositories/cloning-a-repository#about-cloning-a-repository) +from GitHub. + +For more information about the files in that example, see the +[required files](#required-files) and [optional files](#optional-files) +sections. + +### Manually create all the files + +To get started, you are going to need a folder for your project and to create a +few files in that directory. The following 2 sections lists those files and +explains their purpose. + +## Required files + +- [deno.jsonc](#denojsonc): The configuration for deno and contains a set of + shortcuts for doing tasks. +- [main.ts](#maints): The main entrypoint for running the application. +- [log.ts](#logts): The configuration for how logs are handled. +- [routes/main.ts](#routesmaints): A wrapper around the server side of the + application. +- [routes/main.tsx](#routesmaintsx): A wrapper around the client side of the + application. +- [routes/index.tsx](#routesindextsx): The homepage for the application. + +### deno.jsonc + +This is the configuration for deno and contains a set of shortcuts for doing +tasks. + +All of the tasks in this file can be used by typing `deno task [task]`. For +example: `deno task dev` would build and run the application in development mode +with hot reloading. All of the configuration options besides the tasks are +required. For more information about the tasks, see the [tasks section](#tasks). + +The `nodeModulesDir` option is set to `true` in this configuration. This is +necessary for compatibility with certain VS Code extensions, such as the +TailwindCSS extension, and for tools like Playwright +([see this comment](https://github.com/denoland/deno/issues/16899#issuecomment-2307899834)). + +While Deno typically doesn't use a `node_modules` directory, enabling this +option ensures better compatibility with tools and extensions that expect a +Node.js-like environment. + +```jsonc +{ + "tasks": { + // Builds the application. + "build": "deno run -A --config=deno.jsonc jsr:@udibo/react-app@0.24/build", + // Builds the application in development mode. + "build-dev": "export APP_ENV=development NODE_ENV=development && deno task build", + // Builds the application in production mode. + "build-prod": "export APP_ENV=production NODE_ENV=production && deno task build", + // Builds and runs the application in development mode, with hot reloading. + "dev": "export APP_ENV=development NODE_ENV=development && deno run -A --config=deno.jsonc jsr:@udibo/react-app@0.24/dev", + // Runs the application. Requires the application to be built first. + "run": "deno run -A ./main.ts", + // Runs the application in development mode. Requires the application to be built first. + "run-dev": "export APP_ENV=development NODE_ENV=development && deno task run", + // Runs the application in production mode. Requires the application to be built first. + "run-prod": "export APP_ENV=production NODE_ENV=production && deno task run", + // Runs the tests. + "test": "export APP_ENV=test NODE_ENV=development && deno test -A --trace-leaks", + // Runs the tests in watch mode. + "test-watch": "export APP_ENV=test NODE_ENV=development && deno test -A --trace-leaks --watch", + // Checks the formatting and runs the linter. + "check": "deno lint && deno fmt --check", + // Gets your branch up to date with master after a squash merge. + "git-rebase": "git fetch origin main && git rebase --onto origin/main HEAD" + }, + "compilerOptions": { + "lib": ["esnext", "dom", "dom.iterable", "dom.asynciterable", "deno.ns"], + "jsx": "react-jsx", + "jsxImportSource": "react", + "jsxImportSourceTypes": "@types/react" + }, + "nodeModulesDir": true, + "lint": { + "exclude": ["public/build", "routes/_main.ts", "routes/_main.tsx"] + }, + "fmt": { + "exclude": ["public/build"] + }, + "imports": { + "/": "./", + "./": "./", + "@udibo/react-app": "jsr:@udibo/react-app@0.22", + "@std/assert": "jsr:@std/assert@1", + "@std/log": "jsr:@std/log@0", + "@std/path": "jsr:@std/path@1", + "@std/testing": "jsr:@std/testing@1", + "react": "npm:react@18", + "@types/react": "npm:@types/react@18", + "react-router-dom": "npm:react-router-dom@6", + "react-helmet-async": "npm:react-helmet-async@2", + "@testing-library/react": "npm:@testing-library/react@16", + "global-jsdom": "npm:global-jsdom@24" + } +} +``` + +### main.ts + +The main entrypoint for running the application. + +The serve function starts the application on the specified port. This example +uses port 9000 but you can use any port you want. Importing the "./log.ts" file +ensures logging is done with your logging configuration specified in that file. +The two _main files in the routes directory are the route and router generated +by the build script. + +```ts +import * as path from "@std/path"; +import { serve } from "@udibo/react-app/server"; + +import route from "./routes/_main.tsx"; +import router from "./routes/_main.ts"; +import "./log.ts"; + +await serve({ + port: 9000, + router, + route, + workingDirectory: path.dirname(path.fromFileUrl(import.meta.url)), +}); +``` + +### log.ts + +The configuration for how logs are handled. + +The react-app logger is used for logs made by the @udibo/react-app package. You +can change the configuration however you'd like. The logFormatter is designed to +handle the logs emitted by react-app. See the documentation for that function +for more details about how it expects calls to be made to the logging functions +and how those calls will translate into log messages. + +```ts +import * as log from "@std/log"; +import { isDevelopment, isServer, logFormatter } from "@udibo/react-app"; + +const level = isDevelopment() ? "DEBUG" : "INFO"; +log.setup({ + handlers: { + default: new log.ConsoleHandler(level, { + formatter: logFormatter, + useColors: isServer(), + }), + }, + loggers: { "react-app": { level, handlers: ["default"] } }, +}); +``` + +For more information, view our [logging guide](logging.md). + +### routes/main.ts + +A wrapper around the server side of the application. + +This is where you should add middleware that you want to apply to all requests. +In the following example, it adds middleware that will set the response time +header and log information about the request. That middleware is not required, +it is just an example of middleware. For more information about middleware, view +our [HTTP middleware guide](http-middleware.md). + +Each subdirectory in the routes directory can have a `main.ts` file that applies +middleware for all routes in that subdirectory. You can learn more about this in +the [routing section](routing.md). + +```ts +import { Router } from "@udibo/react-app/server"; +import * as log from "@std/log"; + +export default new Router() + .use(async (context, next) => { + const { request, response } = context; + const start = Date.now(); + try { + await next(); + } finally { + const responseTime = Date.now() - start; + response.headers.set("X-Response-Time", `${responseTime}ms`); + log.info( + `${request.method} ${request.url.href}`, + { status: response.status, responseTime }, + ); + } + }); +``` + +### routes/main.tsx + +A wrapper around the client side of the application. + +This is a good place to define the layout for your website along with default +metadata for all pages. It's a good place to add providers for context shared by +your entire application. For example, theming context or information about the +current session. If your server has context you would like relayed to the +client, you can use the `useInitialState` function to access it. + +Each subdirectory in the routes directory can have a `main.tsx` file that wraps +the entire route path. You can learn more about this in the [routing](#routing) +section. + +```tsx +import { Suspense } from "react"; +import { Link, Outlet } from "npm:react-router-dom@6"; +import { DefaultErrorFallback, ErrorBoundary, Helmet } from "@udibo/react-app"; +import "../log.ts"; + +import { Loading } from "../components/loading.tsx"; + +const navLinks = [ + { label: "Home", to: "/" }, + { label: "About", to: "/about" }, + { label: "Blog", to: "/blog" }, + { label: "Fake", to: "/fake" }, +]; + +export default function Main() { + return ( + <> + + + + +
    + {navLinks.map((link) => ( +
  • + {link.label} +
  • + ))} +
+ }> + + + + + + ); +} +``` + +The ErrorBoundary around the Outlet will create an error boundary for the entire +application, if an error is thrown and not caught by another error boundary +first, the fallback component will be shown. For more information, view our +[error handling guide](error-handling.md). + +### routes/index.tsx + +The homepage for the application. The contents of the component aren't required, +just an example of a homepage. + +Each subdirectory in the routes directory can have an `index.tsx` file that +represents the default view for that path. You can learn more about this in the +[routing section](routing.md). + +```tsx +import { Helmet } from "@udibo/react-app"; + +export default function Index() { + return ( + <> + + Home + + +

Home

+

This is a basic example of a Udibo React App.

+ + + ); +} +``` + +## Optional files + +- [build.ts](#buildts): Builds the application with your own build options. +- [dev.ts](#devts): Starts a development server using your own build options. +- [test-utils.tsx](#test-utilstsx): Provides additional utilities for testing + your application. +- [.gitignore](#gitignore): Contains a list of files that should not be + committed. + +### build.ts + +If the default build configuration settings are insufficient for your +application, you can create a build script like shown below: + +```ts +import { buildOnce, type BuildOptions } from "@udibo/react-app/build"; +import "./log.ts"; + +// export the buildOptions so that you can use them in your dev script. +// You will need a dev script if you have non default build options. +export const buildOptions: BuildOptions = { + // Add your own build options here if the defaults are not sufficient. +}; + +if (import.meta.main) { + buildOnce(buildOptions); +} +``` + +Then update your deno config file's tasks section to use your build script: + +```jsonc +"tasks": { + // Builds the application. + "build": "deno run -A ./build.ts", + // Builds the application in development mode. + "build-dev": "export APP_ENV=development NODE_ENV=development && deno task build", + // Builds the application in production mode. + "build-prod": "export APP_ENV=production NODE_ENV=production && deno task build", +} +``` + +For more information, view our [configuration guide](configuration.md). + +### dev.ts + +If the default build or dev configuration settings are insufficient for your +application, you can create a custom dev script like shown below: + +```ts +import { startDev } from "@udibo/react-app/dev"; +import "./log.ts"; + +// Import the build options from the build script +import { buildOptions } from "./build.ts"; + +startDev({ + buildOptions, + // Add your own options here +}); +``` + +Then update your deno config file's tasks section to use your dev script: + +```jsonc +"tasks": { + // Builds and runs the application in development mode, with hot reloading. + "dev": "export APP_ENV=development NODE_ENV=development && deno run -A ./dev.ts", +} +``` + +For more information, view our [configuration guide](configuration.md). + +### .gitignore + +Contains a list of files that should not be committed. We don't need to commit +the build artifacts which are stored in the public directory or the router and +route scripts in the routes directory. We also don't need to commit the coverage +files. + +``` +# Build +public/build +public/test-build +routes/_main.tsx +routes/_main.ts + +# Coverage +coverage + +# Node modules +node_modules +``` + +### test-utils.tsx + +Provides additional utilities for testing your application. This script sets up +the global document object, then re-exports all of the tools from react testing +library. It also overrides the current render function with one that is +disposable, making it easier to cleanup the global document object at the end of +each test. + +We recommend writing tests for both your user interface and API. Tests help +ensure your application functions as it should and will alert you if changes +break your application. + +```tsx +import "@udibo/react-app/global-jsdom"; +import { + cleanup, + render as _render, + type RenderOptions, +} from "@testing-library/react"; +export * from "@testing-library/react"; + +export function render( + ui: React.ReactElement, + options?: Omit, +): ReturnType & Disposable { + const result = _render(ui, options); + return { + ...result, + [Symbol.dispose]() { + cleanup(); + }, + }; +} +``` + +Below is an example of how to make use of this module in a test case. In this +test case, it renders the loading component and tests that the text content of +it matches what we expect it to. By `using` the screen returned by the render +function call, the test will call the cleanup function before the test finishes. + +```tsx +import { assertEquals } from "@std/assert"; +import { describe, it } from "@std/testing/bdd"; + +import { render } from "../test-utils.tsx"; + +import { Loading } from "./loading.tsx"; + +const loadingTests = describe("Loading component"); + +it(loadingTests, "renders loading message", () => { + using screen = render(); + assertEquals(screen.getByText("Loading...").textContent, "Loading..."); +}); +``` + +If you'd prefer calling the cleanup function yourself, you would want to add an +afterEach hook to your test suite that calls the cleanup function. + +```tsx +import { assertEquals } from "@std/assert"; +import { describe, it } from "@std/testing/bdd"; + +import { cleanup, render } from "../test-utils.tsx"; + +import { Loading } from "./loading.tsx"; + +const loadingTests = describe({ + name: "Loading component", + afterEach() { + cleanup(); + }, +}); + +it(loadingTests, "renders loading message", () => { + const screen = render(); + assertEquals(screen.getByText("Loading...").textContent, "Loading..."); +}); +``` + +### .github/* + +If you plan on using GitHub actions for CI/CD, you'll want some of the files in +this directory. If the default build configuration works for you, the examples +below. For more information, view our [CI/CD guide](ci-cd.md). + +#### .github/workflows/main.yml + +If you'd like a GitHub action that will test your code, upload coverage reports, +and deploy your code to Deno Deploy, you can use the following workflow for +that. If you are not going to upload your test coverage report to Codecov, just +omit the secret. If you are not using deploy, you can look at the referenced +script to see how to write a workflow for preparing a production build and +uploading it. If your configuration is unique, you can write your own CI and CD +workflows based on ours. Those can be found on GitHub in the `.github/workflows` +folder. + +In this example, the CI step builds the application, checks the formatting, +lints it, and runs the tests. The CD step builds the application for production, +and deploys it to Deno Deploy. + +```yml +name: CI/CD +on: + pull_request: + push: + branches: + - main +jobs: + ci: + name: CI + uses: udibo/react-app/.github/workflows/ci.yml@0.24.0 + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + cd: + name: CD + needs: ci + uses: udibo/react-app/.github/workflows/deploy.yml@0.24.0 + with: + project: udibo-react-app-example +``` + +#### .github/codecov.yml + +If you are using codecov to report your test coverage, this file is a good +starting point for configuring it. It's recommended to ignore coverage for your +build artifacts. + +```yml +comment: false +codecov: + require_ci_to_pass: true +coverage: + status: + project: + default: + informational: true +ignore: + - "public/build/**/*" +``` + +### .vscode/* + +There is just one file required here if you are using VS code. That's the file +with the settings for VS code to use. If you'd like more details about other +configuration options like the ones for using the debugger or running tasks, see +the [development tools guide](development-tools.md#vs-code) for more +information. + +#### .vscode/settings.json + +Your vscode settings must have your deno.jsonc referenced so that the extension +knows how to work with your project. You can use unstable APIs if you want but +by default I left that disabled in this example. Your code will automatically +get formatted by the deno extension when you save. If there are parts of your +code you would like ignored by Deno's linter or formatter, you can configure +that in your [deno.jsonc](#denojsonc) file. + +```json +{ + "deno.enable": true, + "deno.lint": true, + "deno.unstable": false, + "deno.config": "./deno.jsonc", + "editor.formatOnSave": true, + "editor.defaultFormatter": "denoland.vscode-deno", + "editor.quickSuggestions": { + "strings": true + } +} +``` + +## Tasks + +To run the tests, use `deno task test` or `deno task test-watch`. + +To check formatting and run the linter, use `deno task check`. + +The following 2 commands can be used for creating builds. + +- `deno task build-dev`: Builds the application in development mode. +- `deno task build-prod`: Builds the application in production mode. + +A build must be generated before you can run an application. You can use the +following 2 commands to run the application. + +- `deno task run-dev`: Runs the application in development mode. +- `deno task run-prod`: Runs the application in production mode. + +To run the application in development mode with live reloading, use +`deno task dev`. + +When in development, identifiers are not minified and sourcemaps are generated +and linked. + +The commands ending in `-dev` and `-prod` set the `APP_ENV` and `NODE_ENV` +environment variables. The `NODE_ENV` environment variable is needed for react. +If you use the `deno task build` or `deno task run` tasks, you should make sure +that you set both of those environment variables. Those environment variables +are also needed if you deploy to Deno Deploy. + +The `deno task git-rebase` task is useful if you use squash and merge. If you +don't need it feel free to remove it. + +If you'd like to customize your build or the tasks available, see the +[tasks section](configuration.md#tasks) of our +[configuration guide](configuration.md). + +## Routing + +This framework supports both file based routing and nesting routes within a +file. This makes it easy to organize your application and visualize it as a tree +just like how React makes it easy to organize and visualize your UI as a tree. + +To learn more about routing, view our [routing guide](routing.md). + +## Error handling + +Error handling is a crucial aspect of both UI and API routes in this framework. +It allows you to gracefully manage and respond to various types of errors that +may occur during the execution of your application. + +For more detailed information on implementing error handling in both UI and API +routes, including best practices and advanced techniques, please refer to our +comprehensive [error handling guide](./error-handling.md). + +## Metadata + +Metadata is crucial for improving SEO, social media sharing, and overall user +experience in your application. + +For detailed information on implementing and managing metadata, including best +practices and advanced techniques, please refer to our +[Metadata guide](./metadata.md). + +## Logging + +TODO: Briefly cover how to log and how it can be configured. + +For more information, view our [logging guide](logging.md). + +## Testing + +TODO: Cover the basics of testing then link to the testing guide. + +For more information, view our [testing guide](testing.md). + +## Deployment + +TODO: Link to the CI-CD guide section for deploymnet for more information about +deploying to other environments and for automating deployment with github +actions. diff --git a/docs/http-middleware.md b/docs/http-middleware.md new file mode 100644 index 0000000..47ee4b9 --- /dev/null +++ b/docs/http-middleware.md @@ -0,0 +1,5 @@ +# HTTP middleware + +TODO: Make an outline then fill it in with details. + +- [HTTP middleware](#http-middleware) diff --git a/docs/logging.md b/docs/logging.md new file mode 100644 index 0000000..a19d695 --- /dev/null +++ b/docs/logging.md @@ -0,0 +1,17 @@ +# Logging + +TODO: Make an outline then fill it in with details. + +- [Logging](#logging) + - [Console](#console) + - [Browser](#browser) + - [Files](#files) + +## Console + +## Browser + +## Files + +TODO: Link to Deno's logging documentation that explains how logs can be written +to files. diff --git a/docs/metadata.md b/docs/metadata.md new file mode 100644 index 0000000..022dd94 --- /dev/null +++ b/docs/metadata.md @@ -0,0 +1,56 @@ +# Metadata + +- [Metadata](#metadata) + - [Setting metadata](#setting-metadata) + - [SEO](#seo) + +## Setting metadata + +[React Helmet Async](https://www.npmjs.com/package/react-helmet-async) is used +to manage all of your changes to the document head. You can add a Helmet tag to +any page that you would like to update the document head. + +- Supports all valid head tags: title, base, meta, link, script, noscript, and + style tags. +- Supports attributes for body, html and title tags. + +The following example can be found in the [main route](example/routes/main.tsx) +of the example in this repository. The Helmet in the main route of a directory +will apply to all routes within the directory. + +```tsx +import { Suspense } from "react"; +import { DefaultErrorFallback, ErrorBoundary, Helmet } from "@udibo/react-app"; +import { Outlet } from "react-router-dom"; +import "../log.ts"; + +import { Loading } from "../components/loading.tsx"; + +export default function Main() { + return ( + <> + + + + + }> + + + + + + ); +} +``` + +More examples of Helmet tag usage can be found in the +[React Helmet Reference Guide](https://github.com/nfl/react-helmet#reference-guide). + +## SEO + +TODO: Explain the basics of setting up SEO metadata and using lighthouse to test +it. diff --git a/docs/routing.md b/docs/routing.md new file mode 100644 index 0000000..23f2498 --- /dev/null +++ b/docs/routing.md @@ -0,0 +1,816 @@ +# Routing + +This framework supports both file based routing and nesting routes within a +file. This makes it easy to organize your application and visualize it as a tree +just like how React makes it easy to organize and visualize your UI as a tree. + +- [Routing](#routing) + - [Route types](#route-types) + - [Naming convention](#naming-convention) + - [Main routes](#main-routes) + - [UI](#ui) + - [API](#api) + - [Index routes](#index-routes) + - [UI](#ui-1) + - [API](#api-1) + - [Named routes](#named-routes) + - [UI](#ui-2) + - [API](#api-2) + - [Parameterized routes](#parameterized-routes) + - [UI](#ui-3) + - [API](#api-3) + - [Inline routing](#inline-routing) + - [UI](#ui-4) + - [API](#api-4) + - [Query parameters](#query-parameters) + - [UI](#ui-5) + - [API](#api-5) + - [Error handling](#error-handling) + - [Metadata](#metadata) + +## Route types + +There are 2 types of routes, UI routes and API routes. UI routes that do not +have an API route with the same path will default to rendering the application +on the server. The naming convention is the same for both types of routes. + +UI routes are defined in files with `.tsx` or `.jsx` extensions, while API +routes are defined in files with `.ts` or `.js` extensions. The framework +determines the route type based on these file extensions: + +- UI routes (`.tsx` or `.jsx`): These routes define the user interface + components and layouts. They are responsible for rendering the visual elements + of your application and handling client-side interactions. + +- API routes (`.ts` or `.js`): These routes define server-side endpoints that + handle data processing, database interactions, and other backend + functionalities. They typically return data in formats like JSON for + consumption by the UI or external clients. + +For example, a file named `blog.tsx` would be treated as a UI route, rendering a +blog page component, while `blog.ts` would be treated as an API route, perhaps +handling operations like fetching or updating blog posts. + +This distinction allows you to organize your frontend and backend code within +the same directory structure, making it easier to manage related functionality. + +## Naming convention + +Each directory can have a [main route](#main-routes) that wraps all the routes +in that directory. Each directory can have an [index route](#index-routes) that +will be used when accessing the route directly. For example, the path `/blog` in +our example has a main UI route that contains layout and default metadata for +all subroutes, and it has an index route that would be used when accessing +`/blog` without a subroute path. + +Besides the reserved main and index route names, you can also create +[named routes](#named-routes) and [parameterized routes](#parameterized-routes). +The closest main route to it's directory path would be used to wrap the route. + +If you would like to include files or subdirectories in your routes directory +that are not routes you can do so by prefixing the file or directory name with +an underscore. The only names that cannot be used are `_main.ts` and `_main.tsx` +in the root of your routes directory. Those 2 files are generated by the +framework and should not be modified. + +## Main routes + +Main routes are special routes that act as wrappers for all the routes within a +directory. They are useful for creating shared layouts, adding common metadata, +or applying middleware to all routes in a specific directory. + +To create a main route for a directory, you need to create a file named +`main.tsx` (for UI routes) or `main.ts` (for API routes) in that directory. + +By using main routes, you can easily apply common functionality, layouts, or +middleware to groups of related routes, keeping your code DRY and organized. + +### UI + +For UI routes, the `main.tsx` file should export a default React component that +will wrap all the routes in that directory. This component typically includes +shared layout elements and can also set default metadata using Helmet. + +Here's an example of a main route for a blog route: + +```tsx +import { Suspense } from "react"; +import { Outlet } from "react-router-dom"; +import { DefaultErrorFallback, ErrorBoundary, Helmet } from "@udibo/react-app"; + +import { Loading } from "../../components/loading.tsx"; + +export const boundary = "/blog"; + +export default function Blog() { + return ( + <> + + + +

Blog

+ }> + + + + + + ); +} +``` + +In this example, the main route for the blog route: + +- Sets a default title and title template for all blog pages +- Adds a common "Blog" heading +- Wraps all child routes in a Suspense component for loading states +- Provides an ErrorBoundary for handling errors within the blog section + +The `` component is where child routes will be rendered. + +### API + +For API routes, the `main.ts` file should export a default Router that will be +used as middleware for all routes in that directory. This is useful for adding +common middleware or error handling for a group of related API routes. + +Here's an example of a main route for API endpoints: + +```ts +import { Router } from "@udibo/react-app/server"; +import * as log from "@std/log"; + +export default new Router() + .use(async (context, next) => { + const { request, response } = context; + const start = Date.now(); + try { + await next(); + } finally { + const responseTime = Date.now() - start; + response.headers.set("X-Response-Time", `${responseTime}ms`); + log.info( + `${request.method} ${request.url.href}`, + { status: response.status, responseTime }, + ); + } + }); +``` + +In this example, the main route for the API: + +- Adds middleware to log request information and response time for all API + routes in this directory +- Sets a custom header with the response time + +## Index routes + +Index routes are special routes that are rendered when a user navigates to the +root of a directory. They are useful for displaying default content or a list of +items for a particular section of your application. + +To create an index route, you need to create a file named `index.tsx` (for UI +routes) or `index.ts` (for API routes) in the directory you want to define the +index for. + +By using index routes, you can provide meaningful content or functionality for +directory-level URLs, improving the overall structure and user experience of +your application. + +### UI + +For UI routes, the `index.tsx` file should export a default React component that +will be rendered when the user navigates to the directory's path. + +Here's an example of an index route for a blog section: + +```tsx +import { Link } from "react-router-dom"; +import { Helmet } from "@udibo/react-app"; + +import { getPosts } from "../../services/posts.tsx"; + +export default function BlogIndex() { + const posts = getPosts(); + return posts + ? ( + <> + + Blog Posts + + +

Blog Posts

+
    + {Object.entries(posts).map(([id, post]) => ( +
  • + {post.title} +
  • + ))} +
+ + ) + :
Loading posts...
; +} +``` + +In this example, the index route for the blog section: + +- Sets the page title and meta description using Helmet +- Displays a list of blog posts with links to individual post pages +- Shows a loading message while posts are being fetched + +This component will be rendered when a user navigates to the `/blog` path. + +### API + +For API routes, the `index.ts` file should export a default Router that will +handle requests to the directory's path. + +Here's an example of an index route for the blog API: + +```ts +import { Router } from "@udibo/react-app/server"; + +import { getPosts } from "../../services/posts.ts"; +import type { PostsState } from "../../models/posts.ts"; + +export default new Router() + .get("/", async (context) => { + const { state } = context; + + state.app.initialState.posts = getPosts(); + await state.app.render(); + }); +``` + +In this example, the index route for the blog API: + +- Handles GET requests to the `/blog` path +- Fetches all posts and adds them to the initial state +- Renders the application with the fetched data + +## Named routes + +Named routes are similar to index routes, but they are used for subroutes that +don't have additional subroutes of their own. They allow you to create specific +pages or endpoints within a directory structure. + +To create a named route, you simply create a file with the desired name (e.g., +`about.tsx` for a UI route or `about.ts` for an API route) in the appropriate +directory. + +The key difference between named routes and index routes is their purpose and +location in the URL structure: + +- Index routes (`index.tsx` or `index.ts`) handle requests to the root of a + directory (e.g., `/blog`). +- Named routes (e.g., `about.tsx` or `posts.ts`) handle requests to specific + subroutes within a directory (e.g., `/about` or `/api/blog/posts`). + +By using a combination of index routes, named routes, and nested routes, you can +create a well-organized and intuitive routing structure for your application. + +### UI + +For UI routes, the named route file should export a default React component that +will be rendered when the user navigates to that specific path. + +Here's an example of a named route for an "About" page: + +```tsx +import { Helmet } from "@udibo/react-app"; + +export default function About() { + return ( + <> + + About + + +

About

+

Udibo React App

+

A React Framework for Deno.

+ + ); +} +``` + +In this example, the named route for the "About" page: + +- Sets the page title and meta description using Helmet +- Displays content specific to the About page + +This component will be rendered when a user navigates to the `/about` path. + +### API + +For API routes, the named route file should export a default Router that will +handle requests to that specific path. + +Here's an example of a named route for a blog posts API: + +```ts +import { HttpError } from "@udibo/react-app"; +import { Router } from "@udibo/react-app/server"; + +import { getPost, getPosts } from "../../../services/posts.ts"; +import type { PostsState } from "../../../models/posts.ts"; + +export default new Router() + .get("/", (context) => { + const { response } = context; + response.body = getPosts(); + }) + .get("/:id", (context) => { + const { response, params } = context; + const id = parseFloat(params.id); + if (isNaN(id) || Math.floor(id) !== id || id < 0) { + throw new HttpError(400, "Invalid id"); + } + const post = getPost(id); + if (!post) throw new HttpError(404, "Not found"); + response.body = post; + }); +``` + +In this example, the named route for the blog posts API: + +- Handles GET requests to `/api/blog/posts` to fetch all posts +- Handles GET requests to `/api/blog/posts/:id` to fetch a specific post by ID +- Implements error handling for invalid IDs and non-existent posts + +Named routes allow you to create specific functionality for individual pages or +API endpoints within your application's route structure. They are particularly +useful for standalone pages or API endpoints that don't require nested routing. + +## Parameterized routes + +Parameterized routes allow you to create dynamic routes that can handle variable +parts in the URL path. These are useful for creating pages or API endpoints that +deal with specific resources identified by an ID or other variable information. + +To create a parameterized route, you name your file with square brackets around +the parameter name, like `[id].tsx` for UI routes or `[id].ts` for API routes. + +Parameterized routes allow you to create flexible and dynamic routes that can +handle a wide range of URL patterns. They are particularly useful for: + +- Displaying details of specific items (e.g., product pages, user profiles) +- Handling CRUD operations on resources with unique identifiers +- Creating reusable route components that can work with different data based on + the path parameters + +By combining parameterized routes with other routing techniques, you can create +a powerful and flexible routing system for your application that can handle +complex URL structures and data relationships. + +### UI + +For UI routes, the parameterized route file should export a default React +component that can access and use the route parameters. + +Here's an example of a parameterized route for a blog post page: + +```ts +import { useParams } from "react-router-dom"; +import { Helmet, HttpError } from "@udibo/react-app"; + +import { getPost } from "../../services/posts.tsx"; + +export default function BlogPost() { + const params = useParams(); + const id = Number(params.id); + if (isNaN(id) || Math.floor(id) !== id || id < 0) { + throw new HttpError(400, "Invalid id"); + } + const post = getPost(id); + return post + ? ( + <> + + {post.title} + + +

{post.title}

+

{post.content}

+ + ) + : ( + <> + + Loading... + +

Loading...

+ + ); +} +``` + +In this example, the parameterized route for a blog post: + +- Uses the `useParams` hook from React Router to access the `id` parameter +- Validates the `id` parameter and throws an error if it's invalid +- Fetches the post data based on the `id` +- Renders the post title and content, or a loading state if the post is not yet + available + +This component will be rendered when a user navigates to paths like `/blog/1`, +`/blog/2`, etc. + +### API + +For API routes, the parameterized route file should export a default Router that +can handle requests with the specified parameter. + +Here's an example of a parameterized route for a single blog post API: + +```ts +import { HttpError } from "@udibo/react-app"; +import { Router } from "@udibo/react-app/server"; + +import { getPost } from "../../services/posts.ts"; +import type { PostsState } from "../../models/posts.ts"; + +export default new Router() + .get("/", async (context) => { + const { state, params } = context; + const id = Number(params.id); + if (isNaN(id) || Math.floor(id) !== id || id < 0) { + throw new HttpError(400, "Invalid id"); + } + + state.app.initialState.posts = { + [id]: getPost(id), + }; + await state.app.render(); + }); +``` + +In this example, the parameterized route for the blog post API: + +- Handles GET requests to `/blog/:id` +- Extracts and validates the `id` parameter from the request +- Fetches the specific post data and adds it to the initial state +- Renders the application with the fetched data + +## Inline routing + +You can also nest routes within a file. This is useful for several reasons: + +1. Keeping all child routes in a single file for better organization +2. Locating related routes together for improved readability +3. Enabling route reuse across different parts of your application + +Inline routing allows you to define a set of routes once and then reuse them in +multiple contexts, promoting code reusability and maintaining a DRY (Don't +Repeat Yourself) approach in your routing structure. + +### UI + +For UI routes, inline routing allows you to define nested routes within a single +component. This approach is particularly useful for creating complex UI flows or +wizards, where multiple steps or views are closely related. Here's an example of +how you can implement inline routing for a blog post creation process: + +```tsx +import { useState } from "react"; +import { Link, Route, Routes, useNavigate } from "react-router-dom"; + +function BlogPostForm({ title, setTitle, content, setContent, onSubmit }) { + return ( +
+
+ + setTitle(e.target.value)} + /> +
+
+ +