@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
|
@@ -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 className="mb-2 text-4xl font-semibold tracking-tight">{
|
|
7
|
+
<h1 className="mb-2 text-4xl font-semibold tracking-tight">{appName}</h1>
|
|
17
8
|
<p className="mb-4">
|
|
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 className="mt-10 mb-2 text-lg font-semibold">Server actions</h2>
|
|
22
|
-
<p className="mb-4">
|
|
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 className="mt-10 mb-2 text-lg font-semibold">Where things are</h2>
|
|
28
13
|
<ul className="mb-4 list-disc space-y-1 pl-5">
|
|
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 className="text-sm text-zinc-500 dark:text-zinc-400">
|
|
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 className="bg-zinc-50 text-zinc-900 antialiased dark:bg-zinc-950 dark:text-zinc-100">
|
|
23
|
-
{/* Paints during a soft navigation, so a slow page still feels answered. */}
|
|
24
|
-
<NavigationProgress />
|
|
25
19
|
<header className="mx-auto max-w-2xl px-6 py-5">
|
|
26
20
|
<nav className="flex items-center justify-between gap-4">
|
|
27
21
|
<a href="/" className="font-semibold no-underline">
|
|
28
|
-
{
|
|
22
|
+
{appName}
|
|
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 className="text-sm">
|
|
32
25
|
/api/health
|
|
33
26
|
</a>
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
@import 'tailwindcss';
|
|
2
2
|
|
|
3
|
-
/*
|
|
4
|
-
* Element defaults for the parts of the app whose markup carries no classes — the form in
|
|
5
|
-
* `components/greet-form.tsx`, and code and links wherever they appear. Everything else is styled with
|
|
6
|
-
* utilities in the markup; see `components/layout.tsx` and `components/home.tsx`.
|
|
7
|
-
*/
|
|
8
3
|
@layer base {
|
|
9
4
|
a {
|
|
10
5
|
@apply text-blue-600 underline decoration-1 underline-offset-2 dark:text-blue-400;
|
|
@@ -17,24 +12,4 @@
|
|
|
17
12
|
pre {
|
|
18
13
|
@apply overflow-x-auto rounded-lg border border-zinc-200 bg-white p-4 font-mono text-xs dark:border-zinc-800 dark:bg-zinc-900;
|
|
19
14
|
}
|
|
20
|
-
|
|
21
|
-
form {
|
|
22
|
-
@apply flex flex-wrap items-center gap-2 rounded-lg border border-zinc-200 bg-white p-4 dark:border-zinc-800 dark:bg-zinc-900;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
label {
|
|
26
|
-
@apply basis-full text-sm text-zinc-500 dark:text-zinc-400;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
input {
|
|
30
|
-
@apply flex-1 basis-48 rounded-md border border-zinc-200 px-3 py-2 dark:border-zinc-800 dark:bg-zinc-950;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
button {
|
|
34
|
-
@apply cursor-pointer rounded-md bg-blue-600 px-4 py-2 font-medium text-white disabled:cursor-progress disabled:opacity-60;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
output {
|
|
38
|
-
@apply basis-full text-zinc-500 dark:text-zinc-400;
|
|
39
|
-
}
|
|
40
15
|
}
|
|
@@ -1,22 +0,0 @@
|
|
|
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
|
-
// A form field is a string or a File, never only a string — a file input posted under this name would
|
|
13
|
-
// stringify to "[object File]" rather than fail. Narrowing is the validation the endpoint owes itself.
|
|
14
|
-
const field = formData.get('name');
|
|
15
|
-
const name = typeof field === 'string' ? field.trim() : '';
|
|
16
|
-
if (!name) return 'Type a name first.';
|
|
17
|
-
|
|
18
|
-
// Where real work goes — a database write, an email, a queue push.
|
|
19
|
-
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
20
|
-
|
|
21
|
-
return `Hello, ${name}. This ran on the server.`;
|
|
22
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
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
|
-
}
|