@rshono/create 1.0.0-rc.4 → 1.0.0-rc.6

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 CHANGED
@@ -50,21 +50,20 @@ presets — a target added to rshono appears here with no edit.
50
50
  ## What you get
51
51
 
52
52
  ```
53
- rshono.config.ts the chosen deploy target; everything else commented with its default
53
+ rshono.config.ts the chosen deploy target; every other setting takes its default
54
54
  tsconfig.json strict, with @/* → ./src/*
55
55
  .env committed defaults; secrets go in .env.local
56
56
  public/ favicon.svg, robots.txt
57
- src/routes.ts one page, a 404 and a 500, with the other route kinds commented
58
- src/server.ts AppEnv, request-id middleware, error reporting, /api/health, redirects, AppType
59
- src/actions.ts a 'use server' action, called from a form that works without JavaScript
60
- src/components/ layout, home, greet-form ('use client'), 404, 500
61
- src/lib/env.ts both sides of the PUBLIC_ boundary in one place
57
+ src/routes.ts one page, a 404 and a 500
58
+ src/server.ts error reporting, trailing-slash and old-path redirects, AppType
59
+ src/components/ layout, home, 404, 500
62
60
  src/styles.css element-level CSS, or the Tailwind entry
63
61
  pnpm-workspace.yaml pnpm only: which dependency install scripts this app runs (none of them)
64
62
  ```
65
63
 
66
- Then, if the dependencies were installed, the scaffold is run through its own formatter so a fresh
67
- project passes its own `format:check` rather than reporting a diff nobody made.
64
+ Scaffolding runs the install and nothing else no formatter, no linter. The templates are written to
65
+ satisfy every formatter option at its configured width, so a fresh project passes its own `format:check`
66
+ without a write pass first.
68
67
 
69
68
  `react` and `react-dom` are pinned **exactly**, at the versions the framework is tested against, and those
70
69
  pins are generated from rshono's own manifest. That is not tidiness: the RSC runtime reaches into React's
