@rshono/create 1.0.0-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -0
- package/bin/create-rshono.mjs +29 -0
- package/dist/api.mjs +749 -0
- package/dist/cli.mjs +3930 -0
- package/package.json +59 -0
- package/templates/base/README.md +42 -0
- package/templates/base/_env +9 -0
- package/templates/base/_gitignore +8 -0
- package/templates/base/public/favicon.svg +1 -0
- package/templates/base/public/robots.txt +2 -0
- package/templates/base/rshono.config.ts +35 -0
- package/templates/base/src/actions.ts +19 -0
- package/templates/base/src/components/404.tsx +17 -0
- package/templates/base/src/components/500.tsx +21 -0
- package/templates/base/src/components/greet-form.tsx +27 -0
- package/templates/base/src/components/home.tsx +50 -0
- package/templates/base/src/components/layout.tsx +45 -0
- package/templates/base/src/env.d.ts +2 -0
- package/templates/base/src/lib/env.ts +26 -0
- package/templates/base/src/routes.ts +32 -0
- package/templates/base/src/server.ts +59 -0
- package/templates/base/src/styles.css +178 -0
- package/templates/base/tsconfig.json +20 -0
- package/templates/biome/biome.json +7 -0
- package/templates/biome-tailwind/biome.json +7 -0
- package/templates/oxfmt/_oxfmtrc.json +7 -0
- package/templates/oxlint/_oxlintrc.json +7 -0
- package/templates/prettier/_prettierignore +10 -0
- package/templates/prettier/_prettierrc.json +7 -0
- package/templates/tailwind/postcss.config.mjs +9 -0
- package/templates/tailwind/rshono.config.ts +45 -0
- package/templates/tailwind/src/components/home.tsx +50 -0
- package/templates/tailwind/src/components/layout.tsx +45 -0
- package/templates/tailwind/src/styles.css +40 -0
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rshono/create",
|
|
3
|
+
"version": "1.0.0-rc.0",
|
|
4
|
+
"description": "Scaffold a new rshono app — Hono + Rspack + React Server Components",
|
|
5
|
+
"author": "Lasse <lasse@lassetange.com> (https://www.lassetange.com)",
|
|
6
|
+
"license": "ISC",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"create",
|
|
9
|
+
"scaffold",
|
|
10
|
+
"starter",
|
|
11
|
+
"rshono",
|
|
12
|
+
"react-server-components",
|
|
13
|
+
"hono",
|
|
14
|
+
"rspack"
|
|
15
|
+
],
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/rshono/rshono.git",
|
|
19
|
+
"directory": "packages/create"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/rshono/rshono/issues"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/rshono/rshono",
|
|
25
|
+
"type": "module",
|
|
26
|
+
"bin": {
|
|
27
|
+
"create-rshono": "./bin/create-rshono.mjs"
|
|
28
|
+
},
|
|
29
|
+
"exports": {
|
|
30
|
+
".": "./dist/api.mjs",
|
|
31
|
+
"./package.json": "./package.json"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"bin",
|
|
35
|
+
"dist",
|
|
36
|
+
"templates"
|
|
37
|
+
],
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=22.1.0"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "node scripts/codegen.mjs && node scripts/build.mjs",
|
|
46
|
+
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
47
|
+
"codegen": "node scripts/codegen.mjs",
|
|
48
|
+
"typecheck": "node scripts/codegen.mjs && tsc --noEmit",
|
|
49
|
+
"test": "npm run build && node --test --test-concurrency=1 \"test/*.test.mjs\"",
|
|
50
|
+
"prepack": "npm run build"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@clack/prompts": "^1.7.0",
|
|
54
|
+
"@rspack/core": "2.1.5",
|
|
55
|
+
"@types/node": "^26.1.1",
|
|
56
|
+
"@rshono/core": "workspace:*",
|
|
57
|
+
"typescript": "^7.0.2"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# **PROJECT_NAME**
|
|
2
|
+
|
|
3
|
+
A [rshono](https://github.com/rshono/rshono) app — [Hono](https://hono.dev) + [Rspack](https://rspack.rs) + [React Server Components](https://react.dev/reference/rsc/server-components).
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
__PM_RUN__ dev # dev server with HMR, http://localhost:3000
|
|
7
|
+
__PM_RUN__ build # production build for __DEPLOY_TARGET__
|
|
8
|
+
__PM_RUN__ typecheck # tsc --noEmit
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
`package.json` has the rest, including whatever your formatter and linter added.
|
|
12
|
+
|
|
13
|
+
## Layout
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
rshono.config.ts deploy target, security and build settings
|
|
17
|
+
public/ served verbatim at the web root (favicon.svg → /favicon.svg)
|
|
18
|
+
src/
|
|
19
|
+
routes.ts the route table — the one file rshono requires
|
|
20
|
+
server.ts a Hono app: middleware, API routes, redirects, error reporting
|
|
21
|
+
actions.ts 'use server' functions the browser can call
|
|
22
|
+
components/ pages and components
|
|
23
|
+
lib/ everything else
|
|
24
|
+
styles.css imported by the layout, so it loads with the page
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Pages are **server components**: they render the whole document, may be `async`, and await data directly.
|
|
28
|
+
Interactive parts are `'use client'` components a page imports — only those ship JavaScript.
|
|
29
|
+
|
|
30
|
+
## Environment
|
|
31
|
+
|
|
32
|
+
`.env` holds committed defaults; `.env.local` overrides it and is gitignored. Only `PUBLIC_`-prefixed
|
|
33
|
+
variables reach the browser — everything else is server-only, and a stray read of it in client code
|
|
34
|
+
compiles to `undefined` rather than shipping. `src/lib/env.ts` is where both sides are read.
|
|
35
|
+
|
|
36
|
+
## Deploying
|
|
37
|
+
|
|
38
|
+
This app is built for `__DEPLOY_TARGET__`: after `__PM_RUN__ build`, **DEPLOY_HINT**.
|
|
39
|
+
|
|
40
|
+
Change `deploy` in `rshono.config.ts` to target somewhere else, or build for one place without editing the
|
|
41
|
+
file: `rshono build --deploy vercel`, or `RSHONO_DEPLOY=vercel` in CI. `dev` always runs the Node dev
|
|
42
|
+
server, whatever the target — it is a property of the build, not of developing.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Committed defaults — safe to share, and the values a fresh clone starts with.
|
|
2
|
+
#
|
|
3
|
+
# Secrets go in `.env.local`, which is gitignored and loaded first, so a value there wins over this
|
|
4
|
+
# file. The real environment wins over both.
|
|
5
|
+
#
|
|
6
|
+
# Only `PUBLIC_`-prefixed variables are compiled into the browser bundle. Anything else is server-only:
|
|
7
|
+
# a stray read of it in client code compiles to `undefined` rather than shipping.
|
|
8
|
+
|
|
9
|
+
PUBLIC_APP_NAME="__PROJECT_NAME__"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><title>Lightning bolt</title><text y=".9em" font-size="90">⚡</text></svg>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { defineConfig } from '@rshono/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Every field is optional — delete this file to accept all the defaults. The commented lines are the
|
|
5
|
+
* defaults, kept as documentation of what is there to change.
|
|
6
|
+
*
|
|
7
|
+
* The framework settings are compiled into the server bundle at build time, so changing one means a
|
|
8
|
+
* rebuild; there is no env-var interface for them. `--port`/`PORT` and `HOST` are the two exceptions,
|
|
9
|
+
* and they win over what is written here.
|
|
10
|
+
*/
|
|
11
|
+
export default defineConfig({
|
|
12
|
+
/** Where `build` targets. Overridable per build with `--deploy` or `RSHONO_DEPLOY`. */
|
|
13
|
+
deploy: '__DEPLOY_TARGET__',
|
|
14
|
+
|
|
15
|
+
// The public origin, baked into prerendered pages' absolute URLs. Set it if you use `render: 'static'`
|
|
16
|
+
// and build canonical tags, `og:url` or absolute links — there is no request to read a Host from.
|
|
17
|
+
// siteUrl: 'https://example.com',
|
|
18
|
+
|
|
19
|
+
// port: 3000, // default for dev/start
|
|
20
|
+
// host: '0.0.0.0', // bind address for start
|
|
21
|
+
|
|
22
|
+
// trustProxy: false, // honour X-Forwarded-Host/-Proto — only behind a proxy you control
|
|
23
|
+
// checkOrigin: true, // CSRF origin check on server-action POSTs
|
|
24
|
+
// allowedOrigins: [], // extra origins allowed to post actions
|
|
25
|
+
// csp: false, // strict per-request-nonce Content-Security-Policy
|
|
26
|
+
// cspDirectives: {}, // widen it, e.g. { 'img-src': "'self' https://cdn.example.com" }
|
|
27
|
+
// bodySizeLimit: '1mb',// request body cap before a 413; false disables it
|
|
28
|
+
// renderTimeout: 10_000, // ms deadline for a request (action + flight + SSR)
|
|
29
|
+
// compress: true, // gzip; turn off behind a proxy that already does it
|
|
30
|
+
|
|
31
|
+
// Escape hatch: mutate the generated Rspack config just before it compiles. Called once per compiler.
|
|
32
|
+
// rspack(config, { isServer, isDev }) {
|
|
33
|
+
// return config;
|
|
34
|
+
// },
|
|
35
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use server';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A server action: an ordinary async function the browser can call. React hands the client an id for
|
|
5
|
+
* this export and posts to it, so **every `'use server'` export is a public HTTP endpoint** —
|
|
6
|
+
* authenticate, authorize and validate the arguments exactly as you would in a route handler. The
|
|
7
|
+
* framework's CSRF check proves a request came from your own site; it says nothing about who sent it.
|
|
8
|
+
*
|
|
9
|
+
* The signature is the one `useActionState` expects: previous state first, then the form data.
|
|
10
|
+
*/
|
|
11
|
+
export async function greet(_previous: string | null, formData: FormData): Promise<string> {
|
|
12
|
+
const name = String(formData.get('name') ?? '').trim();
|
|
13
|
+
if (!name) return 'Type a name first.';
|
|
14
|
+
|
|
15
|
+
// Where real work goes — a database write, an email, a queue push.
|
|
16
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
17
|
+
|
|
18
|
+
return `Hello, ${name}. This ran on the server.`;
|
|
19
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PageProps } from '@rshono/core';
|
|
2
|
+
import { Layout } from './layout';
|
|
3
|
+
|
|
4
|
+
/** Declared as `notFound` in routes.ts. Answers unmatched paths, and any `notFound()` call, with a 404. */
|
|
5
|
+
export default function NotFound({ url }: PageProps) {
|
|
6
|
+
return (
|
|
7
|
+
<Layout title="Not found">
|
|
8
|
+
<h1>404</h1>
|
|
9
|
+
<p>
|
|
10
|
+
Nothing at <code>{url.pathname}</code>.
|
|
11
|
+
</p>
|
|
12
|
+
<p>
|
|
13
|
+
<a href="/">Back home</a>
|
|
14
|
+
</p>
|
|
15
|
+
</Layout>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ErrorPageProps } from '@rshono/core';
|
|
2
|
+
import { Layout } from './layout';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Declared as `error` in routes.ts. Answers a request that threw, with a 500.
|
|
6
|
+
*
|
|
7
|
+
* `error.message` is the real message in development and a generic `'Internal Server Error'` in
|
|
8
|
+
* production, where `error.stack` is absent — so this component can show it without leaking anything.
|
|
9
|
+
*/
|
|
10
|
+
export default function ServerError({ error }: ErrorPageProps) {
|
|
11
|
+
return (
|
|
12
|
+
<Layout title="Something went wrong">
|
|
13
|
+
<h1>500</h1>
|
|
14
|
+
<p>{error.message}</p>
|
|
15
|
+
{error.stack && <pre>{error.stack}</pre>}
|
|
16
|
+
<p>
|
|
17
|
+
<a href="/">Back home</a>
|
|
18
|
+
</p>
|
|
19
|
+
</Layout>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useActionState } from 'react';
|
|
4
|
+
import { greet } from '../actions';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The client half. `'use client'` is the boundary: this module ships to the browser and hydrates, while
|
|
8
|
+
* everything that only renders it stays on the server.
|
|
9
|
+
*
|
|
10
|
+
* Wiring the action to `<form action>` means it works before hydration and with JavaScript switched off
|
|
11
|
+
* — the browser posts the form, the server runs the action and answers with a fresh page. That is what
|
|
12
|
+
* progressive enhancement buys, and it costs nothing here.
|
|
13
|
+
*/
|
|
14
|
+
export function GreetForm() {
|
|
15
|
+
const [message, action, pending] = useActionState(greet, null);
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<form action={action}>
|
|
19
|
+
<label htmlFor="name">Your name</label>
|
|
20
|
+
<input id="name" name="name" placeholder="Ada" autoComplete="off" />
|
|
21
|
+
<button type="submit" disabled={pending}>
|
|
22
|
+
{pending ? 'Saying hello…' : 'Say hello'}
|
|
23
|
+
</button>
|
|
24
|
+
{message && <output>{message}</output>}
|
|
25
|
+
</form>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { PageProps } from '@rshono/core';
|
|
2
|
+
import { publicEnv } from '../lib/env';
|
|
3
|
+
import type { AppEnv } from '../server';
|
|
4
|
+
import { GreetForm } from './greet-form';
|
|
5
|
+
import { Layout } from './layout';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A page is a React **server component**: it runs on the server only, may be `async`, and can await data
|
|
9
|
+
* directly — no loaders, no client bundle for any of this.
|
|
10
|
+
*
|
|
11
|
+
* The `AppEnv` type argument is what types `ctx.var` key by key; without it `ctx.var` is an open record.
|
|
12
|
+
*/
|
|
13
|
+
export default function Home({ url, ctx }: PageProps<'/', AppEnv>) {
|
|
14
|
+
return (
|
|
15
|
+
<Layout description="A new rshono app.">
|
|
16
|
+
<h1>{publicEnv.appName}</h1>
|
|
17
|
+
<p>
|
|
18
|
+
Edit <code>src/components/home.tsx</code> and save. The page re-renders in place — the form below keeps whatever you have typed in it.
|
|
19
|
+
</p>
|
|
20
|
+
|
|
21
|
+
<h2>Server actions</h2>
|
|
22
|
+
<p>
|
|
23
|
+
This form calls a <code>'use server'</code> function in <code>src/actions.ts</code>. It works before hydration and with JavaScript disabled.
|
|
24
|
+
</p>
|
|
25
|
+
<GreetForm />
|
|
26
|
+
|
|
27
|
+
<h2>Where things are</h2>
|
|
28
|
+
<ul>
|
|
29
|
+
<li>
|
|
30
|
+
<code>src/routes.ts</code> — the route table, the one file rshono requires
|
|
31
|
+
</li>
|
|
32
|
+
<li>
|
|
33
|
+
<code>src/server.ts</code> — a Hono app for middleware and API routes, mounted ahead of the pages
|
|
34
|
+
</li>
|
|
35
|
+
<li>
|
|
36
|
+
<code>src/components/</code> — pages and components; <code>src/lib/</code> — everything else
|
|
37
|
+
</li>
|
|
38
|
+
<li>
|
|
39
|
+
<code>rshono.config.ts</code> — deploy target, security and build settings
|
|
40
|
+
</li>
|
|
41
|
+
</ul>
|
|
42
|
+
|
|
43
|
+
<p>
|
|
44
|
+
{/* `ctx` is the request context — cookies, headers, env, middleware variables — handed to the page as a
|
|
45
|
+
prop, so reading it needs no import. It is server-only and never crosses into a client component. */}
|
|
46
|
+
Rendered on the server for <code>{url.pathname}</code>, request <code>{ctx.var.requestId}</code>.
|
|
47
|
+
</p>
|
|
48
|
+
</Layout>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { NavigationProgress } from '@rshono/core/client';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
import { publicEnv } from '../lib/env';
|
|
4
|
+
import '../styles.css';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A page renders the whole document, so the shell lives in one component every page wraps its content
|
|
8
|
+
* in. Importing the stylesheet here is what attaches it to each of those pages.
|
|
9
|
+
*/
|
|
10
|
+
export function Layout({ title, description, children }: { title?: string; description?: string; children: ReactNode }) {
|
|
11
|
+
const heading = title ? `${title} · ${publicEnv.appName}` : publicEnv.appName;
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<html lang="en">
|
|
15
|
+
<head>
|
|
16
|
+
<meta charSet="utf-8" />
|
|
17
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
18
|
+
<title>{heading}</title>
|
|
19
|
+
{description && <meta name="description" content={description} />}
|
|
20
|
+
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
|
21
|
+
</head>
|
|
22
|
+
<body>
|
|
23
|
+
{/* Paints during a soft navigation, so a slow page still feels answered. */}
|
|
24
|
+
<NavigationProgress />
|
|
25
|
+
<header>
|
|
26
|
+
<nav>
|
|
27
|
+
<a href="/">
|
|
28
|
+
<strong>{publicEnv.appName}</strong>
|
|
29
|
+
</a>
|
|
30
|
+
{/* `data-prefetch` warms a page on hover; `data-native` opts a link out of soft navigation. */}
|
|
31
|
+
<a href="/api/health" data-native>
|
|
32
|
+
/api/health
|
|
33
|
+
</a>
|
|
34
|
+
</nav>
|
|
35
|
+
</header>
|
|
36
|
+
<main>{children}</main>
|
|
37
|
+
<footer>
|
|
38
|
+
<p>
|
|
39
|
+
Built with <a href="https://github.com/rshono/rshono">rshono</a> — Hono + Rspack + React Server Components.
|
|
40
|
+
</p>
|
|
41
|
+
</footer>
|
|
42
|
+
</body>
|
|
43
|
+
</html>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment access in one place, on the right side of the client/server line.
|
|
3
|
+
*
|
|
4
|
+
* The boundary is the RSC directives, not filenames. In a `'use client'` module — and in its SSR pass —
|
|
5
|
+
* `process.env` is replaced at build time with a literal holding `NODE_ENV` and the `PUBLIC_`-prefixed
|
|
6
|
+
* variables only, so a secret read there compiles to `undefined` and cannot ship. Server components and
|
|
7
|
+
* `'use server'` actions read the real environment.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Safe anywhere: `PUBLIC_` variables are the ones compiled into the browser bundle. */
|
|
11
|
+
export const publicEnv = {
|
|
12
|
+
appName: process.env.PUBLIC_APP_NAME ?? '__PROJECT_NAME__',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Server-only. Throws rather than handing back `undefined`, so a missing secret fails at the point of
|
|
17
|
+
* use with a name in the message instead of turning into a confusing error further down.
|
|
18
|
+
*
|
|
19
|
+
* Calling this from a client component would always throw — that view of `process.env` holds nothing but
|
|
20
|
+
* `NODE_ENV` and the `PUBLIC_` set. Read secrets in server code and pass derived values down as props.
|
|
21
|
+
*/
|
|
22
|
+
export function requireEnv(name: string): string {
|
|
23
|
+
const value = process.env[name];
|
|
24
|
+
if (!value) throw new Error(`Missing environment variable ${name} — add it to .env.local`);
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { defineRoutes } from '@rshono/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The one file rshono requires. It only ever runs on the server, so importing server-only modules from
|
|
5
|
+
* it — inside `staticPaths`, say — is safe.
|
|
6
|
+
*
|
|
7
|
+
* Write each page as the inline `component: () => import('…')` thunk you see below: the build detects
|
|
8
|
+
* that exact form and attaches the page's own JS and CSS to it, which is what makes assets code-split
|
|
9
|
+
* per route.
|
|
10
|
+
*/
|
|
11
|
+
export const routes = defineRoutes({
|
|
12
|
+
routes: [
|
|
13
|
+
{ path: '/', component: () => import('./components/home') },
|
|
14
|
+
|
|
15
|
+
// A page with params. `PageProps<'/posts/:slug'>` types `params.slug` for the component.
|
|
16
|
+
// { path: '/posts/:slug', component: () => import('./components/post') },
|
|
17
|
+
|
|
18
|
+
// Prerendered at build time, one file per param set, served from disk.
|
|
19
|
+
// {
|
|
20
|
+
// path: '/docs/:slug',
|
|
21
|
+
// render: 'static',
|
|
22
|
+
// component: () => import('./components/doc'),
|
|
23
|
+
// staticPaths: async () => [{ slug: 'getting-started' }],
|
|
24
|
+
// },
|
|
25
|
+
|
|
26
|
+
// A raw Hono handler instead of a page — for anything that isn't HTML. The other place for these
|
|
27
|
+
// is src/server.ts, which is the better home for a group of them.
|
|
28
|
+
// { type: 'endpoint', path: '/api/posts', server: () => import('./api/posts') },
|
|
29
|
+
],
|
|
30
|
+
notFound: { component: () => import('./components/404') },
|
|
31
|
+
error: { component: () => import('./components/500') },
|
|
32
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { onServerError } from '@rshono/core/server';
|
|
2
|
+
import { Hono } from 'hono';
|
|
3
|
+
import { trimTrailingSlash } from 'hono/trailing-slash';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* What this app's middleware puts on the Hono context. Pass it to `PageProps<path, AppEnv>` (or
|
|
7
|
+
* `getContext<AppEnv>()`) and `ctx.var` is typed key by key instead of an open record — see
|
|
8
|
+
* `components/home.tsx`.
|
|
9
|
+
*/
|
|
10
|
+
export type AppEnv = { Variables: { requestId: string } };
|
|
11
|
+
|
|
12
|
+
const server = new Hono<AppEnv>();
|
|
13
|
+
const startedAt = Date.now();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Where an error tracker goes. Registered at module load — this file is imported as the server starts —
|
|
17
|
+
* so every error the framework catches reaches one place: a thrown action, a failed render, SSR falling
|
|
18
|
+
* over. Swap the log for `Sentry.captureException(error)` or whatever you use.
|
|
19
|
+
*/
|
|
20
|
+
onServerError((error, { source, request }) => {
|
|
21
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
22
|
+
console.error(`[error] ${source} ${new URL(request.url).pathname}: ${message}`);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
/** `/about/` and `/about` should not be two pages. */
|
|
26
|
+
server.use(trimTrailingSlash({ alwaysRedirect: true }));
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* This sub-app is mounted at `/` *ahead of* the page routes, so middleware registered here wraps page
|
|
30
|
+
* requests too — auth, logging, headers. The flip side: a terminal handler at the same path as a page
|
|
31
|
+
* route shadows the page.
|
|
32
|
+
*/
|
|
33
|
+
server.use('*', async (c, next) => {
|
|
34
|
+
c.set('requestId', crypto.randomUUID());
|
|
35
|
+
const start = performance.now();
|
|
36
|
+
await next();
|
|
37
|
+
c.res.headers.set('X-Response-Time', `${(performance.now() - start).toFixed(1)} ms`);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/** Old paths that should keep working. One place to add to, rather than a handler each. */
|
|
41
|
+
const REDIRECTS: Record<string, string> = {
|
|
42
|
+
'/home': '/',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
for (const [from, to] of Object.entries(REDIRECTS)) {
|
|
46
|
+
server.get(from, (c) => c.redirect(to, 301));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
server.get('/api/health', (c) => {
|
|
50
|
+
return c.json({ status: 'ok', uptime: (Date.now() - startedAt) / 1000, requestId: c.var.requestId });
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export default server;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* End-to-end types for a client that calls this app: `hc<AppType>('/')` from `hono/client` gives typed
|
|
57
|
+
* paths, params and responses, checked against the handlers above.
|
|
58
|
+
*/
|
|
59
|
+
export type AppType = typeof server;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Deliberately element-level: nothing here names a class, so the markup in src/components stays plain
|
|
3
|
+
* HTML you can restyle or replace wholesale without hunting for selectors.
|
|
4
|
+
*
|
|
5
|
+
* Rspack compiles this natively — there is no PostCSS anywhere in the framework. If you want a plugin
|
|
6
|
+
* chain, add the loader in `rshono.config.ts`'s `rspack` hook; that, plus the packages, is all the
|
|
7
|
+
* Tailwind setup is.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
*,
|
|
11
|
+
*::before,
|
|
12
|
+
*::after {
|
|
13
|
+
box-sizing: border-box;
|
|
14
|
+
margin: 0;
|
|
15
|
+
padding: 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
:root {
|
|
19
|
+
--bg: #fcfcfd;
|
|
20
|
+
--surface: #fff;
|
|
21
|
+
--text: #18181b;
|
|
22
|
+
--muted: #71717a;
|
|
23
|
+
--border: #e4e4e7;
|
|
24
|
+
--accent: #2563eb;
|
|
25
|
+
--radius: 8px;
|
|
26
|
+
color-scheme: light dark;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@media (prefers-color-scheme: dark) {
|
|
30
|
+
:root {
|
|
31
|
+
--bg: #09090b;
|
|
32
|
+
--surface: #131316;
|
|
33
|
+
--text: #f4f4f5;
|
|
34
|
+
--muted: #a1a1aa;
|
|
35
|
+
--border: #27272a;
|
|
36
|
+
--accent: #60a5fa;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
body {
|
|
41
|
+
font-family:
|
|
42
|
+
ui-sans-serif,
|
|
43
|
+
system-ui,
|
|
44
|
+
-apple-system,
|
|
45
|
+
'Segoe UI',
|
|
46
|
+
sans-serif;
|
|
47
|
+
line-height: 1.6;
|
|
48
|
+
color: var(--text);
|
|
49
|
+
background: var(--bg);
|
|
50
|
+
-webkit-font-smoothing: antialiased;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
header,
|
|
54
|
+
main,
|
|
55
|
+
footer {
|
|
56
|
+
max-width: 44rem;
|
|
57
|
+
margin-inline: auto;
|
|
58
|
+
padding-inline: 1.5rem;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
header {
|
|
62
|
+
padding-block: 1.25rem;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
nav {
|
|
66
|
+
display: flex;
|
|
67
|
+
align-items: center;
|
|
68
|
+
justify-content: space-between;
|
|
69
|
+
gap: 1rem;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
main {
|
|
73
|
+
padding-block: 1rem 4rem;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
footer {
|
|
77
|
+
padding-block: 2rem;
|
|
78
|
+
border-top: 1px solid var(--border);
|
|
79
|
+
color: var(--muted);
|
|
80
|
+
font-size: 0.875rem;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
h1 {
|
|
84
|
+
font-size: 2.25rem;
|
|
85
|
+
letter-spacing: -0.02em;
|
|
86
|
+
margin-block-end: 0.5rem;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
h2 {
|
|
90
|
+
font-size: 1.125rem;
|
|
91
|
+
margin-block: 2.5rem 0.5rem;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
p,
|
|
95
|
+
ul {
|
|
96
|
+
margin-block-end: 1rem;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
ul {
|
|
100
|
+
padding-inline-start: 1.25rem;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
li {
|
|
104
|
+
margin-block-end: 0.25rem;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
a {
|
|
108
|
+
color: var(--accent);
|
|
109
|
+
text-decoration-thickness: 1px;
|
|
110
|
+
text-underline-offset: 2px;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
code {
|
|
114
|
+
font-family: ui-monospace, 'SF Mono', Menlo, monospace;
|
|
115
|
+
font-size: 0.875em;
|
|
116
|
+
padding: 0.1em 0.35em;
|
|
117
|
+
border: 1px solid var(--border);
|
|
118
|
+
border-radius: 4px;
|
|
119
|
+
background: var(--surface);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
pre {
|
|
123
|
+
overflow-x: auto;
|
|
124
|
+
padding: 1rem;
|
|
125
|
+
border: 1px solid var(--border);
|
|
126
|
+
border-radius: var(--radius);
|
|
127
|
+
background: var(--surface);
|
|
128
|
+
font-size: 0.8125rem;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
form {
|
|
132
|
+
display: flex;
|
|
133
|
+
flex-wrap: wrap;
|
|
134
|
+
align-items: center;
|
|
135
|
+
gap: 0.5rem;
|
|
136
|
+
padding: 1rem;
|
|
137
|
+
border: 1px solid var(--border);
|
|
138
|
+
border-radius: var(--radius);
|
|
139
|
+
background: var(--surface);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
label {
|
|
143
|
+
flex-basis: 100%;
|
|
144
|
+
font-size: 0.875rem;
|
|
145
|
+
color: var(--muted);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
input {
|
|
149
|
+
flex: 1 1 12rem;
|
|
150
|
+
padding: 0.5rem 0.75rem;
|
|
151
|
+
border: 1px solid var(--border);
|
|
152
|
+
border-radius: 6px;
|
|
153
|
+
background: var(--bg);
|
|
154
|
+
color: inherit;
|
|
155
|
+
font: inherit;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
button {
|
|
159
|
+
padding: 0.5rem 1rem;
|
|
160
|
+
border: 0;
|
|
161
|
+
border-radius: 6px;
|
|
162
|
+
background: var(--accent);
|
|
163
|
+
color: #fff;
|
|
164
|
+
font: inherit;
|
|
165
|
+
font-weight: 500;
|
|
166
|
+
cursor: pointer;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
button:disabled {
|
|
170
|
+
opacity: 0.6;
|
|
171
|
+
cursor: progress;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
output {
|
|
175
|
+
flex-basis: 100%;
|
|
176
|
+
font-size: 0.9375rem;
|
|
177
|
+
color: var(--muted);
|
|
178
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"jsx": "react-jsx",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"types": ["node", "react"],
|
|
14
|
+
// `@/…` resolves to `src/…` in both compilers; this is what makes the editor agree. Relative to this
|
|
15
|
+
// file, since TypeScript 7 removed `baseUrl`.
|
|
16
|
+
"paths": { "@/*": ["./src/*"] }
|
|
17
|
+
},
|
|
18
|
+
"include": ["src", "rshono.config.ts"],
|
|
19
|
+
"exclude": ["node_modules", "dist"]
|
|
20
|
+
}
|