diff --git a/.changeset/light-carpets-cough.md b/.changeset/light-carpets-cough.md new file mode 100644 index 00000000..e0abcd53 --- /dev/null +++ b/.changeset/light-carpets-cough.md @@ -0,0 +1,8 @@ +--- +"create-hwy": patch +"@hwy-js/build": patch +"hwy": patch +"@hwy-js/dev": patch +--- + +update types diff --git a/.changeset/pre.json b/.changeset/pre.json index 1282c3e6..66d3690e 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -12,6 +12,7 @@ "changesets": [ "early-readers-rule", "famous-adults-push", + "light-carpets-cough", "neat-moles-brush", "poor-phones-lie", "rude-kiwis-approve" diff --git a/docs/hwy.config.js b/docs/hwy.config.js new file mode 100644 index 00000000..25697146 --- /dev/null +++ b/docs/hwy.config.js @@ -0,0 +1,7 @@ +export default { + dev: { + port: 1270, + watchExclusions: ["src/styles/tw-output.bundle.css"], + }, + deploymentTarget: "vercel-lambda", +}; diff --git a/docs/package-lock.json b/docs/package-lock.json index 89f85740..d13c19f9 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -11,11 +11,11 @@ "@hono/node-server": "^1.2.0", "highlight.js": "^11.9.0", "hono": "^3.8.3", - "hwy": "0.4.2-beta.4" + "hwy": "0.4.2-beta.5" }, "devDependencies": { - "@hwy-js/build": "0.4.2-beta.4", - "@hwy-js/dev": "0.4.2-beta.4", + "@hwy-js/build": "0.4.2-beta.5", + "@hwy-js/dev": "0.4.2-beta.5", "@types/node": "^20.8.7", "@types/nprogress": "^0.2.2", "htmx.org": "^1.9.6", @@ -400,9 +400,9 @@ } }, "node_modules/@hwy-js/build": { - "version": "0.4.2-beta.4", - "resolved": "https://registry.npmjs.org/@hwy-js/build/-/build-0.4.2-beta.4.tgz", - "integrity": "sha512-gq7fG7PyZMNrsxIJ0BpUCrFcw7KsRoidJNnSTepm1K3c6cJV3tvomuRkw1A5HRUEVR3Fpv3rjObGv02Zv6hLSQ==", + "version": "0.4.2-beta.5", + "resolved": "https://registry.npmjs.org/@hwy-js/build/-/build-0.4.2-beta.5.tgz", + "integrity": "sha512-siIT9N5oMkapOe6P0UAeGTea44prfAwmJsyFE8mkw44EXqTDdMNUDk9jF4Tx+jx25ozObkwEcmHO2xfmNaWJ6A==", "dev": true, "dependencies": { "chokidar": "^3.5.3", @@ -417,9 +417,9 @@ } }, "node_modules/@hwy-js/dev": { - "version": "0.4.2-beta.4", - "resolved": "https://registry.npmjs.org/@hwy-js/dev/-/dev-0.4.2-beta.4.tgz", - "integrity": "sha512-fq2X3ZhW7mjjqGHIokmeDiKhv+YENYB9XMs8EBnaijfFupkXJ/DlDjBzSHkCv/rlDulOHLxrapwSeJfHvEBqrg==", + "version": "0.4.2-beta.5", + "resolved": "https://registry.npmjs.org/@hwy-js/dev/-/dev-0.4.2-beta.5.tgz", + "integrity": "sha512-o1/Bq3RYWW2IDH24exqIwteWcRs3jLxHLu7vCF7s2YQyYXFJZHfVvzXiKscepXlrCnSGvEmCzBRc4nan3GBvwA==", "dev": true, "dependencies": { "picocolors": "^1.0.0" @@ -830,9 +830,9 @@ "dev": true }, "node_modules/hwy": { - "version": "0.4.2-beta.4", - "resolved": "https://registry.npmjs.org/hwy/-/hwy-0.4.2-beta.4.tgz", - "integrity": "sha512-+rLOEwbc6ruAXoRf+xUj8jxlFinFMGdzFM3tyQ3w8zthbXRRm0tXbxjoZ20XOvZW924Qkw6J54PfC2vxHwvfVA==" + "version": "0.4.2-beta.5", + "resolved": "https://registry.npmjs.org/hwy/-/hwy-0.4.2-beta.5.tgz", + "integrity": "sha512-gc4mt5PuWJ/qYunDwTYNLbuchi59hfBYQfIEicTi+fE7mqsOEu+zOuNm4xQD3KiRypjxTCO58kENhpsOiQLYtg==" }, "node_modules/inflight": { "version": "1.0.6", diff --git a/docs/package.json b/docs/package.json index 939d9007..26c55e4f 100644 --- a/docs/package.json +++ b/docs/package.json @@ -13,11 +13,11 @@ "@hono/node-server": "^1.2.0", "highlight.js": "^11.9.0", "hono": "^3.8.3", - "hwy": "0.4.2-beta.4" + "hwy": "0.4.2-beta.5" }, "devDependencies": { - "@hwy-js/build": "0.4.2-beta.4", - "@hwy-js/dev": "0.4.2-beta.4", + "@hwy-js/build": "0.4.2-beta.5", + "@hwy-js/dev": "0.4.2-beta.5", "@types/node": "^20.8.7", "@types/nprogress": "^0.2.2", "htmx.org": "^1.9.6", diff --git a/docs/src/client.entry.js b/docs/src/client.entry.js new file mode 100644 index 00000000..227ba846 --- /dev/null +++ b/docs/src/client.entry.js @@ -0,0 +1,7 @@ +const __window = window; +import htmx from "htmx.org"; +__window.htmx = htmx; +import NProgress from "nprogress"; +__window.NProgress = NProgress; +// @ts-ignore +import("htmx.org/dist/ext/head-support.js"); diff --git a/docs/src/components/anchor-heading.js b/docs/src/components/anchor-heading.js new file mode 100644 index 00000000..af91a5d7 --- /dev/null +++ b/docs/src/components/anchor-heading.js @@ -0,0 +1,6 @@ +import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime"; +function AnchorHeading({ content }) { + const slugified = encodeURIComponent(content.toLowerCase().replace(/ /g, "-")); + return (_jsxs("div", { class: "flex gap-3 text-xl font-bold pt-4", id: slugified, children: [_jsx("a", { class: "hover:underline text-[#777] hover:text-[unset]", href: `#${slugified}`, children: "#" }), content] })); +} +export { AnchorHeading }; diff --git a/docs/src/components/bold-italic.js b/docs/src/components/bold-italic.js new file mode 100644 index 00000000..b589c392 --- /dev/null +++ b/docs/src/components/bold-italic.js @@ -0,0 +1,5 @@ +import { jsx as _jsx } from "hono/jsx/jsx-runtime"; +function Boldtalic({ children }) { + return (_jsx("b", { children: _jsx("i", { children: children }) })); +} +export { Boldtalic }; diff --git a/docs/src/components/code-block.js b/docs/src/components/code-block.js new file mode 100644 index 00000000..86b10c51 --- /dev/null +++ b/docs/src/components/code-block.js @@ -0,0 +1,13 @@ +import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime"; +import hljs from "highlight.js/lib/core"; +import typescript from "highlight.js/lib/languages/typescript"; +import bash from "highlight.js/lib/languages/bash"; +import json from "highlight.js/lib/languages/json"; +const lang_map = { typescript, bash, json }; +function CodeBlock({ language, code }) { + hljs.registerLanguage(language, lang_map[language]); + return (_jsxs("pre", { class: "overflow-x-auto rounded-2xl bg-slate-800 border-4 border-solid border-[#7777] py-4 px-5 max-w-full flex gap-5 text-white", children: [_jsx("code", { class: `language-${language}`, dangerouslySetInnerHTML: { + __html: hljs.highlight(code.trim(), { language }).value, + } }), _jsx("div", {})] })); +} +export { CodeBlock }; diff --git a/docs/src/components/dialog.js b/docs/src/components/dialog.js new file mode 100644 index 00000000..81cf0813 --- /dev/null +++ b/docs/src/components/dialog.js @@ -0,0 +1,9 @@ +import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime"; +function DialogModal({ open_button_inner, dialog_inner, wrapper_class, open_button_class, }) { + return (_jsxs("div", { "hx-boost": "false", class: wrapper_class, children: [_jsx("button", { onclick: `this.nextElementSibling.showModal()`, class: open_button_class, children: open_button_inner }), _jsx("dialog", { onclick: "event.target==this && this.close()", style: { + padding: String(0), + border: "none", + background: "transparent", + }, children: _jsx("form", { method: "dialog", children: dialog_inner }) })] })); +} +export { DialogModal }; diff --git a/docs/src/components/dialog.tsx b/docs/src/components/dialog.tsx index 7a60c6bb..de6cd773 100644 --- a/docs/src/components/dialog.tsx +++ b/docs/src/components/dialog.tsx @@ -23,7 +23,7 @@ function DialogModal({ {children} diff --git a/docs/src/components/nav.js b/docs/src/components/nav.js new file mode 100644 index 00000000..a59d26bc --- /dev/null +++ b/docs/src/components/nav.js @@ -0,0 +1,5 @@ +import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime"; +function Nav() { + return (_jsxs("nav", { class: "flex-wrap mt-6 mb-12 items-center flex justify-between", children: [_jsx("a", { href: "/", class: "hover:text-orange-700 hover:dark:text-orange-300 font-bold text-2xl mr-2 h-[33px] flex items-center leading-none", children: _jsx("h1", { children: "Hwy" }) }), _jsxs("div", { class: "flex", children: [_jsx("a", { href: "/docs", class: "px-2 rounded hover:bg-blue-500 hover:text-white uppercase h-[33px] flex items-center leading-none font-bold", title: "Hwy Documentation", children: "Docs" }), _jsx("a", { href: "https://github.com/hwy-js/hwy", target: "_blank", class: "px-2 rounded hover:bg-blue-500 hover:text-white uppercase h-[33px] flex items-center leading-none font-bold", title: "Star on GitHub", children: "\u2B50 GitHub" })] })] })); +} +export { Nav }; diff --git a/docs/src/components/paragraph.js b/docs/src/components/paragraph.js new file mode 100644 index 00000000..94265cf3 --- /dev/null +++ b/docs/src/components/paragraph.js @@ -0,0 +1,6 @@ +import { jsx as _jsx } from "hono/jsx/jsx-runtime"; +import { cx } from "../utils/utils.js"; +function Paragraph({ children, ...rest }) { + return (_jsx("p", { ...rest, class: cx("leading-7", rest.class), children: children })); +} +export { Paragraph }; diff --git a/docs/src/components/unordered-list.js b/docs/src/components/unordered-list.js new file mode 100644 index 00000000..326debbb --- /dev/null +++ b/docs/src/components/unordered-list.js @@ -0,0 +1,9 @@ +import { jsx as _jsx } from "hono/jsx/jsx-runtime"; +import { cx } from "../utils/utils.js"; +function UnorderedList({ children, ...rest }) { + return (_jsx("ul", { ...rest, class: cx("space-y-6", rest.class), children: children })); +} +function ListItem({ children, ...rest }) { + return (_jsx("li", { ...rest, class: cx("list-disc ml-6 pl-1 leading-7", rest.class), children: children })); +} +export { UnorderedList, ListItem }; diff --git a/docs/src/main.js b/docs/src/main.js new file mode 100644 index 00000000..f6bb17af --- /dev/null +++ b/docs/src/main.js @@ -0,0 +1,89 @@ +import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime"; +import { IS_DEV } from "./utils/constants.js"; +import { hwyInit, CssImports, rootOutlet, DevLiveRefreshScript, ClientScripts, HeadElements, getDefaultBodyProps, renderRoot, } from "hwy"; +import { Hono } from "hono"; +import { serve } from "@hono/node-server"; +import { handle } from "@hono/node-server/vercel"; +import { serveStatic } from "@hono/node-server/serve-static"; +import { Nav } from "./components/nav.js"; +import { logger } from "hono/logger"; +import { secureHeaders } from "hono/secure-headers"; +import { FallbackErrorBoundary } from "./components/fallback-error-boundary.js"; +const app = new Hono(); +app.use("*", logger()); +app.get("*", secureHeaders()); +await hwyInit({ + app, + importMetaUrl: import.meta.url, + serveStatic, + /* + * The publicUrlPrefix makes the monorepo work with the public + * folder when deployed with Vercel. If you aren't using a + * monorepo (or aren't deploying to Vercel), you won't need + * to add a publicUrlPrefix. + */ + publicUrlPrefix: process.env.NODE_ENV === "production" ? "docs/" : undefined, +}); +const default_head_blocks = [ + { title: "Hwy Framework" }, + { + tag: "meta", + props: { + name: "description", + content: "Hwy is a simple, lightweight, and flexible web framework, built on Hono and HTMX.", + }, + }, + { + tag: "link", + props: { + rel: "icon", + href: `data:image/svg+xml,🔥`, + }, + }, + { + tag: "meta", + props: { + name: "og:image", + content: "/create-hwy-snippet.webp", + }, + }, + { + tag: "meta", + props: { + name: "htmx-config", + content: JSON.stringify({ + selfRequestsOnly: true, + refreshOnHistoryMiss: true, + scrollBehavior: "auto", + }), + }, + }, +]; +app.all("*", async (c, next) => { + if (IS_DEV) + await new Promise((r) => setTimeout(r, 150)); + // 31 days vercel edge cache (invalidated each deploy) + c.header("CDN-Cache-Control", "public, max-age=2678400"); + // 10 seconds client cache + c.header("Cache-Control", "public, max-age=10"); + return await renderRoot(c, next, async ({ activePathData }) => { + return (_jsxs("html", { lang: "en", children: [_jsxs("head", { children: [_jsx("meta", { charset: "UTF-8" }), _jsx("meta", { name: "viewport", content: "width=device-width,initial-scale=1" }), _jsx(HeadElements, { c: c, activePathData: activePathData, defaults: default_head_blocks }), _jsx(CssImports, {}), _jsx(ClientScripts, { activePathData: activePathData }), _jsx(DevLiveRefreshScript, {})] }), _jsx("body", { ...getDefaultBodyProps({ nProgress: true }), class: "p-2 sm:p-4 flex", children: _jsxs("div", { class: "px-5 lg:px-8 w-full flex flex-col", children: [_jsxs("div", { class: "grow", children: [_jsx(Nav, {}), _jsx("div", { class: "flex flex-col gap-8 lg:gap-12 max-w-[640px] mb-8 mt-12 mx-auto", children: await rootOutlet({ + c, + activePathData, + fallbackErrorBoundary: FallbackErrorBoundary, + }) })] }), _jsx("footer", { class: "text-xs border-t border-t-solid border-1 border-[#7773] pt-3 pb-4 shrink mt-6", children: _jsx("span", { class: "opacity-60", children: "MIT License. Copyright (c) 2023 Samuel J. Cook." }) })] }) })] })); + }); +}); +app.notFound((c) => { + return c.text("404 Not Found", 404); +}); +app.onError((error, c) => { + console.error(error); + return c.text("500 Internal Server Error", 500); +}); +export default handle(app); +if (IS_DEV) { + serve({ fetch: app.fetch, port: Number(process.env.PORT || 3000) }, (info) => { + console.log(`\nListening on http://${IS_DEV ? "localhost" : info.address}:${info.port}\n`); + }); +} diff --git a/docs/src/pages/$.page.js b/docs/src/pages/$.page.js new file mode 100644 index 00000000..7e218ffc --- /dev/null +++ b/docs/src/pages/$.page.js @@ -0,0 +1,4 @@ +import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime"; +export default function () { + return (_jsxs("div", { children: [_jsx("h2", { class: "text-6xl", children: "404" }), _jsx("p", { class: "mt-4 mb-8", children: "Nothing found!" })] })); +} diff --git a/docs/src/pages/_index.page.js b/docs/src/pages/_index.page.js new file mode 100644 index 00000000..19a4168e --- /dev/null +++ b/docs/src/pages/_index.page.js @@ -0,0 +1,28 @@ +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "hono/jsx/jsx-runtime"; +import { Boldtalic } from "../components/bold-italic.js"; +import { CodeBlock } from "../components/code-block.js"; +import { InlineCode } from "../components/inline-code.js"; +import { Paragraph } from "../components/paragraph.js"; +import { ListItem, UnorderedList } from "../components/unordered-list.js"; +export default function () { + return (_jsxs(_Fragment, { children: [_jsxs("h1", { class: "text-3xl lg:text-4xl leading-snug lg:leading-normal opacity-[0.95] py-6", children: ["Hwy is a ", _jsx(Boldtalic, { children: "simple" }), ",", " ", _jsx(Boldtalic, { children: "lightweight" }), ", and ", _jsx(Boldtalic, { children: "flexible" }), " ", "web framework, built on ", _jsx(Boldtalic, { children: "Hono" }), " and", " ", _jsx(Boldtalic, { children: "HTMX" }), "."] }), _jsxs("div", { class: "flex flex-col gap-4", children: [_jsx("h3", { class: "text-2xl uppercase font-bold underline-offset-4 underline italic mb-3", children: "Quickstart" }), _jsx(InlineCode, { class: "self-start text-xl italic font-bold", high_contrast: true, children: "npx create-hwy@latest" })] }), _jsxs("div", { class: "flex flex-col gap-4", children: [_jsx("h3", { class: "text-2xl uppercase font-bold underline-offset-4 underline italic", children: "What is Hwy?" }), _jsxs(Paragraph, { children: ["Hwy is a lot like NextJS or Remix, but it uses", " ", _jsx(Boldtalic, { children: "HTMX" }), " instead of React on the frontend."] }), _jsxs(Paragraph, { children: ["Hwy lets you write ", _jsx(Boldtalic, { children: "React-style JSX" }), " in", " ", _jsx(Boldtalic, { children: "nested, file-based routes" }), ", with", " ", _jsx(Boldtalic, { children: "Remix-style actions and parallel loaders" }), "."] }), _jsxs(Paragraph, { children: ["Page components are async, so you can even", " ", _jsx(Boldtalic, { children: "fetch data in JSX" }), " if you want to (just be careful with waterfalls)."] }), _jsxs(Paragraph, { children: ["The backend server is built on ", _jsx(Boldtalic, { children: "Hono" }), ", so you have access to a rich, growing ecosystem with lots of middleware and wonderful docs."] }), _jsxs(Paragraph, { children: ["Hwy is ", _jsx(Boldtalic, { children: "100% server-rendered" }), ", but with the HTMX defaults Hwy sets up for you out of the box, your app still", " ", _jsx(Boldtalic, { children: "feels like an SPA" }), "."] }), _jsxs(Paragraph, { children: ["Links and forms are automatically", " ", _jsx(Boldtalic, { children: "progressively enhanced" }), " thanks to HTMX's", " ", _jsx(InlineCode, { children: "hx-boost" }), " feature. Just use normal anchor tags and traditional form attributes."] }), _jsxs(Paragraph, { children: ["Because Hwy replaces the ", _jsx(Boldtalic, { children: "full page" }), " on transitions by default, everything stays ", _jsx(Boldtalic, { children: "simple" }), ". You don't have to return different components from different endpoints (unless you want to)."] }), _jsxs(Paragraph, { children: ["And best of all,", " ", _jsx(Boldtalic, { children: "anything you can do with Hono or HTMX, you can do with Hwy" }), "."] })] }), _jsxs("div", { children: [_jsx("h3", { class: "text-2xl mb-4 uppercase font-bold underline-offset-4 underline italic", children: "Features" }), _jsxs(UnorderedList, { class: "!space-y-0", children: [_jsx(ListItem, { children: "Server-rendered JSX / TSX" }), _jsx(ListItem, { children: "Nested, file-based routing" }), _jsx(ListItem, { children: "Remix-style actions and parallel loaders" }), _jsx(ListItem, { children: "Async page components" }), _jsx(ListItem, { children: "Rich Hono middleware ecosystem" }), _jsx(ListItem, { children: "100% type-safe" }), _jsx(ListItem, { children: "Server built on Hono" }), _jsx(ListItem, { children: "Client built on HTMX" }), _jsx(ListItem, { children: "Built-in critical CSS inlining" }), _jsx(ListItem, { children: "Live browser refresh during development" }), _jsx(ListItem, { children: "And more..." })] })] }), _jsxs("div", { children: [_jsx("h3", { class: "text-2xl mb-4 uppercase font-bold underline-offset-4 underline italic", children: "Guiding principles" }), _jsxs(UnorderedList, { class: "!space-y-0", children: [_jsx(ListItem, { children: "No speed limits" }), _jsx(ListItem, { children: "Numerous off-ramps" }), _jsx(ListItem, { children: "Smooth, safe roads" }), _jsx(ListItem, { children: "Clear traffic signs" })] })] }), _jsxs("div", { children: [_jsx("h3", { class: "text-2xl mb-4 uppercase font-bold underline-offset-4 underline italic", children: "Simple usage" }), _jsx(Paragraph, { class: "mb-6", children: "Below is an example of a simple Hwy page. You'll notice it looks a lot like Remix, and you're right! Hwy is heavily inspired by Remix, but it uses HTMX instead of React." }), _jsx(CodeBlock, { language: "typescript", code: ` +// src/pages/user/$user_id.page.tsx + +import type { DataFunctionArgs, PageProps } from 'hwy' +import { UserProfile, getUser } from './somewhere.js' + +export async function loader({ params }: DataFunctionArgs) { + return await getUser(params.user_id) +} + +export default function ({ loaderData }: PageProps) { + return +} +` }), _jsx(Paragraph, { class: "my-6", children: "Or, if you prefer to fetch inside your components:" }), _jsx(CodeBlock, { language: "typescript", code: ` +export default async function ({ params }: PageProps) { + const user = await getUser(params.user_id) + + return +} +` })] }), _jsxs("div", { class: "flex flex-col gap-4", children: [_jsx("h3", { class: "text-2xl uppercase font-bold underline-offset-4 underline italic", children: "Get Started" }), _jsxs(Paragraph, { children: ["If you want to dive right in, just open a terminal and run", " ", _jsx(InlineCode, { children: "npx create-hwy@latest" }), " and follow the prompts."] }), _jsxs(Paragraph, { children: ["If you'd prefer to read more first, take a peek at", " ", _jsx("a", { href: "/docs", class: "underline", children: "our docs" }), "."] })] }), _jsxs("div", { children: [_jsx("h3", { class: "text-2xl mb-4 uppercase font-bold underline-offset-4 underline italic", children: "Acknowledgements" }), _jsx(Paragraph, { children: "Hwy's APIs are obviously inspired by Remix. If Remix didn't exist, Hwy likely wouldn't exist either. Hwy doesn't use any Remix code, but it still owes a big thanks to the Remix team (past and present) for their top-tier patterns design. If you're building something huge and important today, use Remix." })] }), _jsxs("div", { children: [_jsx("h3", { class: "text-2xl mb-4 uppercase font-bold underline-offset-4 underline italic", children: "Disclaimer" }), _jsx(Paragraph, { children: "Hwy is in beta! Act accordingly." })] })] })); +} diff --git a/docs/src/pages/docs.page.js b/docs/src/pages/docs.page.js new file mode 100644 index 00000000..6966bc6f --- /dev/null +++ b/docs/src/pages/docs.page.js @@ -0,0 +1,253 @@ +import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime"; +import { CodeBlock } from "../components/code-block.js"; +import { AnchorHeading } from "../components/anchor-heading.js"; +import { Paragraph } from "../components/paragraph.js"; +import { InlineCode } from "../components/inline-code.js"; +import { ListItem, UnorderedList } from "../components/unordered-list.js"; +import { Boldtalic } from "../components/bold-italic.js"; +export const head = () => { + return [ + { title: "Hwy Framework Docs" }, + { + tag: "meta", + props: { + name: "description", + content: "Documentation for the Hwy framework, a simple, lightweight, and flexible web framework, built on Hono and HTMX.", + }, + }, + ]; +}; +export default function () { + return (_jsxs("div", { class: "space-y-6", children: [_jsx("h2", { class: "text-3xl font-bold mb-4", children: "Docs" }), _jsx(AnchorHeading, { content: "Creating a new project" }), _jsx(Paragraph, { children: "To create a new project, open a terminal and run the following commands (adjust as appropriate for your preferred package manager):" }), _jsx(CodeBlock, { language: "bash", code: `npx create-hwy@latest\nnpm i\nnpm run dev` }), _jsx(AnchorHeading, { content: "Project structure" }), _jsx(Paragraph, { children: "A simple Hwy project is structured like this:" }), _jsx(CodeBlock, { language: "bash", code: ` +root +├── public/ +│ ├── favicon.ico +├── src/ +│ ├── pages/ +│ │ ├── _index.page.tsx +│ │ ├── $.page.tsx +│ ├── styles/ +│ │ ├── global.bundle.css +│ │ ├── global.critical.css +│ ├── client.entry.ts +│ ├── main.tsx +│ .gitignore +│ ... + ` }), _jsxs(Paragraph, { children: ["The ", _jsx(InlineCode, { children: "public" }), " directory is where you'll put static, public assets, such as favicons, opengraph images, font files, etc."] }), _jsxs(Paragraph, { children: ["The ", _jsx(InlineCode, { children: "src" }), " directory is where you'll put your source files. Most of the structure is completely up to you, but there are a few conventions that Hwy expects."] }), _jsx(AnchorHeading, { content: "Pages directory" }), _jsxs(Paragraph, { children: ["First, you must have a ", _jsx(InlineCode, { children: "src/pages" }), " directory. This is where you'll put your page files. This is similar to the \"routes\" directory in Remix."] }), _jsxs(Paragraph, { children: ["The rules are very simple:", _jsxs(UnorderedList, { children: [_jsxs(ListItem, { children: ["Pages should include ", _jsx(InlineCode, { children: ".page." }), " (e.g.,", _jsx(InlineCode, { children: "about.page.tsx" }), ") in the filename. If you want co-location in this directory, you can always just exclude the", " ", _jsx(InlineCode, { children: ".page." }), " part in any filename (e.g.,", _jsx(InlineCode, { children: "about-components.tsx" }), ")."] }), _jsxs(ListItem, { children: ["Directory names will become part of the path, unless they are prefixed with double underscores. For example, if you have a", " ", _jsx(InlineCode, { children: "src/pages/foo" }), " directory, and a file inside the ", _jsx(InlineCode, { children: "foo" }), " directory called", " ", _jsx(InlineCode, { children: "bar.page.tsx" }), " (", _jsx(InlineCode, { children: "/src/pages/foo/bar.page.tsx" }), "), the path would be ", _jsx(InlineCode, { children: "example.com/foo/bar" }), ". If you want the directory to be ignored, prefix it with two underscores (e.g., ", _jsx(InlineCode, { children: "__foo" }), "). In that case, the route will just be ", _jsx(InlineCode, { children: "example.com/bar" }), "."] }), _jsxs(ListItem, { children: ["If you want a default index page inside at any route, just include an ", _jsx(InlineCode, { children: "_index.page.tsx" }), " file in that directory. This includes the ", _jsx(InlineCode, { children: "pages" }), " directory itself;", " ", _jsx(InlineCode, { children: "/src/pages/_index.page.tsx" }), " will be the default route for your site."] }), _jsxs(ListItem, { children: ["If you want to include a layout for a route (e.g., a sidebar or sub-navigation), include a file with the same name as the directory (but with ", _jsx(InlineCode, { children: ".page.tsx" }), " included) as a sibling to the route directory. For example, if you have a route at", " ", _jsx(InlineCode, { children: "/foo/bar" }), ", you can include a layout at", _jsx(InlineCode, { children: "/src/pages/foo/bar.page.tsx" }), " and a default page at ", _jsx(InlineCode, { children: "/src/pages/foo/bar/_index.page.tsx" }), ". Note that any layouts for your main home page (", _jsx(InlineCode, { children: "src/_index.page.tsx" }), "), such as a global navigation header, should be inserted into your root component that is rendered from your main server entry point (i.e.,", " ", _jsx(InlineCode, { children: "src/main.tsx" }), ")."] }), _jsxs(ListItem, { children: ["If you want to include dynamic child routes, you can just prefix the file name with a dollar sign (", _jsx(InlineCode, { children: "$" }), "). For example, if you have a route at ", _jsx(InlineCode, { children: "/foo/bar" }), ", you can include a dynamic child route at", " ", _jsx(InlineCode, { children: "/src/pages/foo/bar/$id.page.tsx" }), ". This will match any route that starts with ", _jsx(InlineCode, { children: "/foo/bar/" }), " ", "and will pass the id as a parameter to the page (including the page's loader, action, and component... more on this later). The", " ", _jsx(InlineCode, { children: "/foo/bar" }), " route will still render the index page, if you have one.", _jsx("br", {}), _jsx("br", {}), "NOTE: One \"gotcha\" with this is that you need to use a string that would be safe to use as a JavaScript variable for your dynamic properties. For example,", " ", _jsx(InlineCode, { children: "/src/pages/$user_id.page.tsx" }), " would be fine, but ", _jsx(InlineCode, { children: "/src/pages/$user-id.page.tsx" }), " would not."] }), _jsxs(ListItem, { children: ["If you want to have \"catch-all\" or \"splat\" routes, you can include a file named simply ", _jsx(InlineCode, { children: "$.page.tsx" }), ". This will match any route that hasn't already been matched by a more specific route. You can also include a top-level 404 page by including a file named ", _jsx(InlineCode, { children: "$.page.tsx" }), " in", " ", _jsx(InlineCode, { children: "src/pages" }), ". Any splat parameters \"caught\" by one of these routes will be passed into the page."] })] })] }), _jsx(AnchorHeading, { content: "Page components" }), _jsxs(Paragraph, { children: ["Pages are very simple as well. They are simply JSX components default exported from a page route file. Again, a page route file is any file in the ", _jsx(InlineCode, { children: "src/pages" }), " directory that includes", " ", _jsx(InlineCode, { children: ".page." }), " in the filename. For example,", _jsx(InlineCode, { children: "src/pages/about.page.tsx" }), " is a page file."] }), _jsx(CodeBlock, { language: "typescript", code: ` +// src/pages/about.page.tsx + +export default function () { + return ( +

+ I like baseball. +

+ ) +} + ` }), _jsxs(Paragraph, { children: ["Pages are passed a ", _jsx(InlineCode, { children: "PageProps" }), " object, which contains a bunch of helpful properties. Here are all the properties available on the PageProps object:"] }), _jsx(CodeBlock, { language: "typescript", code: ` +export default function ({ + c, + loaderData, + actionData, + outlet, + params, + path, + splatSegments, +}: PageProps) { + return ( +

+ I like {loaderData?.sport}. +

+ ) +} + ` }), _jsx(Paragraph, { children: _jsxs(UnorderedList, { children: [_jsxs(ListItem, { children: [_jsx(InlineCode, { children: "c" }), " - This is the Hono Context object. It contains the request and response objects, as well as some other useful properties and methods. See the", " ", _jsx("a", { href: "https://hono.dev", target: "_blank", class: "underline", children: "Hono docs" }), " ", "for more info."] }), _jsxs(ListItem, { children: [_jsx(InlineCode, { children: "loaderData" }), " - This is the data returned from the route loader. If you aren't using a route loader, this will be", " ", _jsx(InlineCode, { children: "undefined" }), ". If you are using a route loader and pass in ", _jsx(InlineCode, { children: "typeof loader" }), " as a generic to", " ", _jsx(InlineCode, { children: "PageProps" }), ", this will be 100% type-safe."] }), _jsxs(ListItem, { children: [_jsx(InlineCode, { children: "actionData" }), " - Same as", " ", _jsx(InlineCode, { children: "loaderData" }), ", except in this case the data comes from your route's action, if applicable. If you are using a route action but ", _jsx(Boldtalic, { children: "not" }), " a route loader, this is how you'd handle the generics:", " ", _jsx(InlineCode, { children: `PageProps` }), "."] }), _jsxs(ListItem, { children: [_jsx(InlineCode, { children: "outlet" }), " - This is the outlet for the page, and it's where child routes get rendered. Because page components are async, you should render outlets like this:", " ", _jsx(InlineCode, { children: `{await outlet()}` }), ", regardless of whether you're actually doing anything asynchronous inside of them."] }), _jsxs(ListItem, { children: [_jsx(InlineCode, { children: "params" }), " - This is an object containing any parameters passed to the page. For example, if you have a page at", " ", _jsx(InlineCode, { children: "src/pages/foo/bar/$id.page.tsx" }), ", the", " ", _jsx(InlineCode, { children: "params" }), " object will contain a property called ", _jsx(InlineCode, { children: "id" }), " with the value of the", " ", _jsx(InlineCode, { children: "id" }), " parameter. In other words, if the user visits the route ", _jsx(InlineCode, { children: "example.com/foo/bar/123" }), ", the ", _jsx(InlineCode, { children: "params" }), " object will be", " ", _jsx(InlineCode, { children: `{ id: '123' }` }), "."] }), _jsxs(ListItem, { children: [_jsx(InlineCode, { children: "splatSegments" }), " - This is an array of any \"splat\" segments caught by the \"deepest\" splat route. For example, if you have a page at", " ", _jsx(InlineCode, { children: "src/pages/foo/bar/$.page.tsx" }), " (a splat route) and the user visits", " ", _jsx(InlineCode, { children: "example.com/foo/bar/123/456" }), ", the", " ", _jsx(InlineCode, { children: "splatSegments" }), " array will be", " ", _jsx(InlineCode, { children: `['123', '456']` }), "."] })] }) }), _jsxs(Paragraph, { children: [_jsx(InlineCode, { children: "PageProps" }), " is also a generic type, which takes", " ", _jsx(InlineCode, { children: "typeof loader" }), " and", " ", _jsx(InlineCode, { children: "typeof action" }), " as its two parameters, respectively. These are the types of the loader and action functions for the page (more on this later). If you aren't using data functions for a certain page, you can just skip the generics."] }), _jsx(Paragraph, { children: "One cool thing about Hwy is that you have access to the Hono Context from within your page components. This means you can do things like set response headers right inside your page components. You can also do this from loaders and actions if you prefer." }), _jsx(CodeBlock, { language: "typescript", code: ` +import { PageProps } from 'hwy' + +export default function ({ c }: PageProps) { + c.res.headers.set('cache-control', 'whatever') + + return +} + ` }), _jsx(AnchorHeading, { content: "Page loaders" }), _jsxs(Paragraph, { children: ["Page loaders are functions named \"loader\" that are exported from a page file. They are passed a subset of the PageProps object:", " ", _jsx(InlineCode, { children: "c" }), ", ", _jsx(InlineCode, { children: "params" }), ", and", " ", _jsx(InlineCode, { children: "splatSegments" }), ". The typescript type exported by Hwy for this object is called ", _jsx(InlineCode, { children: "DataFunctionArgs" }), ", which can take an optional generic of your Hono Env type (see the Hono docs for more details on that, and why you might want to do that)."] }), _jsx(Paragraph, { children: "Loaders run before your page is returned, and they all run in parallel. They are useful for fetching data, and any data returned from a loader will be passed to its associated page component. They can also be useful for redirecting to another page (covered a little later)." }), _jsx(Paragraph, { children: "If you want to consume data from a loader in your page component (usually you will), then you should just return standard raw data from the loader, like this:" }), _jsx(CodeBlock, { language: "typescript", code: ` +import type { DataFunctionArgs } from 'hwy' + +export function loader({ c }: DataFunctionArgs) { + return "baseball" as const +} + +export default function ({ loaderData }: PageProps) { + return ( +

+ I like {loaderData}. // I like baseball. +

+ ) +} +` }), _jsx(Paragraph, { children: "If you return a Response object, then that will \"take over\" and be returned from the route instead of your page. This is fine if you're creating a \"resource route\" (more on this below), but usually it's not what you want." }), _jsx(Paragraph, { children: "However, one thing that is more common is that you may want to return a redirect from a loader. You can do this with a Response object if you want, but Hwy exports a helper function that covers more edge cases and is built to work nicely with the HTMX request lifecycle." }), _jsx(CodeBlock, { language: "typescript", code: ` +import { redirect, type DataFunctionArgs } from 'hwy' + +export function loader({ c }: DataFunctionArgs) { + return redirect({ c, to: '/login' }) +} +` }), _jsxs(Paragraph, { children: ["You can also \"throw\" a ", _jsx(InlineCode, { children: "redirect" }), " if you want, which can be helpful in keeping your typescript types clean."] }), _jsx(AnchorHeading, { content: "Server components" }), _jsx(Paragraph, { children: "In addition to using loaders to load data in parallel before rendering any components, you can also load data inside your Hwy page components. Be careful with this, as it can introduce waterfalls, but if you are doing low-latency data fetching and prefer that pattern, it's available to you in Hwy." }), _jsx(CodeBlock, { language: "typescript", code: ` +// src/some-page.page.tsx + +export default async function ({ outlet }: PageProps) { + const someData = await getSomeData() + + return ( +
+ {JSON.stringify(someData)} + + {await outlet()} +
+ ) +} + ` }), _jsx(Paragraph, { children: "You can also pass data to the child outlet if you want, and it will be available in the child page component's props. Here's how that would look in the parent page component:" }), _jsx(CodeBlock, { language: "typescript", code: `{await outlet({ someData })}` }), _jsxs(Paragraph, { children: ["And in the child component, you'll want to use", " ", _jsx(InlineCode, { children: `PageProps & { someData: SomeType }` }), " as your prop type."] }), _jsxs(Paragraph, { children: ["Because page components are async and let you fetch data inside them, be sure to always await your ", _jsx(InlineCode, { children: "outlet" }), " calls, like this: ", _jsx(InlineCode, { children: `{await outlet()}` }), ". If you don't, things might not render correctly."] }), _jsxs(Paragraph, { children: ["Another way of doing this would be to use Hono's", " ", _jsx(InlineCode, { children: "c.set('some-key', someData)" }), " feature. If you do that, any child component will be able to access the data without re-fetching via ", _jsx(InlineCode, { children: "c.get('some-key')" }), "."] }), _jsx(Paragraph, { children: "The world is your oyster!" }), _jsx(AnchorHeading, { content: "Page actions" }), _jsx(Paragraph, { children: "Page actions behave just like loaders, except they don't run until you call them (usually from a form submission). Loaders are for loading/fetching data, and actions are for mutations. Use actions when you want to log users in, mutate data in your database, etc." }), _jsxs(Paragraph, { children: ["Data returned from an action will be passed to the page component as the", " ", _jsx(InlineCode, { children: "actionData" }), " property on the", " ", _jsx(InlineCode, { children: "PageProps" }), " object. Unlike loaders, which are designed to run in parallel and pass different data to each nested component, actions are called individually and return the same", " ", _jsx(InlineCode, { children: "actionData" }), " to all nested components."] }), _jsx(Paragraph, { children: "Here is an example page with a login form. Note that this is highly simplified and not intended to be used in production. It is only intended to show how actions work." }), _jsx(CodeBlock, { language: "typescript", code: ` +import { DataFunctionArgs, PageProps } from 'hwy' +import { extractFormData, logUserIn } from './pretend-lib.js' + +export async function action({ c }: DataFunctionArgs) { + const { email, password } = await extractFormData({ c }) + return await logUserIn({ email, password }) +} + +export default function ({ actionData }: PageProps) { + if (actionData?.success) return

Success!

+ + return ( +
+ + + +
+ ) +} + ` }), _jsxs(Paragraph, { children: ["This form uses 100% standard html attributes, and it will be automatically progressively enhanced by HTMX (uses the", " ", _jsx(InlineCode, { children: "hx-boost" }), " feature). If JavaScript doesn't load for some reason, it will fall back to traditional web behavior (full-page reload)."] }), _jsx(AnchorHeading, { content: "Resource routes" }), _jsx(Paragraph, { children: "Remix has the concept of \"resource routes\", where you can define loaders and actions without defining a default export component, and then use them to build a view-less API." }), _jsxs(Paragraph, { children: ["In Hwy, you're probably better off just using your Hono server directly for this, as it's arguably more traditional, straightforward, and convenient. However, if you really want to use resource routes with Hwy's file-based routing, nothing is stopping you! You can do so by just making sure you return a fetch ", _jsx(InlineCode, { children: "Response" }), " object from your loader or action. For example:"] }), _jsx(CodeBlock, { language: "typescript", code: ` +// src/pages/api/example-resource-root.ts + +export function loader() { + return new Response('Hello from resource route!') +} +` }), _jsxs(Paragraph, { children: ["All of that being said, Hwy is designed to work with HTMX-style hypermedia APIs, not JSON APIs. So if you return JSON from a resource route or a normal Hono endpoint, you'll be in charge of handling that on the client side. This will likely entail disabling HTMX on the form submission, doing an ", _jsx(InlineCode, { children: "e.preventDefault()" }), " in the form's onsubmit handler, and then doing a standard fetch request to the Hono endpoint. You can then parse the JSON response and do whatever you want with it."] }), _jsx(Paragraph, { children: "You probably don't need to do this, and if you think you do, I would challenge you to try using the hypermedia approach instead. If you still decide you need to use JSON, this is roughly the escape hatch." }), _jsx(AnchorHeading, { content: "Error boundaries" }), _jsxs(Paragraph, { children: ["Any Hwy page can export an ", _jsx(InlineCode, { children: "ErrorBoundary" }), " ", "component, which takes the same parameters as", " ", _jsx(InlineCode, { children: "loaders" }), " and ", _jsx(InlineCode, { children: "actions" }), ", as well as the error itself. The type for the ErrorBoundary component props is exported as ", _jsx(InlineCode, { children: "ErrorBoundaryProps" }), ". If an error is thrown in the page or any of its children, the error will be caught and passed to the nearest applicable parent error boundary component. You can also pass a default error boundary component that effectively wraps your outermost ", _jsx(InlineCode, { children: "rootOutlet" }), " (in", " ", _jsx(InlineCode, { children: "main.tsx" }), ") like so:"] }), _jsx(CodeBlock, { language: "typescript", code: ` +import type { ErrorBoundaryProps } from 'hwy' + +... + +{await rootOutlet({ + activePathData, + c, + fallbackErrorBoundary: (props: ErrorBoundaryProps) => { + return
{props.error.message}
+ }, +})} + ` }), _jsx(AnchorHeading, { content: "Hono middleware and variables" }), _jsx(Paragraph, { children: "You will very likely find yourself in a situation where there is some data you'd like to fetch before you even run your loaders, and you'd like that data to be available in all downstream loaders and page components. Here's how you might do it:" }), _jsx(CodeBlock, { language: "typescript", code: ` +app.use('*', async (c, next) => { + const user = await getUserFromCtx(c) + + c.set('user', user) + + await next() +}) +` }), _jsx(Paragraph, { children: "This isn't very typesafe though, so you'll want to make sure you create app-specific types. For this reason, all Hwy types that include the Hono Context object are generic, and you can pass your app-specific Hono Env type as a generic to the PageProps object. For example:" }), _jsx(CodeBlock, { language: "typescript", code: ` +import type { DataFunctionArgs } from 'hwy' + +type AppEnv = { + Variables: { + user: Awaited> + } +} + +type AppDataFunctionArgs = DataFunctionArgs + +export async function loader({ c }: AppDataFunctionArgs) { + // this will be type safe! + const user = c.get('user') +} +` }), _jsx(AnchorHeading, { content: "Main.tsx" }), _jsxs(Paragraph, { children: ["In your ", _jsx(InlineCode, { children: "main.tsx" }), " file, you'll have a handler that looks something like this."] }), _jsx(CodeBlock, { language: "typescript", code: ` +import { + CssImports, + rootOutlet, + DevLiveRefreshScript, + ClientScripts, + getDefaultBodyProps, + renderRoot, +} from 'hwy' + +app.all('*', async (c, next) => { + return await renderRoot(c, next, async ({ activePathData }) => { + return ( + + + + + + + + + + + + + + {await rootOutlet({ + c, + activePathData, + fallbackErrorBoundary: () => { + return
Something went wrong!
+ }, + })} + + + ) + } +}) + ` }), _jsxs(Paragraph, { children: ["The easiest way to get this set up correctly is to bootstrap your app with ", _jsx(InlineCode, { children: "npx create-hwy@latest" }), "."] }), _jsx(AnchorHeading, { content: "Document head (page metadata)" }), _jsxs(Paragraph, { children: ["Your document's ", _jsx(InlineCode, { children: "head" }), " is rendered via the", " ", _jsx(InlineCode, { children: `HeadElements` }), " component in your", " ", _jsx(InlineCode, { children: "main.tsx" }), " file, like this:"] }), _jsx(CodeBlock, { language: "typescript", code: ` + + ` }), _jsx(Paragraph, { children: "As you can probably see, the \"defaults\" property takes an array of head elements. \"Title\" is a special one, in that it is just an object with a title key. Other elements are just objects with a tag and props key. The props key is an object of key-value pairs that will be spread onto the element." }), _jsxs(Paragraph, { children: ["The defaults you set here can be overridden at any Hwy page component by exporting a ", _jsx(InlineCode, { children: "head" }), " function. For example:"] }), _jsx(CodeBlock, { language: "typescript", code: ` +import { HeadFunction } from 'hwy' + +export const head: HeadFunction = (props) => { + // props are the same as PageProps, but without the outlet + + return [ + { title: 'Some Child Page' }, + { + tag: 'meta', + props: { + name: 'description', + content: + 'Description for some child page.', + }, + }, + ] +} +` }), _jsxs(Paragraph, { children: ["This will override any conflicting head elements set either by an ancestor page component or by the root defaults. The", " ", _jsx(InlineCode, { children: "head" }), " function is passed all the same props as a page component, excluding ", _jsx(InlineCode, { children: "outlet" }), "."] }), _jsx(AnchorHeading, { content: "Styling" }), _jsxs(Paragraph, { children: ["Hwy includes built-in support for several CSS patterns, including a very convenient way to inline critical CSS. CSS is rendered into your app through the ", _jsx(InlineCode, { children: "CssImports" }), " component. That component in turn reads from the ", _jsx(InlineCode, { children: "src/styles" }), " ", "directory, which is where you should put your CSS files. Inside the styles directory, you can put two types of CSS files:", " ", _jsx(InlineCode, { children: "critical" }), " and ", _jsx(InlineCode, { children: "bundle" }), ". Any files that include ", _jsx(InlineCode, { children: ".critical." }), " in the filename will be concatenated (sorted alphabetically by file name), processed by esbuild, and inserted inline into the", " ", _jsx(InlineCode, { children: "head" }), " of your document. Any files that include", " ", _jsx(InlineCode, { children: ".bundle." }), " in the filename will similarly be concatenated (sorted alphabetically by file name), processed by esbuild, and inserted as a standard linked stylesheet in the", " ", _jsx(InlineCode, { children: "head" }), " of your document."] }), _jsxs(Paragraph, { children: ["It's also very easy to configure Tailwind, if that's your thing. To see how this works, spin up a new project with", " ", _jsx(InlineCode, { children: "npx create-hwy@latest" }), " and select \"Tailwind\" when prompted."] }), _jsx(Paragraph, { children: "And of course, if you don't like these patterns, you can just choose not to use them, and do whatever you want for styling instead!" }), _jsx(AnchorHeading, { content: "Deployment" }), _jsxs(Paragraph, { children: ["Hwy can be deployed to any Node-compatible runtime with filesystem read access. This includes more traditional Node app hosting like Render.com or Railway.app, or Vercel (Lambda), or Deno Deploy. This should also include Bun once that ecosystem becomes more stable and has more hosting options. Just choose your preferred deployment target when you run", " ", _jsx(InlineCode, { children: "npx create-hwy@latest" }), "."] }), _jsx(Paragraph, { children: "Cloudflare is a bit trickier, however, because Hwy reads from the filesystem at runtime. We may add support for this in the future through a specialized build step, but for now, it's not supported. This also means that Vercel edge functions are not supported, as they rely on Cloudflare Workers, which do not have runtime read access to the filesystem. Normal Vercel serverless, which runs on AWS Lambda under the hood, will work just fine." }), _jsx(AnchorHeading, { content: "Progressive enhancement" }), _jsxs(Paragraph, { children: ["When you included the ", _jsx(InlineCode, { children: "hx-boost" }), " attribute on the", " ", _jsx(InlineCode, { children: "body" }), " tag (included by default when you use", " ", _jsx(InlineCode, { children: "getDefaultBodyProps" }), "), anchor tags (links) and form submissions will be automatically progressively enhanced. For forms, include the traditional attributes, like this:", _jsx(CodeBlock, { language: "typescript", code: `
` })] }), _jsx(AnchorHeading, { content: "Random" }), _jsx(Paragraph, { children: "Here is some random stuff that is worth noting, but doesn't fit well into any existing sections." }), _jsxs(UnorderedList, { children: [_jsx(ListItem, { children: "HTMX handles scroll restoration for you!" }), _jsx(ListItem, { children: "Code splitting is not a concern with this architecture." }), _jsxs(ListItem, { children: [_jsx(InlineCode, { children: "@hwy/dev" }), " is in a separate package so that it doesn't need to be loaded in production. This probably doesn't matter much, but theoretically it could help with cold starts if you're deploying to serverless."] }), _jsx(ListItem, { children: "Never have to fix a hydration error again." })] }), _jsx(AnchorHeading, { content: "Using Hwy without HTMX" }), _jsxs(Paragraph, { children: ["If you would like to use Hwy like a traditional MPA framework, and skip using HTMX, you can do so simply by excluding HTMX from your", " ", _jsx(InlineCode, { children: "src/client.entry.ts" }), " file."] }), _jsx(AnchorHeading, { content: "Security" }), _jsx(Paragraph, { children: "A few points on security:" }), _jsxs(UnorderedList, { children: [_jsxs(ListItem, { children: [_jsx(Paragraph, { children: "Similar to React, Hono JSX rendering will automatically escape the outputted html. If you want to render scripts, you should do the classic React thing (works the same with Hono JSX):" }), _jsx("br", {}), _jsx(CodeBlock, { language: "typescript", code: ` +