package/dist/api.mjs CHANGED
@@ -79,7 +79,7 @@ __webpack_require__.d(__webpack_exports__, {
79
79
 
80
80
  ;// CONCATENATED MODULE: ./src/generated/framework.ts
81
81
  // GENERATED by scripts/codegen.mjs from packages/core — do not edit. Run `pnpm --filter @rshono/create codegen`.
82
- /** The framework release a scaffolded app is pinned to: this package and rshono ship together. */ const RSHONO_VERSION = '1.0.0-rc.4';
82
+ /** The framework release a scaffolded app is pinned to: this package and rshono ship together. */ const RSHONO_VERSION = '1.0.0-rc.6';
83
83
  /**
84
84
  * The Node range rshono itself declares, restated in every scaffolded app's `engines` — so a CI image
85
85
  * or a contributor on an older Node hears it from their package manager rather than from a stack trace.
@@ -743,12 +743,6 @@ function packageManager(name, version) {
743
743
  */ function runInstall(pm, cwd) {
744
744
  return run(pm, pm.install, cwd);
745
745
  }
746
- /** `<pm> run <script>` — the one form every package manager accepts, Yarn v1 included. */ function runScript(pm, script, cwd) {
747
- return run(pm, [
748
- 'run',
749
- script
750
- ], cwd);
751
- }
752
746
  function run(pm, args, cwd) {
753
747
  const result = spawnSync(pm.name, args, {
754
748
  cwd,
package/dist/cli.mjs CHANGED
@@ -2943,7 +2943,7 @@ function hasGit(cwd) {
2943
2943
 
2944
2944
  ;// CONCATENATED MODULE: ./src/generated/framework.ts
2945
2945
  // GENERATED by scripts/codegen.mjs from packages/core — do not edit. Run `pnpm --filter @rshono/create codegen`.
2946
- /** The framework release a scaffolded app is pinned to: this package and rshono ship together. */ const RSHONO_VERSION = '1.0.0-rc.4';
2946
+ /** The framework release a scaffolded app is pinned to: this package and rshono ship together. */ const RSHONO_VERSION = '1.0.0-rc.6';
2947
2947
  /**
2948
2948
  * The Node range rshono itself declares, restated in every scaffolded app's `engines` — so a CI image
2949
2949
  * or a contributor on an older Node hears it from their package manager rather than from a stack trace.
@@ -3628,12 +3628,6 @@ function packageManager(name, version) {
3628
3628
  */ function runInstall(pm, cwd) {
3629
3629
  return run(pm, pm.install, cwd);
3630
3630
  }
3631
- /** `<pm> run <script>` — the one form every package manager accepts, Yarn v1 included. */ function runScript(pm, script, cwd) {
3632
- return run(pm, [
3633
- 'run',
3634
- script
3635
- ], cwd);
3636
- }
3637
3631
  function run(pm, args, cwd) {
3638
3632
  const result = spawnSync(pm.name, args, {
3639
3633
  cwd,
@@ -3846,7 +3840,7 @@ function fail(message) {
3846
3840
  async function main() {
3847
3841
  const { values, positionals } = parse();
3848
3842
  if (values.help) return console.log(HELP);
3849
- if (values.version) return console.log("1.0.0-rc.4");
3843
+ if (values.version) return console.log("1.0.0-rc.6");
3850
3844
  /*
3851
3845
  * A pipe, a CI job or an agent gets the defaults rather than a prompt nothing can answer. Both streams
3852
3846
  * have to be a terminal: the prompts draw on stdout but *read from stdin*, so `echo | npx @rshono/create`
@@ -3986,16 +3980,6 @@ async function main() {
3986
3980
  installed = runInstall(pm, targetDir);
3987
3981
  if (!installed) log.warn(`${pm.name} install failed — the files are all written, so run it yourself in the project.`);
3988
3982
  }
3989
- /*
3990
- * Format the scaffold with the tool it was scaffolded with, so a fresh project passes its own
3991
- * `format:check` instead of reporting a diff nobody made. Needs the install, since the formatter is a
3992
- * devDependency — hence the skip, rather than a failure, when there is none.
3993
- *
3994
- * Whether there is a `format` script is the plan's answer, not the formatter answer's: `--formatter
3995
- * none --linter biome` has no formatter and a `format` script all the same, because Biome brings one.
3996
- */ if (installed && plan.features.some((feature)=>feature.scripts?.format)) {
3997
- if (!runScript(pm, 'format', targetDir)) log.warn(`\`${pm.run} format\` failed — the scaffold is written, just not formatted.`);
3998
- }
3999
3983
  if (git) {
4000
3984
  if (!hasGit(targetDir)) {
4001
3985
  log.warn('git was not found on PATH — skipped.');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rshono/create",
3
- "version": "1.0.0-rc.4",
3
+ "version": "1.0.0-rc.6",
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
6
  "license": "ISC",
@@ -46,7 +46,7 @@
46
46
  "@rspack/core": "2.1.5",
47
47
  "@types/node": "^26.1.1",
48
48
  "typescript": "^7.0.2",
49
- "@rshono/core": "1.0.0-rc.4"
49
+ "@rshono/core": "1.0.0-rc.6"
50
50
  },
51
51
  "scripts": {
52
52
  "build": "node scripts/codegen.mjs && node scripts/build.mjs",
@@ -1,7 +1,5 @@
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
4
  {{PM_RUN}} dev # dev server with HMR, http://localhost:3000
7
5
  {{PM_RUN}} build # production build for {{DEPLOY_TARGET}}
@@ -18,9 +16,7 @@ public/ served verbatim at the web root (favicon.svg → /favicon.svg
18
16
  src/
19
17
  routes.ts the route table — the one file rshono requires
20
18
  server.ts a Hono app: middleware, API routes, redirects, error reporting
21
- actions.ts 'use server' functions the browser can call
22
19
  components/ pages and components
23
- lib/ everything else
24
20
  styles.css imported by the layout, so it loads with the page
25
21
  ```
26
22
 
@@ -31,7 +27,7 @@ Interactive parts are `'use client'` components a page imports — only those sh
31
27
 
32
28
  `.env` holds committed defaults; `.env.local` overrides it and is gitignored. Only `PUBLIC_`-prefixed
33
29
  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.
30
+ compiles to `undefined` rather than shipping. `src/components/layout.tsx` reads `PUBLIC_APP_NAME` that way.
35
31
 
36
32
  ## Deploying
37
33
 
@@ -1,7 +1,4 @@
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.
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,33 +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
-
29
- // Escape hatch: mutate the generated Rspack config just before it compiles. Called once per compiler.
30
- // rspack(config, { isServer, isDev }) {
31
- // return config;
32
- // },
33
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 { publicEnv } from '../lib/env';
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>{publicEnv.appName}</h1>
7
+ <h1>{appName}</h1>
17
8
  <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.
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; <code>src/lib/</code> — everything else
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
- {/* `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>.
29
+ Rendered on the server for <code>{url.pathname}</code>.
47
30
  </p>
48
31
  </Layout>
49
32
  );
@@ -1,13 +1,10 @@
1
1
  import type { ReactNode } from 'react';
2
- import { publicEnv } from '../lib/env';
3
2
  import '../styles.css';
4
3
 
5
- /**
6
- * A page renders the whole document, so the shell lives in one component every page wraps its content
7
- * in. Importing the stylesheet here is what attaches it to each of those pages.
8
- */
4
+ export const appName = process.env.PUBLIC_APP_NAME ?? '{{PROJECT_NAME}}';
5
+
9
6
  export function Layout({ title, description, children }: { title?: string; description?: string; children: ReactNode }) {
10
- const heading = title ? `${title} · ${publicEnv.appName}` : publicEnv.appName;
7
+ const heading = title ? `${title} · ${appName}` : appName;
11
8
 
12
9
  return (
13
10
  <html lang="en">
@@ -22,9 +19,8 @@ export function Layout({ title, description, children }: { title?: string; descr
22
19
  <header>
23
20
  <nav>
24
21
  <a href="/">
25
- <strong>{publicEnv.appName}</strong>
22
+ <strong>{appName}</strong>
26
23
  </a>
27
- {/* `data-native` opts a link out of soft navigation and does a full browser load. */}
28
24
  <a href="/api/health" data-native>
29
25
  /api/health
30
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
  });
@@ -3,20 +3,12 @@ import { Hono } from 'hono';
3
3
  import { trimTrailingSlash } from 'hono/trailing-slash';
4
4
 
5
5
  /**
6
- * What this app's middleware puts on the Hono context. Pass it to `PageProps<path, AppEnv>` (or
7
- * `getRequestContext<AppEnv>()`) and `ctx.var` is typed key by key instead of an open record see
8
- * `components/home.tsx`.
6
+ * Mounted at `/` ahead of the page routes, so middleware registered here wraps page requests too — auth,
7
+ * logging, headers. The flip side: a terminal handler at a page's path shadows the page.
9
8
  */
10
- export type AppEnv = { Variables: { requestId: string } };
9
+ const server = new Hono();
11
10
 
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
- */
11
+ /** Every error the framework catches lands here: a thrown action, a failed render, SSR falling over. */
20
12
  onServerError((error, { source, request }) => {
21
13
  const message = error instanceof Error ? error.message : String(error);
22
14
  console.error(`[error] ${source} ${new URL(request.url).pathname}: ${message}`);
@@ -25,35 +17,14 @@ onServerError((error, { source, request }) => {
25
17
  /** `/about/` and `/about` should not be two pages. */
26
18
  server.use(trimTrailingSlash({ alwaysRedirect: true }));
27
19
 
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
20
  /** 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
- };
21
+ const REDIRECTS: Record<string, string> = { '/home': '/' };
44
22
 
45
23
  for (const [from, to] of Object.entries(REDIRECTS)) {
46
24
  server.get(from, (c) => c.redirect(to, 301));
47
25
  }
48
26
 
49
- server.get('/api/health', (c) => {
50
- return c.json({ status: 'ok', uptime: (Date.now() - startedAt) / 1000, requestId: c.var.requestId });
51
- });
52
-
53
27
  export default server;
54
28
 
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
- */
29
+ /** `hc<AppType>('/')` from `hono/client` gives paths, params and responses typed against the handlers above. */
59
30
  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
- }
@@ -3,5 +3,6 @@
3
3
  "files": { "includes": ["**", "!dist", "!.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
  }
@@ -2,30 +2,15 @@ import js from '@eslint/js';
2
2
  import reactHooks from 'eslint-plugin-react-hooks';
3
3
  import tseslint from 'typescript-eslint';
4
4
 
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
5
  export default tseslint.config(
15
- // ESLint's own default ignores cover node_modules and nothing else, so the build output — and whatever
16
- // the deploy target leaves beside it — would otherwise be linted as if you had written it.
17
6
  { ignores: ['dist/**', '.wrangler/**', '.vercel/**', '.netlify/**'] },
18
7
  js.configs.recommended,
19
8
  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
9
  reactHooks.configs.flat['recommended-latest'],
23
10
  {
24
11
  languageOptions: {
25
12
  parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname },
26
13
  },
27
14
  },
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
15
  { files: ['**/*.{js,mjs,cjs}'], extends: [tseslint.configs.disableTypeChecked] },
31
16
  );
@@ -3,5 +3,5 @@
3
3
  "printWidth": 140,
4
4
  "singleQuote": true,
5
5
  "trailingComma": "all",
6
- "ignorePatterns": ["dist", ".wrangler", ".vercel", ".netlify", "wrangler.jsonc"]
6
+ "ignorePatterns": ["dist", ".wrangler", ".vercel", ".netlify", "wrangler.jsonc", "package.json"]
7
7
  }
@@ -1,7 +1,3 @@
1
- /**
2
- * The plugin list. postcss-loader finds this file on its own; what puts postcss-loader in front of
3
- * Rspack's CSS parser in the first place is the rule in `rshono.config.ts`.
4
- */
5
1
  export default {
6
2
  plugins: {
7
3
  '@tailwindcss/postcss': {},
@@ -1,42 +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
-
29
- /**
30
- * Tailwind, and the only thing the build needs to know about it.
31
- *
32
- * Rspack compiles CSS natively, which is fast and is all a plain stylesheet needs — but that parser
33
- * reads *finished* CSS, and `@import 'tailwindcss'`, `@theme` and `@apply` are not that. Tailwind is a
34
- * PostCSS plugin, so it has to run in front of the parser, which is what this rule does. The plugin
35
- * list itself is in `postcss.config.mjs`, where postcss-loader looks for it.
36
- *
37
- * The hook is called once per compiler, so the rule reaches the client and the server graph both.
38
- * Delete all of this, and the four Tailwind packages, to go back to plain CSS.
39
- */
5
+ /** Tailwind is a PostCSS plugin, so postcss-loader has to run ahead of Rspack's native CSS parser. */
40
6
  rspack(config) {
41
7
  config.module!.rules!.push({ test: /\.css$/i, use: ['postcss-loader'], type: 'css/auto' });
42
8
  },
@@ -1,29 +1,14 @@
1
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';
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">{publicEnv.appName}</h1>
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. The page re-renders in place — the form below keeps whatever you have typed in it.
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; <code>src/lib/</code> — everything else
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
- {/* `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>.
29
+ Rendered on the server for <code>{url.pathname}</code>.
47
30
  </p>
48
31
  </Layout>
49
32
  );
@@ -1,13 +1,10 @@
1
1
  import type { ReactNode } from 'react';
2
- import { publicEnv } from '../lib/env';
3
2
  import '../styles.css';
4
3
 
5
- /**
6
- * A page renders the whole document, so the shell lives in one component every page wraps its content
7
- * in. Importing the stylesheet here is what attaches it to each of those pages.
8
- */
4
+ export const appName = process.env.PUBLIC_APP_NAME ?? '{{PROJECT_NAME}}';
5
+
9
6
  export function Layout({ title, description, children }: { title?: string; description?: string; children: ReactNode }) {
10
- const heading = title ? `${title} · ${publicEnv.appName}` : publicEnv.appName;
7
+ const heading = title ? `${title} · ${appName}` : appName;
11
8
 
12
9
  return (
13
10
  <html lang="en">
@@ -22,9 +19,8 @@ export function Layout({ title, description, children }: { title?: string; descr
22
19
  <header className="mx-auto max-w-2xl px-6 py-5">
23
20
  <nav className="flex items-center justify-between gap-4">
24
21
  <a href="/" className="font-semibold no-underline">
25
- {publicEnv.appName}
22
+ {appName}
26
23
  </a>
27
- {/* `data-native` opts a link out of soft navigation and does a full browser load. */}
28
24
  <a href="/api/health" data-native className="text-sm">
29
25
  /api/health
30
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
- }