@rshono/create 1.0.0-rc.2 → 1.0.0-rc.20
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/LICENSE +21 -0
- package/README.md +94 -61
- package/bin/create-rshono.mjs +9 -7
- package/dist/api.mjs +297 -254
- package/dist/cli.mjs +343 -304
- package/package.json +12 -13
- package/templates/base/AGENTS.md +5 -0
- package/templates/base/CLAUDE.md +1 -0
- package/templates/base/README.md +11 -9
- package/templates/base/_env +1 -4
- package/templates/base/_gitignore +1 -0
- package/templates/base/public/favicon.svg +2 -1
- package/templates/base/rshono.config.ts +0 -30
- package/templates/base/src/components/404.tsx +0 -1
- package/templates/base/src/components/500.tsx +0 -6
- package/templates/base/src/components/home.tsx +6 -23
- package/templates/base/src/components/layout.tsx +4 -11
- package/templates/base/src/routes.ts +1 -26
- package/templates/base/src/server.ts +20 -35
- package/templates/base/src/styles.css +1 -64
- package/templates/biome/biome.json +2 -1
- package/templates/biome-tailwind/biome.json +1 -1
- package/templates/eslint/eslint.config.mjs +16 -16
- package/templates/oxfmt/_oxfmtrc.json +1 -1
- package/templates/tailwind/postcss.config.mjs +0 -4
- package/templates/tailwind/rshono.config.ts +1 -37
- package/templates/tailwind/src/components/home.tsx +6 -23
- package/templates/tailwind/src/components/layout.tsx +4 -11
- package/templates/tailwind/src/styles.css +0 -25
- package/templates/base/src/actions.ts +0 -22
- package/templates/base/src/components/greet-form.tsx +0 -27
- package/templates/base/src/lib/env.ts +0 -26
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rshono/create",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.20",
|
|
4
4
|
"description": "Scaffold a new rshono app — Hono + Rspack + React Server Components",
|
|
5
5
|
"author": "Lasse <lasse@lassetange.com> (https://www.lassetange.com)",
|
|
6
|
-
"license": "
|
|
6
|
+
"license": "MIT",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"create",
|
|
9
9
|
"scaffold",
|
|
@@ -36,24 +36,23 @@
|
|
|
36
36
|
"templates"
|
|
37
37
|
],
|
|
38
38
|
"engines": {
|
|
39
|
-
"node": ">=22.
|
|
39
|
+
"node": ">=22.18.0"
|
|
40
40
|
},
|
|
41
41
|
"publishConfig": {
|
|
42
42
|
"access": "public"
|
|
43
43
|
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@clack/prompts": "^1.7.0",
|
|
46
|
+
"@rspack/core": "2.2.2",
|
|
47
|
+
"@types/node": "^26.4.0",
|
|
48
|
+
"typescript": "^7.0.2",
|
|
49
|
+
"@rshono/core": "1.0.0-rc.20"
|
|
50
|
+
},
|
|
44
51
|
"scripts": {
|
|
45
52
|
"build": "node scripts/codegen.mjs && node scripts/build.mjs",
|
|
46
53
|
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
47
54
|
"codegen": "node scripts/codegen.mjs",
|
|
48
55
|
"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"
|
|
56
|
+
"test": "npm run build && node --test --test-concurrency=1 \"test/*.test.mjs\""
|
|
58
57
|
}
|
|
59
|
-
}
|
|
58
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
package/templates/base/README.md
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
# {{PROJECT_NAME}}
|
|
2
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
3
|
```bash
|
|
6
|
-
{{
|
|
7
|
-
{{PM_RUN}} build # production build for {{DEPLOY_TARGET}}
|
|
8
|
-
{{PM_RUN}} typecheck # tsc --noEmit
|
|
4
|
+
{{SCRIPT_TABLE}}
|
|
9
5
|
```
|
|
10
6
|
|
|
11
7
|
`package.json` has the rest, including whatever your formatter and linter added.
|
|
@@ -18,9 +14,7 @@ public/ served verbatim at the web root (favicon.svg → /favicon.svg
|
|
|
18
14
|
src/
|
|
19
15
|
routes.ts the route table — the one file rshono requires
|
|
20
16
|
server.ts a Hono app: middleware, API routes, redirects, error reporting
|
|
21
|
-
actions.ts 'use server' functions the browser can call
|
|
22
17
|
components/ pages and components
|
|
23
|
-
lib/ everything else
|
|
24
18
|
styles.css imported by the layout, so it loads with the page
|
|
25
19
|
```
|
|
26
20
|
|
|
@@ -31,12 +25,20 @@ Interactive parts are `'use client'` components a page imports — only those sh
|
|
|
31
25
|
|
|
32
26
|
`.env` holds committed defaults; `.env.local` overrides it and is gitignored. Only `PUBLIC_`-prefixed
|
|
33
27
|
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/
|
|
28
|
+
compiles to `undefined` rather than shipping. `src/components/layout.tsx` reads `PUBLIC_APP_NAME` that way.
|
|
29
|
+
|
|
30
|
+
Both are read **once, at startup**, and the `PUBLIC_` view of them is compiled into the bundle — so editing
|
|
31
|
+
either while `pnpm dev` is running changes nothing until you restart it. The dev server says so when it
|
|
32
|
+
happens; the same is true of `rshono.config.ts`.
|
|
35
33
|
|
|
36
34
|
## Deploying
|
|
37
35
|
|
|
38
|
-
This app is built for `{{DEPLOY_TARGET}}
|
|
36
|
+
This app is built for `{{DEPLOY_TARGET}}`. {{DEPLOY_STEP}}
|
|
37
|
+
|
|
38
|
+
{{PLATFORM_SETUP}}
|
|
39
39
|
|
|
40
40
|
Change `deploy` in `rshono.config.ts` to target somewhere else, or build for one place without editing the
|
|
41
41
|
file: `rshono build --deploy vercel`, or `RSHONO_DEPLOY=vercel` in CI. `dev` always runs the Node dev
|
|
42
42
|
server, whatever the target — it is a property of the build, not of developing.
|
|
43
|
+
|
|
44
|
+
Every deployment target, and what each one needs: <https://www.rshono.com/docs/deployment>
|
package/templates/base/_env
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
#
|
|
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.
|
|
1
|
+
# Every variable here is on `process.env` — the layout reads `process.env.PUBLIC_APP_NAME`.
|
|
5
2
|
#
|
|
6
3
|
# Only `PUBLIC_`-prefixed variables are compiled into the browser bundle. Anything else is server-only:
|
|
7
4
|
# a stray read of it in client code compiles to `undefined` rather than shipping.
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
<!-- The rshono mark, as a placeholder — replace it with your own and the tab is yours. -->
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"><title>rshono</title><path fill="#087EA4" d="M12.4365 0.2520325c0.062375 -0.0080215 0.11745 0.0077075 0.16515 0.0471875 1.755075 2.142025 3.40665 4.359855 4.954725 6.65348 1.14615 1.725625 2.121325 3.550175 2.92565 5.4738 1.2845 3.426025 0.616 6.40675 -2.005475 8.9421 -2.293725 1.938575 -4.936225 2.693575 -7.927575 2.265025 -3.57555 -0.729025 -6.00575 -2.7974 -7.290525 -6.205225 -0.33465 -1.109425 -0.44475 -2.241925 -0.3303 -3.397525 0.19055 -1.9891 0.662425 -3.9081 1.415625 -5.7569 0.31385 -0.75435 0.722825 -1.4464 1.2269 -2.076275 0.411225 0.4898 0.80445 0.993175 1.179675 1.510025 0.17375 0.181625 0.354625 0.35465 0.542675 0.51905C8.728325 5.378325 10.44285 2.7201 12.4365 0.2520325Z"/><path fill="#61DAFB" d="M12.10625 4.07425c1.73145 2.008325 3.296525 4.1475 4.695175 6.41755 0.438525 0.75115 0.800275 1.537625 1.085325 2.3594 0.593825 2.336175 -0.043225 4.26305 -1.9111 5.7805 -1.80655 1.2712 -3.788425 1.6487 -5.945675 1.132525 -2.326325 -0.721875 -3.671175 -2.286975 -4.034575 -4.6952 -0.088175 -0.7593 -0.009525 -1.4986 0.23595 -2.217825 0.35005 -0.888875 0.774725 -1.73825 1.274075 -2.54815 0.471875 -0.6921 0.94375 -1.38415 1.415625 -2.07625 1.071925 -1.378375 2.13365 -2.762525 3.1852 -4.15255Z"/></svg>
|
|
@@ -1,35 +1,5 @@
|
|
|
1
1
|
import { defineConfig } from '@rshono/core';
|
|
2
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
3
|
export default defineConfig({
|
|
12
|
-
/** Where `build` targets. Overridable per build with `--deploy` or `RSHONO_DEPLOY`. */
|
|
13
4
|
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
5
|
});
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { PageProps } from '@rshono/core';
|
|
2
2
|
import { Layout } from './layout';
|
|
3
3
|
|
|
4
|
-
/** Declared as `notFound` in routes.ts. Answers unmatched paths, and any `notFound()` call, with a 404. */
|
|
5
4
|
export default function NotFound({ url }: PageProps) {
|
|
6
5
|
return (
|
|
7
6
|
<Layout title="Not found">
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import type { ErrorPageProps } from '@rshono/core';
|
|
2
2
|
import { Layout } from './layout';
|
|
3
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
4
|
export default function ServerError({ error }: ErrorPageProps) {
|
|
11
5
|
return (
|
|
12
6
|
<Layout title="Something went wrong">
|
|
@@ -1,29 +1,14 @@
|
|
|
1
1
|
import type { PageProps } from '@rshono/core';
|
|
2
|
-
import {
|
|
3
|
-
import type { AppEnv } from '../server';
|
|
4
|
-
import { GreetForm } from './greet-form';
|
|
5
|
-
import { Layout } from './layout';
|
|
2
|
+
import { appName, Layout } from './layout';
|
|
6
3
|
|
|
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>) {
|
|
4
|
+
export default function Home({ url }: PageProps<'/'>) {
|
|
14
5
|
return (
|
|
15
6
|
<Layout description="A new rshono app.">
|
|
16
|
-
<h1>{
|
|
7
|
+
<h1>{appName}</h1>
|
|
17
8
|
<p>
|
|
18
|
-
Edit <code>src/components/home.tsx</code> and save
|
|
9
|
+
Edit <code>src/components/home.tsx</code> and save — the page re-renders in place.
|
|
19
10
|
</p>
|
|
20
11
|
|
|
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
12
|
<h2>Where things are</h2>
|
|
28
13
|
<ul>
|
|
29
14
|
<li>
|
|
@@ -33,7 +18,7 @@ export default function Home({ url, ctx }: PageProps<'/', AppEnv>) {
|
|
|
33
18
|
<code>src/server.ts</code> — a Hono app for middleware and API routes, mounted ahead of the pages
|
|
34
19
|
</li>
|
|
35
20
|
<li>
|
|
36
|
-
<code>src/components/</code> — pages and components
|
|
21
|
+
<code>src/components/</code> — pages and components
|
|
37
22
|
</li>
|
|
38
23
|
<li>
|
|
39
24
|
<code>rshono.config.ts</code> — deploy target, security and build settings
|
|
@@ -41,9 +26,7 @@ export default function Home({ url, ctx }: PageProps<'/', AppEnv>) {
|
|
|
41
26
|
</ul>
|
|
42
27
|
|
|
43
28
|
<p>
|
|
44
|
-
|
|
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>.
|
|
29
|
+
Rendered on the server for <code>{url.pathname}</code>.
|
|
47
30
|
</p>
|
|
48
31
|
</Layout>
|
|
49
32
|
);
|
|
@@ -1,14 +1,10 @@
|
|
|
1
|
-
import { NavigationProgress } from '@rshono/core/client';
|
|
2
1
|
import type { ReactNode } from 'react';
|
|
3
|
-
import { publicEnv } from '../lib/env';
|
|
4
2
|
import '../styles.css';
|
|
5
3
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* in. Importing the stylesheet here is what attaches it to each of those pages.
|
|
9
|
-
*/
|
|
4
|
+
export const appName = process.env.PUBLIC_APP_NAME ?? '{{PROJECT_NAME}}';
|
|
5
|
+
|
|
10
6
|
export function Layout({ title, description, children }: { title?: string; description?: string; children: ReactNode }) {
|
|
11
|
-
const heading = title ? `${title} · ${
|
|
7
|
+
const heading = title ? `${title} · ${appName}` : appName;
|
|
12
8
|
|
|
13
9
|
return (
|
|
14
10
|
<html lang="en">
|
|
@@ -20,14 +16,11 @@ export function Layout({ title, description, children }: { title?: string; descr
|
|
|
20
16
|
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
|
21
17
|
</head>
|
|
22
18
|
<body>
|
|
23
|
-
{/* Paints during a soft navigation, so a slow page still feels answered. */}
|
|
24
|
-
<NavigationProgress />
|
|
25
19
|
<header>
|
|
26
20
|
<nav>
|
|
27
21
|
<a href="/">
|
|
28
|
-
<strong>{
|
|
22
|
+
<strong>{appName}</strong>
|
|
29
23
|
</a>
|
|
30
|
-
{/* `data-prefetch` warms a page on hover; `data-native` opts a link out of soft navigation. */}
|
|
31
24
|
<a href="/api/health" data-native>
|
|
32
25
|
/api/health
|
|
33
26
|
</a>
|
|
@@ -1,32 +1,7 @@
|
|
|
1
1
|
import { defineRoutes } from '@rshono/core';
|
|
2
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
3
|
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
|
-
],
|
|
4
|
+
routes: [{ path: '/', component: () => import('./components/home') }],
|
|
30
5
|
notFound: { component: () => import('./components/404') },
|
|
31
6
|
error: { component: () => import('./components/500') },
|
|
32
7
|
});
|
|
@@ -1,59 +1,44 @@
|
|
|
1
|
-
import { onServerError } from '@rshono/core/server';
|
|
1
|
+
import { onServerError, publicUrl } from '@rshono/core/server';
|
|
2
2
|
import { Hono } from 'hono';
|
|
3
|
+
import { bodyLimit } from 'hono/body-limit';
|
|
4
|
+
import { csrf } from 'hono/csrf';
|
|
3
5
|
import { trimTrailingSlash } from 'hono/trailing-slash';
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* `components/home.tsx`.
|
|
8
|
+
* Mounted at `/` ahead of the page routes, so middleware registered here wraps page requests too — auth,
|
|
9
|
+
* logging, headers. The flip side: a terminal handler at a page's path shadows the page.
|
|
9
10
|
*/
|
|
10
|
-
|
|
11
|
+
const server = new Hono();
|
|
11
12
|
|
|
12
|
-
|
|
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
|
-
*/
|
|
13
|
+
/** Every error the framework catches lands here: a thrown action, a failed render, SSR falling over. */
|
|
20
14
|
onServerError((error, { source, request }) => {
|
|
21
15
|
const message = error instanceof Error ? error.message : String(error);
|
|
22
16
|
console.error(`[error] ${source} ${new URL(request.url).pathname}: ${message}`);
|
|
23
17
|
});
|
|
24
18
|
|
|
19
|
+
/** Caps every request body before anything downstream buffers it. Raise it where you accept uploads. */
|
|
20
|
+
server.use(bodyLimit({ maxSize: 1024 * 1024 }));
|
|
21
|
+
|
|
22
|
+
/** Cross-origin hosts allowed to post server actions, alongside this app's own. */
|
|
23
|
+
const ALLOWED_ORIGINS: string[] = [];
|
|
24
|
+
|
|
25
|
+
/** Rejects a cross-origin POST before it reaches a server action. `publicUrl(c)`, not `c.req.url`: behind a proxy those differ. */
|
|
26
|
+
server.use(csrf({ origin: (origin, c) => origin === publicUrl(c).origin || ALLOWED_ORIGINS.includes(origin) }));
|
|
27
|
+
|
|
25
28
|
/** `/about/` and `/about` should not be two pages. */
|
|
26
29
|
server.use(trimTrailingSlash({ alwaysRedirect: true }));
|
|
27
30
|
|
|
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
31
|
/** 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
|
-
};
|
|
32
|
+
const REDIRECTS: Record<string, string> = { '/home': '/' };
|
|
44
33
|
|
|
45
34
|
for (const [from, to] of Object.entries(REDIRECTS)) {
|
|
46
35
|
server.get(from, (c) => c.redirect(to, 301));
|
|
47
36
|
}
|
|
48
37
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
});
|
|
38
|
+
/** A JSON API route, with no page involved. The layout links to it. */
|
|
39
|
+
server.get('/api/health', (c) => c.json({ status: 'ok' }));
|
|
52
40
|
|
|
53
41
|
export default server;
|
|
54
42
|
|
|
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
|
-
*/
|
|
43
|
+
/** `hc<AppType>('/')` from `hono/client` gives paths, params and responses typed against the handlers above. */
|
|
59
44
|
export type AppType = typeof server;
|
|
@@ -1,12 +1,3 @@
|
|
|
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
1
|
*,
|
|
11
2
|
*::before,
|
|
12
3
|
*::after {
|
|
@@ -38,12 +29,7 @@
|
|
|
38
29
|
}
|
|
39
30
|
|
|
40
31
|
body {
|
|
41
|
-
font-family:
|
|
42
|
-
ui-sans-serif,
|
|
43
|
-
system-ui,
|
|
44
|
-
-apple-system,
|
|
45
|
-
'Segoe UI',
|
|
46
|
-
sans-serif;
|
|
32
|
+
font-family: ui-sans-serif, system-ui, 'Segoe UI', sans-serif;
|
|
47
33
|
line-height: 1.6;
|
|
48
34
|
color: var(--text);
|
|
49
35
|
background: var(--bg);
|
|
@@ -127,52 +113,3 @@ pre {
|
|
|
127
113
|
background: var(--surface);
|
|
128
114
|
font-size: 0.8125rem;
|
|
129
115
|
}
|
|
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
|
-
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://biomejs.dev/schemas/2.5.6/schema.json",
|
|
3
|
-
"files": { "includes": ["**", "!dist", "!.wrangler", "!.vercel", "!.netlify", "!wrangler.jsonc"] },
|
|
3
|
+
"files": { "includes": ["**", "!dist", "!.rshono", "!.wrangler", "!.vercel", "!.netlify", "!wrangler.jsonc"] },
|
|
4
4
|
"formatter": { "indentStyle": "space", "indentWidth": 2, "lineWidth": 140 },
|
|
5
5
|
"javascript": { "formatter": { "quoteStyle": "single", "jsxQuoteStyle": "double" } },
|
|
6
|
+
"css": { "formatter": { "quoteStyle": "single" } },
|
|
6
7
|
"linter": { "enabled": true, "rules": { "preset": "recommended" } }
|
|
7
8
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://biomejs.dev/schemas/2.5.6/schema.json",
|
|
3
|
-
"files": { "includes": ["**", "!dist", "!.wrangler", "!.vercel", "!.netlify", "!wrangler.jsonc", "!**/*.css"] },
|
|
3
|
+
"files": { "includes": ["**", "!dist", "!.rshono", "!.wrangler", "!.vercel", "!.netlify", "!wrangler.jsonc", "!**/*.css"] },
|
|
4
4
|
"formatter": { "indentStyle": "space", "indentWidth": 2, "lineWidth": 140 },
|
|
5
5
|
"javascript": { "formatter": { "quoteStyle": "single", "jsxQuoteStyle": "double" } },
|
|
6
6
|
"linter": { "enabled": true, "rules": { "preset": "recommended" } }
|
|
@@ -1,31 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint with type-aware rules, which is the reason to reach for it over a linter that only reads syntax.
|
|
3
|
+
*
|
|
4
|
+
* **This app's `typescript` is pinned below the one rshono is built with.** typescript-eslint reads the
|
|
5
|
+
* compiler API directly and accepts nothing from TypeScript 6.1 up, so `package.json` holds the newest
|
|
6
|
+
* version it takes. Your editor and `tsc` are unaffected — rshono's declarations compile the same under
|
|
7
|
+
* either — but the rules below reason about your program through the *older* compiler.
|
|
8
|
+
*
|
|
9
|
+
* The practical consequence: `lint:fix` can rewrite code that `tsc` then rejects. Run `typecheck` after it,
|
|
10
|
+
* not instead of it. When typescript-eslint widens its peer range, drop the `typescript` pin from
|
|
11
|
+
* `package.json` and this note with it.
|
|
12
|
+
*/
|
|
1
13
|
import js from '@eslint/js';
|
|
2
14
|
import reactHooks from 'eslint-plugin-react-hooks';
|
|
3
15
|
import tseslint from 'typescript-eslint';
|
|
4
16
|
|
|
5
|
-
/**
|
|
6
|
-
* Flat config, with type-aware rules — the reason to run ESLint over a syntax-only linter. `projectService`
|
|
7
|
-
* builds the same program `tsc` does from tsconfig.json, so a rule can ask what a value actually *is*:
|
|
8
|
-
* an unawaited promise, a `catch` that swallows an error, a `String()` around something that is not one.
|
|
9
|
-
*
|
|
10
|
-
* The cost is that ESLint needs TypeScript to answer, which is why this app pins TypeScript 6 —
|
|
11
|
-
* typescript-eslint reads the compiler API directly and accepts nothing newer. `npm run typecheck` is
|
|
12
|
-
* still the thing that decides whether the app compiles; these rules only see what it sees.
|
|
13
|
-
*/
|
|
14
17
|
export default tseslint.config(
|
|
15
|
-
//
|
|
16
|
-
// the deploy target
|
|
17
|
-
|
|
18
|
+
// Build output, all of it generated: `dist/` from `rshono build`, `.rshono/` from `rshono dev`, and
|
|
19
|
+
// whatever the deploy target assembles beside them. Without this, linting after a dev run reports on
|
|
20
|
+
// bundles rather than on anything you wrote.
|
|
21
|
+
{ ignores: ['dist/**', '.rshono/**', '.wrangler/**', '.vercel/**', '.netlify/**'] },
|
|
18
22
|
js.configs.recommended,
|
|
19
23
|
tseslint.configs.recommendedTypeChecked,
|
|
20
|
-
// The rules of hooks: the one class of React mistake no type checker catches, and the reason a React
|
|
21
|
-
// app wants a linter at all.
|
|
22
24
|
reactHooks.configs.flat['recommended-latest'],
|
|
23
25
|
{
|
|
24
26
|
languageOptions: {
|
|
25
27
|
parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname },
|
|
26
28
|
},
|
|
27
29
|
},
|
|
28
|
-
// This file and any other plain JavaScript sits outside the TypeScript program, so the type-aware rules
|
|
29
|
-
// have nothing to run against and would report every file as unconfigured.
|
|
30
30
|
{ files: ['**/*.{js,mjs,cjs}'], extends: [tseslint.configs.disableTypeChecked] },
|
|
31
31
|
);
|
|
@@ -1,44 +1,8 @@
|
|
|
1
1
|
import { defineConfig } from '@rshono/core';
|
|
2
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
3
|
export default defineConfig({
|
|
12
|
-
/** Where `build` targets. Overridable per build with `--deploy` or `RSHONO_DEPLOY`. */
|
|
13
4
|
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
|
-
/**
|
|
32
|
-
* Tailwind, and the only thing the build needs to know about it.
|
|
33
|
-
*
|
|
34
|
-
* Rspack compiles CSS natively, which is fast and is all a plain stylesheet needs — but that parser
|
|
35
|
-
* reads *finished* CSS, and `@import 'tailwindcss'`, `@theme` and `@apply` are not that. Tailwind is a
|
|
36
|
-
* PostCSS plugin, so it has to run in front of the parser, which is what this rule does. The plugin
|
|
37
|
-
* list itself is in `postcss.config.mjs`, where postcss-loader looks for it.
|
|
38
|
-
*
|
|
39
|
-
* The hook is called once per compiler, so the rule reaches the client and the server graph both.
|
|
40
|
-
* Delete all of this, and the four Tailwind packages, to go back to plain CSS.
|
|
41
|
-
*/
|
|
5
|
+
/** Tailwind is a PostCSS plugin, so postcss-loader has to run ahead of Rspack's native CSS parser. */
|
|
42
6
|
rspack(config) {
|
|
43
7
|
config.module!.rules!.push({ test: /\.css$/i, use: ['postcss-loader'], type: 'css/auto' });
|
|
44
8
|
},
|