diff --git a/README.md b/README.md index f5d3221..b4eed16 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,51 @@ The `req` JSX prop will be given the `Hono` request: To put a file in `./src/pages` but prevent it from being server, preface it with `_`. +### SPA Shell Mode + +If `src/client/index.tsx` (or `src/client/index.ts`) exists, hype automatically serves a minimal HTML shell at `/` - no `src/pages/index.tsx` needed. + +``` +src/ + client/ + index.tsx <- This triggers SPA shell mode! + server/ + index.ts +``` + +The shell HTML served at `/`: + +```html + + + + App + + + + + +
+ + + +``` + +Your client code mounts to `#app`: + +```tsx +// src/client/index.tsx +import { render } from 'hono/jsx/dom' + +function App() { + return

Hello from the SPA!

+} + +render(, document.getElementById('app')!) +``` + +See `examples/spa-shell` for a complete example. + ### Layout `hype` will wrap everything in a simple default layout unless `./src/pages/_layout.tsx` exists, diff --git a/examples/spa-shell/README.md b/examples/spa-shell/README.md new file mode 100644 index 0000000..51b412c --- /dev/null +++ b/examples/spa-shell/README.md @@ -0,0 +1,44 @@ +# SPA Shell Mode Example + +This example demonstrates the SPA shell mode feature. When `src/client/index.tsx` (or `.ts`) exists, hype automatically serves a minimal HTML shell at `/` - no `src/pages/index.tsx` needed. + +## Structure + +``` +src/ + client/ + index.tsx <- This triggers SPA shell mode! + server/ + index.ts +``` + +## Run + +```sh +bun install +bun dev +``` + +Visit http://localhost:3000 to see the SPA in action. + +## How it works + +When hype detects `src/client/index.tsx` or `src/client/index.ts`, it serves this HTML at `/`: + +```html + + + + App + + + + + +
+ + + +``` + +Your client code mounts to `#app` and takes over from there. diff --git a/examples/spa-shell/package.json b/examples/spa-shell/package.json new file mode 100644 index 0000000..379de9f --- /dev/null +++ b/examples/spa-shell/package.json @@ -0,0 +1,10 @@ +{ + "name": "spa-shell-example", + "scripts": { + "start": "bun run src/server/index.ts", + "dev": "bun run --hot src/server/index.ts" + }, + "dependencies": { + "hype": "link:../.." + } +} diff --git a/examples/spa-shell/src/client/index.tsx b/examples/spa-shell/src/client/index.tsx new file mode 100644 index 0000000..fdc6179 --- /dev/null +++ b/examples/spa-shell/src/client/index.tsx @@ -0,0 +1,21 @@ +import { render } from 'hono/jsx/dom' +import { useState } from 'hono/jsx/dom' + +function App() { + const [count, setCount] = useState(0) + + return ( + <> +

SPA Shell Mode

+

This page is served from a minimal HTML shell - no src/pages/index.tsx needed!

+

Count: {count}

+
+ + {' '} + +
+ + ) +} + +render(, document.getElementById('app')!) diff --git a/examples/spa-shell/src/server/index.ts b/examples/spa-shell/src/server/index.ts new file mode 100644 index 0000000..d534b9f --- /dev/null +++ b/examples/spa-shell/src/server/index.ts @@ -0,0 +1,8 @@ +import { Hype } from 'hype' + +const app = new Hype() + +// Add your API routes here +app.get('/api/hello', (c) => c.json({ message: 'Hello from the API!' })) + +export default app.defaults diff --git a/examples/spa-shell/tsconfig.json b/examples/spa-shell/tsconfig.json new file mode 100644 index 0000000..1c47734 --- /dev/null +++ b/examples/spa-shell/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "jsxImportSource": "hono/jsx", + "allowJs": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false, + "baseUrl": ".", + "paths": { + "$*": ["src/server/*"], + "#*": ["src/client/*"], + "@*": ["src/shared/*"] + } + } +} diff --git a/src/index.tsx b/src/index.tsx index f5c5947..faef3c8 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -19,6 +19,22 @@ export type { Context } from 'hono' const pageCache = new Map() +function generateShellHtml(title = 'App', mountId = 'app', clientEntry = '/client/index.js'): string { + return ` + + + ${title} + + + + + +
+ + +` +} + export type HypeProps = { pico?: boolean reset?: boolean @@ -68,7 +84,7 @@ export class Hype< }) } - registerRoutes() { + async registerRoutes() { if (this.routesRegistered) return this.routesRegistered = true @@ -167,6 +183,15 @@ export class Hype< return render404(c) }) + // SPA auto-detection: serve shell if src/client/index.tsx exists + const clientIndexPath = join(process.env.PWD ?? '.', 'src/client/index.tsx') + const clientIndexTsPath = join(process.env.PWD ?? '.', 'src/client/index.ts') + const hasSpaClient = await Bun.file(clientIndexPath).exists() || await Bun.file(clientIndexTsPath).exists() + + if (hasSpaClient) { + this.get('/', (c) => c.html(generateShellHtml())) + } + // file based routing this.on('GET', ['/', '/:page'], async c => { const pageName = (c.req.param('page') ?? 'index').replace('.', '')