diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..1f5c47b --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,25 @@ +{ + "parser": "@babel/eslint-parser", + "parserOptions": { + "ecmaVersion": 2021, + "sourceType": "module", + "babelOptions": { + "presets": [ + "@babel/preset-env", + "@babel/preset-react" + ] + } + }, + "env": { + "browser": true, + "node": true + }, + "plugins": [ + "react" + ], + "extends": [ + "eslint:recommended", + "plugin:react/recommended" + ], + "rules": {} +} \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..115338d --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,12 @@ +on: push + +name: test +jobs: + install: + name: test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: oven-sh/setup-bun@v1 + + - run: bun install && bun test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5c005f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,46 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local + +# vercel +.vercel + +**/*.trace +**/*.zip +**/*.tar.gz +**/*.tgz +**/*.log +package-lock.json +**/*.bun + +server + +db.sqlite diff --git a/README.md b/README.md new file mode 100644 index 0000000..1f39de5 --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ +# hyperwave 🌊 + +hyperwave is a server-side framework for building web applications. + +- fast: tiny payloads with no bloated client side rendering +- productive: build _really_ fast with htmx, tailwind, tsx, hyperscript +- ergonomic: best tooling of modern tooling combined with rock solid app architecture +- portable: compile a binary to deploy anywhere + +Choosing hyperwave means embracing a smarter way to develop web applications, +where ease of use, performance, and developer experience go hand in hand. + +### Setup + +`bun install && bun run src/db.ts && bun dev` + +Visit port 1234 and edit `server.tsx` + +--- + +### Example + +This is the endpoint serving our initial landing page: + +```typescript +app.get("/", ({ html }) => + html( + +
+
+ +
+
+
, + ), +); +``` + +- The API serves a full HTML document to the client, which includes Tailwind classes and HTMX attributes +- The response is wrapped in a `` tag, a server-rendered functional component, which takes a `title` prop +- The button, when clicked, will issue a `GET` request to `/instructions` and replace the content of its parent div with the response. +- Includes a tiny hyperscript to toggle a class when the button is clicked + +--- + +### Deployment + +Build an executable for your current architecture with `bun run build` + +`PORT` environment variable is available if needed (default is 1234) + +Note: deploy `public/` with the executable, it contains the generated UnoCSS build. + +--- + +### Components + +- [bun](https://bun.sh/) provides the bundler, runtime, test runner, and package manager. +- [SQLite](https://bun.sh/docs/api/sqlite) is production-ready and built into Bun. +- [hono](https://hono.dev) is a robust web framework with great DX and performance +- [unoCSS](https://unocss.dev/integrations/cli) is Tailwind-compatible and generates only the styles used in application code. +- [htmx](https://htmx.org/reference/) gives 99% of the client-side interactivity most apps need. +- [hyperscript](http://hyperscript.org) is a scripting library for rapid application development. +- [zod](https://zod.dev/) is a powerful runtime validation library. + +--- + +### Benefits and takeways + +**Why bother switching to hyperwave?** + +- Drastically reduces time from idea to rendered UI +- Very little cognitive friction to creating something new, after initial learning curve + +**Speed / performance benefit** + +- hyperwave is designed to generate the smallest possible payloads +- Deployment is as simple as compiling and running a binary 😎 + +**Simplicity** + +- Bun saves us a ton of time and effort fighting tooling issues +- SPAs are over-prescribed and inherently introduce serious costs + +**Dev UX benefit** + +- Better primitives for quickly building UX +- Uniform interface simplifies writing and reading code + +**Architectural benefit** + +- Can scale backend and product independently, loosely coupled diff --git a/assets/icons/gear.tsx b/assets/icons/gear.tsx new file mode 100644 index 0000000..c577e21 --- /dev/null +++ b/assets/icons/gear.tsx @@ -0,0 +1,12 @@ +export default function Gear() { + return ( + + + + ); +} diff --git a/assets/icons/house.tsx b/assets/icons/house.tsx new file mode 100644 index 0000000..8b15499 --- /dev/null +++ b/assets/icons/house.tsx @@ -0,0 +1,12 @@ +export default function House() { + return ( + + + + ); +} diff --git a/assets/icons/magnify.tsx b/assets/icons/magnify.tsx new file mode 100644 index 0000000..90a56d9 --- /dev/null +++ b/assets/icons/magnify.tsx @@ -0,0 +1,15 @@ +export default function Magnify() { + return ( + + + + ); +} diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..9fbe423 Binary files /dev/null and b/bun.lockb differ diff --git a/package.json b/package.json new file mode 100644 index 0000000..43ca157 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "hyperwave", + "version": "0.1.0", + "scripts": { + "build": "bun css && bun build --compile ./src/server.tsx", + "css": "unocss \"src/**/*.tsx\" -o public/styles/uno.css", + "css:watch": "unocss \"src/**/*.tsx\" -o public/styles/uno.css --watch", + "db": "bun run src/db.ts", + "dev": "concurrently \"bun css:watch\" \"bun server:watch\"", + "prettier": "bunx prettier --write src/ test/ --plugin prettier-plugin-tailwindcss", + "server:watch": "bun --watch run src/server.tsx", + "test": "bun run test" + }, + "dependencies": { + "@unocss/preset-web-fonts": "^0.58.0", + "hono": "^3.6.3", + "unocss": "^0.58.0", + "zod": "^3.23.5" + }, + "devDependencies": { + "@unocss/cli": "^0.56.5", + "bun-types": "latest", + "concurrently": "^8.2.1", + "prettier": "^3.1.0", + "prettier-plugin-tailwindcss": "^0.5.9" + }, + "module": "src/server.tsx" +} diff --git a/public/styles/uno.css b/public/styles/uno.css new file mode 100644 index 0000000..65b2357 --- /dev/null +++ b/public/styles/uno.css @@ -0,0 +1,122 @@ +/* layer: preflights */ +*,::before,::after{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgb(0 0 0 / 0);--un-ring-shadow:0 0 rgb(0 0 0 / 0);--un-shadow-inset: ;--un-shadow:0 0 rgb(0 0 0 / 0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgb(147 197 253 / 0.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: ;}::backdrop{--un-rotate:0;--un-rotate-x:0;--un-rotate-y:0;--un-rotate-z:0;--un-scale-x:1;--un-scale-y:1;--un-scale-z:1;--un-skew-x:0;--un-skew-y:0;--un-translate-x:0;--un-translate-y:0;--un-translate-z:0;--un-pan-x: ;--un-pan-y: ;--un-pinch-zoom: ;--un-scroll-snap-strictness:proximity;--un-ordinal: ;--un-slashed-zero: ;--un-numeric-figure: ;--un-numeric-spacing: ;--un-numeric-fraction: ;--un-border-spacing-x:0;--un-border-spacing-y:0;--un-ring-offset-shadow:0 0 rgb(0 0 0 / 0);--un-ring-shadow:0 0 rgb(0 0 0 / 0);--un-shadow-inset: ;--un-shadow:0 0 rgb(0 0 0 / 0);--un-ring-inset: ;--un-ring-offset-width:0px;--un-ring-offset-color:#fff;--un-ring-width:0px;--un-ring-color:rgb(147 197 253 / 0.5);--un-blur: ;--un-brightness: ;--un-contrast: ;--un-drop-shadow: ;--un-grayscale: ;--un-hue-rotate: ;--un-invert: ;--un-saturate: ;--un-sepia: ;--un-backdrop-blur: ;--un-backdrop-brightness: ;--un-backdrop-contrast: ;--un-backdrop-grayscale: ;--un-backdrop-hue-rotate: ;--un-backdrop-invert: ;--un-backdrop-opacity: ;--un-backdrop-saturate: ;--un-backdrop-sepia: ;} +/* latin-ext */ +@font-face { + font-family: 'Lato'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(https://fonts.gstatic.com/s/lato/v24/S6uyw4BMUTPHjxAwXjeu.woff2) format('woff2'); + unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Lato'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(https://fonts.gstatic.com/s/lato/v24/S6uyw4BMUTPHjx4wXg.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +/* layer: default */ +.fixed{position:fixed;} +.relative{position:relative;} +.sticky{position:sticky;} +.left-10{left:2.5rem;} +.right-8{right:2rem;} +.top-0\.5{top:0.125rem;} +.top-11{top:2.75rem;} +.m-0{margin:0;} +.m-auto{margin:auto;} +.me{margin-inline-end:1rem;} +.block{display:block;} +.hidden{display:none;} +.h-8{height:2rem;} +.h-full{height:100%;} +.max-h-14{max-height:3.5rem;} +.min-h-14{min-height:3.5rem;} +.w-16{width:4rem;} +.w-40{width:10rem;} +.w-80{width:20rem;} +.w-full{width:100%;} +.hover\:w-56:hover{width:14rem;} +.flex{display:flex;} +.flex-col{flex-direction:column;} +.cursor-pointer{cursor:pointer;} +.list-none{list-style-type:none;} +.items-center{align-items:center;} +.self-start{align-self:flex-start;} +.justify-center{justify-content:center;} +.justify-between{justify-content:space-between;} +.gap-3{gap:0.75rem;} +.gap-4{gap:1rem;} +.gap-8{gap:2rem;} +.border{border-width:1px;} +.border-b-1{border-bottom-width:1px;} +.border-blue-300{--un-border-opacity:1;border-color:rgb(147 197 253 / var(--un-border-opacity));} +.border-gray-2{--un-border-opacity:1;border-color:rgb(229 231 235 / var(--un-border-opacity));} +.focus\:border-blue-200:focus{--un-border-opacity:1;border-color:rgb(191 219 254 / var(--un-border-opacity));} +.rounded-md{border-radius:0.375rem;} +.border-none{border-style:none;} +.border-solid{border-style:solid;} +.border-b-solid{border-bottom-style:solid;} +.bg-blue-100{--un-bg-opacity:1;background-color:rgb(219 234 254 / var(--un-bg-opacity));} +.bg-blue-200{--un-bg-opacity:1;background-color:rgb(191 219 254 / var(--un-bg-opacity));} +.bg-blue-300{--un-bg-opacity:1;background-color:rgb(147 197 253 / var(--un-bg-opacity));} +.bg-blue-50{--un-bg-opacity:1;background-color:rgb(239 246 255 / var(--un-bg-opacity));} +.bg-blue-700{--un-bg-opacity:1;background-color:rgb(29 78 216 / var(--un-bg-opacity));} +.bg-blue-900{--un-bg-opacity:1;background-color:rgb(30 58 138 / var(--un-bg-opacity));} +.bg-transparent{background-color:transparent;} +.bg-white{--un-bg-opacity:1;background-color:rgb(255 255 255 / var(--un-bg-opacity));} +.hover\:bg-blue-400:hover{--un-bg-opacity:1;background-color:rgb(96 165 250 / var(--un-bg-opacity));} +.hover\:bg-blue-700:hover{--un-bg-opacity:1;background-color:rgb(29 78 216 / var(--un-bg-opacity));} +.fill-neutral-500{--un-fill-opacity:1;fill:rgb(115 115 115 / var(--un-fill-opacity));} +.fill-white{--un-fill-opacity:1;fill:rgb(255 255 255 / var(--un-fill-opacity));} +.p-0{padding:0;} +.p-4{padding:1rem;} +.px-10{padding-left:2.5rem;padding-right:2.5rem;} +.px-4{padding-left:1rem;padding-right:1rem;} +.py-1{padding-top:0.25rem;padding-bottom:0.25rem;} +.py-2{padding-top:0.5rem;padding-bottom:0.5rem;} +.py-3{padding-top:0.75rem;padding-bottom:0.75rem;} +.py-4{padding-top:1rem;padding-bottom:1rem;} +.pl-20{padding-left:5rem;} +.pl-3{padding-left:0.75rem;} +.pl-6{padding-left:1.5rem;} +.pr-10{padding-right:2.5rem;} +.text-left{text-align:left;} +.text-base{font-size:1rem;line-height:1.5rem;} +.text-sm{font-size:0.875rem;line-height:1.25rem;} +.text-neutral-500{--un-text-opacity:1;color:rgb(115 115 115 / var(--un-text-opacity));} +.text-slate-400{--un-text-opacity:1;color:rgb(148 163 184 / var(--un-text-opacity));} +.text-white{--un-text-opacity:1;color:rgb(255 255 255 / var(--un-text-opacity));} +.hover\:text-white:hover{--un-text-opacity:1;color:rgb(255 255 255 / var(--un-text-opacity));} +.font-bold{font-weight:700;} +.leading-5{line-height:1.25rem;} +.font-lato{font-family:"Lato";} +.uppercase{text-transform:uppercase;} +.no-underline{text-decoration:none;} +.opacity-0{opacity:0;} +.group:hover .group-hover\:opacity-100{opacity:1;} +.shadow-md{--un-shadow:var(--un-shadow-inset) 0 4px 6px -1px var(--un-shadow-color, rgb(0 0 0 / 0.1)),var(--un-shadow-inset) 0 2px 4px -2px var(--un-shadow-color, rgb(0 0 0 / 0.1));box-shadow:var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);} +.shadow-sm{--un-shadow:var(--un-shadow-inset) 0 1px 2px 0 var(--un-shadow-color, rgb(0 0 0 / 0.05));box-shadow:var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow);} +.outline{outline-style:solid;} +.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms;} +.transition-width{transition-property:width;transition-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transition-duration:150ms;} +.duration-200{transition-duration:200ms;} +.duration-300{transition-duration:300ms;} +.duration-50{transition-duration:50ms;} +@media (min-width: 640px){ +.sm\:ml-4{margin-left:1rem;} +} +@media (min-width: 768px){ +.md\:top-14{top:3.5rem;} +.md\:block{display:block;} +.md\:w-56{width:14rem;} +.md\:w-96{width:24rem;} +.md\:flex-row{flex-direction:row;} +.md\:pl-60{padding-left:15rem;} +.md\:pr-10{padding-right:2.5rem;} +.md\:opacity-100{opacity:1;} +} \ No newline at end of file diff --git a/src/components/Button.tsx b/src/components/Button.tsx new file mode 100644 index 0000000..b278911 --- /dev/null +++ b/src/components/Button.tsx @@ -0,0 +1,20 @@ +type Props = { + class?: string; + children: any; + [string: string]: any; +}; + +export default function PinkButton({ + class: className, + children, + ...rest +}: Props) { + return ( + + ); +} diff --git a/src/components/Input.tsx b/src/components/Input.tsx new file mode 100644 index 0000000..8a161cb --- /dev/null +++ b/src/components/Input.tsx @@ -0,0 +1,15 @@ +export default function Input({ class: className, placeholder, ...rest }) { + return ( + + ); +} diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx new file mode 100644 index 0000000..c35d98a --- /dev/null +++ b/src/components/Layout.tsx @@ -0,0 +1,52 @@ +import Magnify from "../../assets/icons/magnify"; +import Input from "./Input"; +import Nav from "./Nav"; + +type Props = { + title: string; + currentPath?: string; + children: any; +}; + +export default function Layout({ title, children, currentPath }: Props) { + return ( + + + + + + {title} + + + + + + + + +
+ +

🌊 hyperwave

+
+ +
+ +