create-rango 0.1.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.
Files changed (96) hide show
  1. package/index.js +348 -0
  2. package/package.json +50 -0
  3. package/templates/basic/.oxfmtrc.json +9 -0
  4. package/templates/basic/.oxlintrc.json +6 -0
  5. package/templates/basic/README.md +35 -0
  6. package/templates/basic/_gitignore +4 -0
  7. package/templates/basic/package.json +44 -0
  8. package/templates/basic/server.mjs +46 -0
  9. package/templates/basic/src/actions/counter.ts +11 -0
  10. package/templates/basic/src/components/Counter.tsx +50 -0
  11. package/templates/basic/src/components/Document.tsx +78 -0
  12. package/templates/basic/src/components/Fallbacks.tsx +36 -0
  13. package/templates/basic/src/components/pages/AboutPage.tsx +21 -0
  14. package/templates/basic/src/components/pages/CounterPage.tsx +27 -0
  15. package/templates/basic/src/components/pages/GreetingPage.tsx +24 -0
  16. package/templates/basic/src/components/pages/HomePage.tsx +52 -0
  17. package/templates/basic/src/data/counter.ts +11 -0
  18. package/templates/basic/src/router.gen.ts +8 -0
  19. package/templates/basic/src/router.named-routes.gen.ts +17 -0
  20. package/templates/basic/src/router.tsx +33 -0
  21. package/templates/basic/src/styles.css +5 -0
  22. package/templates/basic/test/router.test.ts +8 -0
  23. package/templates/basic/tsconfig.json +19 -0
  24. package/templates/basic/vite.config.ts +17 -0
  25. package/templates/basic/vitest.config.ts +10 -0
  26. package/templates/basic-js/.oxfmtrc.json +9 -0
  27. package/templates/basic-js/.oxlintrc.json +6 -0
  28. package/templates/basic-js/README.md +38 -0
  29. package/templates/basic-js/_gitignore +4 -0
  30. package/templates/basic-js/package.json +40 -0
  31. package/templates/basic-js/server.mjs +46 -0
  32. package/templates/basic-js/src/actions/counter.js +11 -0
  33. package/templates/basic-js/src/components/Counter.jsx +50 -0
  34. package/templates/basic-js/src/components/Document.jsx +78 -0
  35. package/templates/basic-js/src/components/Fallbacks.jsx +36 -0
  36. package/templates/basic-js/src/components/pages/AboutPage.jsx +21 -0
  37. package/templates/basic-js/src/components/pages/CounterPage.jsx +27 -0
  38. package/templates/basic-js/src/components/pages/GreetingPage.jsx +24 -0
  39. package/templates/basic-js/src/components/pages/HomePage.jsx +52 -0
  40. package/templates/basic-js/src/data/counter.js +11 -0
  41. package/templates/basic-js/src/router.gen.ts +8 -0
  42. package/templates/basic-js/src/router.jsx +25 -0
  43. package/templates/basic-js/src/router.named-routes.gen.ts +17 -0
  44. package/templates/basic-js/src/styles.css +5 -0
  45. package/templates/basic-js/test/router.test.js +8 -0
  46. package/templates/basic-js/vite.config.js +17 -0
  47. package/templates/basic-js/vitest.config.js +10 -0
  48. package/templates/cloudflare/.oxfmtrc.json +9 -0
  49. package/templates/cloudflare/.oxlintrc.json +6 -0
  50. package/templates/cloudflare/README.md +40 -0
  51. package/templates/cloudflare/_gitignore +6 -0
  52. package/templates/cloudflare/package.json +44 -0
  53. package/templates/cloudflare/src/actions/counter.ts +11 -0
  54. package/templates/cloudflare/src/components/Counter.tsx +50 -0
  55. package/templates/cloudflare/src/components/Document.tsx +78 -0
  56. package/templates/cloudflare/src/components/Fallbacks.tsx +36 -0
  57. package/templates/cloudflare/src/components/pages/AboutPage.tsx +21 -0
  58. package/templates/cloudflare/src/components/pages/CounterPage.tsx +27 -0
  59. package/templates/cloudflare/src/components/pages/GreetingPage.tsx +24 -0
  60. package/templates/cloudflare/src/components/pages/HomePage.tsx +52 -0
  61. package/templates/cloudflare/src/data/counter.ts +11 -0
  62. package/templates/cloudflare/src/env.ts +13 -0
  63. package/templates/cloudflare/src/router.gen.ts +8 -0
  64. package/templates/cloudflare/src/router.named-routes.gen.ts +17 -0
  65. package/templates/cloudflare/src/router.tsx +33 -0
  66. package/templates/cloudflare/src/styles.css +5 -0
  67. package/templates/cloudflare/src/worker.rsc.tsx +9 -0
  68. package/templates/cloudflare/test/router.test.ts +8 -0
  69. package/templates/cloudflare/tsconfig.json +19 -0
  70. package/templates/cloudflare/vite.config.ts +17 -0
  71. package/templates/cloudflare/vitest.config.ts +10 -0
  72. package/templates/cloudflare/wrangler.json +9 -0
  73. package/templates/vercel/.oxfmtrc.json +9 -0
  74. package/templates/vercel/.oxlintrc.json +6 -0
  75. package/templates/vercel/README.md +50 -0
  76. package/templates/vercel/_gitignore +5 -0
  77. package/templates/vercel/package.json +43 -0
  78. package/templates/vercel/scripts/preview.mjs +23 -0
  79. package/templates/vercel/scripts/serve-vercel-output.mjs +67 -0
  80. package/templates/vercel/src/actions/counter.ts +11 -0
  81. package/templates/vercel/src/components/Counter.tsx +50 -0
  82. package/templates/vercel/src/components/Document.tsx +78 -0
  83. package/templates/vercel/src/components/Fallbacks.tsx +36 -0
  84. package/templates/vercel/src/components/pages/AboutPage.tsx +21 -0
  85. package/templates/vercel/src/components/pages/CounterPage.tsx +27 -0
  86. package/templates/vercel/src/components/pages/GreetingPage.tsx +24 -0
  87. package/templates/vercel/src/components/pages/HomePage.tsx +52 -0
  88. package/templates/vercel/src/data/counter.ts +11 -0
  89. package/templates/vercel/src/router.gen.ts +8 -0
  90. package/templates/vercel/src/router.named-routes.gen.ts +17 -0
  91. package/templates/vercel/src/router.tsx +33 -0
  92. package/templates/vercel/src/styles.css +5 -0
  93. package/templates/vercel/test/router.test.ts +8 -0
  94. package/templates/vercel/tsconfig.json +19 -0
  95. package/templates/vercel/vite.config.ts +17 -0
  96. package/templates/vercel/vitest.config.ts +10 -0
@@ -0,0 +1,36 @@
1
+ import { Link, href } from "@rangojs/router/client";
2
+
3
+ const linkClass =
4
+ "mt-6 inline-flex font-medium text-blue-600 underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-blue-600 dark:text-blue-400 dark:focus-visible:outline-blue-400";
5
+
6
+ export function ErrorFallback() {
7
+ return (
8
+ <main>
9
+ <div role="alert">
10
+ <h1 className="text-3xl font-semibold tracking-tight">
11
+ Something went wrong
12
+ </h1>
13
+ <p className="mt-4 leading-7 text-zinc-600 dark:text-zinc-400">
14
+ The page could not be rendered. Try again or return home.
15
+ </p>
16
+ </div>
17
+ <Link to={href("/")} className={linkClass}>
18
+ Return home
19
+ </Link>
20
+ </main>
21
+ );
22
+ }
23
+
24
+ export function NotFoundPage() {
25
+ return (
26
+ <main>
27
+ <h1 className="text-3xl font-semibold tracking-tight">Page not found</h1>
28
+ <p className="mt-4 leading-7 text-zinc-600 dark:text-zinc-400">
29
+ The page you requested does not exist.
30
+ </p>
31
+ <Link to={href("/")} className={linkClass}>
32
+ Return home
33
+ </Link>
34
+ </main>
35
+ );
36
+ }
@@ -0,0 +1,21 @@
1
+ import { Meta, type HandlerContext } from "@rangojs/router";
2
+
3
+ export function AboutPage(ctx: HandlerContext) {
4
+ const meta = ctx.use(Meta);
5
+ meta({ title: "About — Rango" });
6
+ meta({
7
+ name: "description",
8
+ content: "How this Rango starter structures routes and server components",
9
+ });
10
+
11
+ return (
12
+ <main>
13
+ <h1 className="text-3xl font-semibold tracking-tight">About</h1>
14
+ <p className="mt-4 leading-7 text-zinc-600 dark:text-zinc-400">
15
+ Routes are plain functions receiving a handler context. Register them in{" "}
16
+ <code>src/router.tsx</code> with Django-style URL patterns; route names
17
+ generate typed <code>href()</code> lookups as you save.
18
+ </p>
19
+ </main>
20
+ );
21
+ }
@@ -0,0 +1,27 @@
1
+ import { Meta, type HandlerContext } from "@rangojs/router";
2
+ import { Counter } from "../Counter.js";
3
+ import { getCount } from "../../data/counter.js";
4
+
5
+ export function CounterPage(ctx: HandlerContext) {
6
+ const meta = ctx.use(Meta);
7
+ meta({ title: "Counter — Rango" });
8
+ meta({
9
+ name: "description",
10
+ content: "A progressively enhanced React server action counter",
11
+ });
12
+
13
+ const initialCount = getCount();
14
+
15
+ return (
16
+ <main>
17
+ <h1 className="text-3xl font-semibold tracking-tight">Counter</h1>
18
+ <p className="mt-4 leading-7 text-zinc-600 dark:text-zinc-400">
19
+ The count lives on the server. The form works before JavaScript loads,
20
+ then React adds pending feedback without changing the action.
21
+ </p>
22
+ <div className="mt-8">
23
+ <Counter initialCount={initialCount} />
24
+ </div>
25
+ </main>
26
+ );
27
+ }
@@ -0,0 +1,24 @@
1
+ import { Meta, type Handler } from "@rangojs/router";
2
+
3
+ export const GreetingPage: Handler<"greeting"> = (ctx) => {
4
+ const name = ctx.params.name;
5
+
6
+ const meta = ctx.use(Meta);
7
+ meta({ title: `Hello ${name} — Rango` });
8
+ meta({
9
+ name: "description",
10
+ content: `A typed dynamic route greeting ${name}`,
11
+ });
12
+
13
+ return (
14
+ <main>
15
+ <h1 className="break-words text-3xl font-semibold tracking-tight">
16
+ Hello, {name}!
17
+ </h1>
18
+ <p className="mt-4 leading-7 text-zinc-600 dark:text-zinc-400">
19
+ This page matched <code>/hello/:name</code> and read the param from{" "}
20
+ <code>ctx.params</code> on the server.
21
+ </p>
22
+ </main>
23
+ );
24
+ };
@@ -0,0 +1,52 @@
1
+ import { Meta, type HandlerContext } from "@rangojs/router";
2
+ import { Link } from "@rangojs/router/client";
3
+
4
+ export function HomePage(ctx: HandlerContext) {
5
+ const meta = ctx.use(Meta);
6
+ meta({ title: "Home — Rango" });
7
+ meta({
8
+ name: "description",
9
+ content: "A React Server Components app powered by @rangojs/router",
10
+ });
11
+
12
+ const greetingUrl = ctx.reverse("greeting", { name: "world" });
13
+ const counterUrl = ctx.reverse("counter");
14
+
15
+ return (
16
+ <main>
17
+ <h1 className="text-3xl font-semibold tracking-tight">
18
+ Welcome to Rango
19
+ </h1>
20
+ <p className="mt-4 leading-7 text-zinc-600 dark:text-zinc-400">
21
+ This starter renders React Server Components with streaming SSR, typed
22
+ routes, and Tailwind CSS. Handlers run on the server; client components
23
+ hydrate where you opt in with <code>"use client"</code>.
24
+ </p>
25
+ <ul className="mt-8 space-y-3">
26
+ <li className="rounded-lg border border-zinc-200 p-4 dark:border-zinc-800">
27
+ <Link
28
+ to={greetingUrl}
29
+ className="font-medium text-blue-600 hover:underline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-blue-600 dark:text-blue-400 dark:focus-visible:outline-blue-400"
30
+ >
31
+ Dynamic route
32
+ </Link>
33
+ <p className="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
34
+ <code>/hello/:name</code> — read path params from the handler
35
+ context.
36
+ </p>
37
+ </li>
38
+ <li className="rounded-lg border border-zinc-200 p-4 dark:border-zinc-800">
39
+ <Link
40
+ to={counterUrl}
41
+ className="font-medium text-blue-600 hover:underline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-blue-600 dark:text-blue-400 dark:focus-visible:outline-blue-400"
42
+ >
43
+ Server actions
44
+ </Link>
45
+ <p className="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
46
+ A client component calling <code>"use server"</code> functions.
47
+ </p>
48
+ </li>
49
+ </ul>
50
+ </main>
51
+ );
52
+ }
@@ -0,0 +1,11 @@
1
+ // In-memory state for demo purposes: it resets on restart and is per-instance.
2
+ // Use a database or KV store in a real app.
3
+ let count = 0;
4
+
5
+ export function getCount(): number {
6
+ return count;
7
+ }
8
+
9
+ export function updateCount(delta: number): void {
10
+ count += delta;
11
+ }
@@ -0,0 +1,8 @@
1
+ // Auto-generated by @rangojs/router - do not edit
2
+ export const routes = {
3
+ about: "/about",
4
+ counter: "/counter",
5
+ greeting: "/hello/:name",
6
+ home: "/",
7
+ } as const;
8
+ export type routes = typeof routes;
@@ -0,0 +1,17 @@
1
+ // Auto-generated by @rangojs/router - do not edit
2
+ export const NamedRoutes = {
3
+ about: "/about",
4
+ counter: "/counter",
5
+ greeting: "/hello/:name",
6
+ home: "/",
7
+ } as const;
8
+
9
+ // Aliased so the augmentation below does not pay a homomorphic mapped-type
10
+ // instantiation per route; `as const` already makes the members readonly.
11
+ type NamedRoutesShape = typeof NamedRoutes;
12
+
13
+ declare global {
14
+ namespace Rango {
15
+ interface GeneratedRouteMap extends NamedRoutesShape {}
16
+ }
17
+ }
@@ -0,0 +1,33 @@
1
+ import { createRouter, urls } from "@rangojs/router";
2
+ import { updateCount } from "./actions/counter.js";
3
+ import { Document } from "./components/Document.js";
4
+ import { ErrorFallback, NotFoundPage } from "./components/Fallbacks.js";
5
+ import { HomePage } from "./components/pages/HomePage.js";
6
+ import { AboutPage } from "./components/pages/AboutPage.js";
7
+ import { GreetingPage } from "./components/pages/GreetingPage.js";
8
+ import { CounterPage } from "./components/pages/CounterPage.js";
9
+
10
+ export const urlpatterns = urls(({ path, revalidate }) => [
11
+ path("/", HomePage, { name: "home" }),
12
+ path("/about", AboutPage, { name: "about" }),
13
+ path("/hello/:name", GreetingPage, { name: "greeting" }),
14
+ path("/counter", CounterPage, { name: "counter" }, () => [
15
+ revalidate((ctx) => ctx.isAction(updateCount) || undefined),
16
+ ]),
17
+ ]);
18
+
19
+ export const router = createRouter({
20
+ document: Document,
21
+ urls: urlpatterns,
22
+ defaultErrorBoundary: ErrorFallback,
23
+ defaultNotFoundBoundary: NotFoundPage,
24
+ notFound: NotFoundPage,
25
+ });
26
+
27
+ type AppRoutes = typeof router.routeMap;
28
+
29
+ declare global {
30
+ namespace Rango {
31
+ interface RegisteredRoutes extends AppRoutes {}
32
+ }
33
+ }
@@ -0,0 +1,5 @@
1
+ @import "tailwindcss";
2
+
3
+ :root {
4
+ color-scheme: light dark;
5
+ }
@@ -0,0 +1,8 @@
1
+ import { assertGeneratedRoutesMatch } from "@rangojs/router/testing";
2
+ import { test } from "vitest";
3
+ import { NamedRoutes } from "../src/router.named-routes.gen.js";
4
+ import { router } from "../src/router.js";
5
+
6
+ test("generated routes match the runtime router", async () => {
7
+ await assertGeneratedRoutesMatch(router, NamedRoutes);
8
+ });
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "moduleDetection": "force",
8
+ "allowImportingTsExtensions": true,
9
+ "isolatedModules": true,
10
+ "noEmit": true,
11
+ "jsx": "react-jsx",
12
+ "strict": true,
13
+ "skipLibCheck": true,
14
+ "noFallthroughCasesInSwitch": true,
15
+ "noUncheckedSideEffectImports": true,
16
+ "types": ["node", "vite/client"]
17
+ },
18
+ "include": ["src", "vite.config.ts"]
19
+ }
@@ -0,0 +1,17 @@
1
+ import { defineConfig } from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+ import tailwindcss from "@tailwindcss/vite";
4
+ import { rango } from "@rangojs/router/vite";
5
+
6
+ // The rango plugin auto-discovers the router (the single createRouter call in
7
+ // src/router.tsx) and provides the client/server entries, so no index.html or
8
+ // entry files are needed.
9
+ export default defineConfig(({ command }) => ({
10
+ plugins: [react(), tailwindcss(), rango()],
11
+ // Fold NODE_ENV at build time so React's dev/prod branches collapse and the
12
+ // production bundle does not ship react development chunks.
13
+ define:
14
+ command === "build"
15
+ ? { "process.env.NODE_ENV": JSON.stringify("production") }
16
+ : undefined,
17
+ }));
@@ -0,0 +1,10 @@
1
+ import { rangoTestConfig } from "@rangojs/router/testing/vitest";
2
+ import { defineConfig } from "vitest/config";
3
+
4
+ export default defineConfig({
5
+ test: {
6
+ environment: "node",
7
+ include: ["test/**/*.test.{ts,tsx}"],
8
+ ...rangoTestConfig(),
9
+ },
10
+ });
@@ -0,0 +1,9 @@
1
+ {
2
+ "printWidth": 80,
3
+ "tabWidth": 2,
4
+ "useTabs": false,
5
+ "semi": true,
6
+ "singleQuote": false,
7
+ "trailingComma": "all",
8
+ "ignorePatterns": ["dist/", "node_modules/", ".vite/"]
9
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "plugins": ["typescript"],
3
+ "categories": { "correctness": "error" },
4
+ "rules": { "typescript/triple-slash-reference": "off" },
5
+ "ignorePatterns": ["dist/", "node_modules/", ".vite/"]
6
+ }
@@ -0,0 +1,38 @@
1
+ # Rango app
2
+
3
+ A [React Server Components](https://react.dev/reference/rsc/server-components) app powered by [`@rangojs/router`](https://www.npmjs.com/package/@rangojs/router), Vite, and Tailwind CSS — plain JavaScript, no TypeScript.
4
+
5
+ ## Commands
6
+
7
+ ```sh
8
+ npm run dev # start the dev server
9
+ npm run build # production build to dist/
10
+ npm run start # run the production server (server.mjs)
11
+ npm run preview # serve the production build locally (Vite preview, local-only)
12
+ npm run generate # regenerate committed route maps
13
+ npm test # run unit tests
14
+ npm run lint # oxlint
15
+ npm run format # oxfmt check
16
+ npm run check # tests + lint + format
17
+ ```
18
+
19
+ Requires Node.js 24 or newer.
20
+
21
+ ## Deploying
22
+
23
+ `npm run build && npm run start` is the production path: `server.mjs` serves fingerprinted assets with immutable cache headers, keeps public files revalidatable, and streams everything else through the router's fetch handler. Set `PORT` to change the port. `vite preview` is a local convenience, not a production server.
24
+
25
+ ## Project layout
26
+
27
+ - `server.mjs` — the production server (`npm run start`): static assets via sirv, everything else via the router.
28
+ - `src/router.jsx` — the router: URL patterns, route names, and the document component. The Vite plugin auto-discovers it; there is no `index.html` or entry file.
29
+ - `src/components/Document.jsx` — the HTML shell (client component). Tailwind is wired here via `styles.css?url`.
30
+ - `src/components/pages/` — route handlers. Each is a server function receiving the handler context (params, meta, headers, …).
31
+ - `src/actions/` — `"use server"` functions callable from client components.
32
+ - `test/router.test.js` — route-map drift test using `@rangojs/router/testing`.
33
+ - `src/router.named-routes.gen.ts` — generated global route names used by `ctx.reverse()` and editor path checks; commit it.
34
+ - `src/router.gen.ts` — generated local route map used by `useReverse()` in client components; run `npm run generate` after route changes and commit it.
35
+
36
+ ## Why is `typescript` in devDependencies?
37
+
38
+ Your app code is plain JavaScript. The router's route-name generator uses the TypeScript compiler API to parse your routes and write `src/router.named-routes.gen.ts`, so the `typescript` package must be installed — no `tsconfig.json`, no type checking.
@@ -0,0 +1,4 @@
1
+ node_modules
2
+ dist
3
+ .env*
4
+ *.local
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "rango-template-basic-js",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "predev": "rango generate src/",
8
+ "dev": "vite",
9
+ "prebuild": "rango generate src/",
10
+ "build": "vite build",
11
+ "start": "node server.mjs",
12
+ "preview": "vite preview",
13
+ "generate": "rango generate src/",
14
+ "test": "vitest run",
15
+ "lint": "oxlint --deny-warnings .",
16
+ "format": "oxfmt --check .",
17
+ "format:fix": "oxfmt .",
18
+ "check": "vitest run && oxlint --deny-warnings . && oxfmt --check ."
19
+ },
20
+ "dependencies": {
21
+ "@rangojs/router": "^0.4.0",
22
+ "react": "^19.2.6",
23
+ "react-dom": "^19.2.6",
24
+ "sirv": "^3.0.0",
25
+ "srvx": "^0.11.22"
26
+ },
27
+ "devDependencies": {
28
+ "@tailwindcss/vite": "^4.3.1",
29
+ "@vitejs/plugin-react": "^6.0.0",
30
+ "oxfmt": "^0.58.0",
31
+ "oxlint": "^1.73.0",
32
+ "tailwindcss": "^4.3.1",
33
+ "typescript": "^5.9.3",
34
+ "vite": "^8.0.16",
35
+ "vitest": "^4.1.10"
36
+ },
37
+ "engines": {
38
+ "node": ">=24.0.0"
39
+ }
40
+ }
@@ -0,0 +1,46 @@
1
+ // Production server: serves the vite build from dist/.
2
+ // Build first (`npm run build`), then `npm run start`.
3
+ import { createServer } from "node:http";
4
+ import sirv from "sirv";
5
+ import { toNodeHandler } from "srvx/node";
6
+ import handler from "./dist/rsc/index.js";
7
+
8
+ const port = Number(process.env.PORT ?? 3000);
9
+
10
+ const serveStatic = sirv("dist/client");
11
+ const serveHashedAssets = sirv("dist/client", {
12
+ immutable: true,
13
+ maxAge: 31536000,
14
+ });
15
+
16
+ // srvx bridges the router's Web fetch handler onto Node's (req, res), piping
17
+ // streamed responses through.
18
+ const serveApp = toNodeHandler((request) =>
19
+ handler(request, { env: process.env }),
20
+ );
21
+
22
+ function isFingerprintedAsset(url) {
23
+ const pathname = new URL(url, "http://localhost").pathname;
24
+ return /^\/assets\/.+-[A-Za-z0-9_-]{8,}\.[A-Za-z0-9]+$/.test(pathname);
25
+ }
26
+
27
+ const server = createServer((req, res) => {
28
+ // Only Vite fingerprinted files are immutable. A user may also place an
29
+ // unhashed public file under public/assets/, which must remain revalidatable.
30
+ const serveAssets = isFingerprintedAsset(req.url ?? "/")
31
+ ? serveHashedAssets
32
+ : serveStatic;
33
+ serveAssets(req, res, () => serveApp(req, res));
34
+ });
35
+ server.listen(port, () => {
36
+ console.log(`Listening on http://localhost:${server.address().port}`);
37
+ });
38
+
39
+ function shutdown() {
40
+ const forceClose = setTimeout(() => server.closeAllConnections(), 10_000);
41
+ forceClose.unref();
42
+ server.close(() => clearTimeout(forceClose));
43
+ }
44
+
45
+ process.once("SIGTERM", shutdown);
46
+ process.once("SIGINT", shutdown);
@@ -0,0 +1,11 @@
1
+ "use server";
2
+
3
+ import { updateCount as persistCount } from "../data/counter.js";
4
+
5
+ export async function updateCount(formData) {
6
+ const delta = formData.get("delta");
7
+ if (delta !== "-1" && delta !== "1") {
8
+ throw new Error("Invalid counter change");
9
+ }
10
+ persistCount(Number(delta));
11
+ }
@@ -0,0 +1,50 @@
1
+ "use client";
2
+
3
+ import { useFormStatus } from "react-dom";
4
+ import { updateCount } from "../actions/counter.js";
5
+
6
+ export function Counter({ initialCount }) {
7
+ return (
8
+ <form action={updateCount}>
9
+ <CounterControls count={initialCount} />
10
+ </form>
11
+ );
12
+ }
13
+
14
+ function CounterControls({ count }) {
15
+ const { pending } = useFormStatus();
16
+ return (
17
+ <div className="flex items-center gap-4" aria-busy={pending}>
18
+ <button
19
+ type="submit"
20
+ name="delta"
21
+ value="-1"
22
+ disabled={pending}
23
+ aria-label="Decrease count"
24
+ className="rounded-md bg-zinc-900 px-4 py-2 text-sm font-medium text-white hover:bg-zinc-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 disabled:opacity-50 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-300 dark:focus-visible:outline-blue-400"
25
+ >
26
+ −1
27
+ </button>
28
+ <output
29
+ aria-live="polite"
30
+ className="min-w-16 text-center text-2xl font-semibold tabular-nums"
31
+ style={{ opacity: pending ? 0.5 : 1 }}
32
+ >
33
+ {count}
34
+ </output>
35
+ <button
36
+ type="submit"
37
+ name="delta"
38
+ value="1"
39
+ disabled={pending}
40
+ aria-label="Increase count"
41
+ className="rounded-md bg-zinc-900 px-4 py-2 text-sm font-medium text-white hover:bg-zinc-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 disabled:opacity-50 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-300 dark:focus-visible:outline-blue-400"
42
+ >
43
+ +1
44
+ </button>
45
+ <span className="sr-only" role="status">
46
+ {pending ? "Updating count" : ""}
47
+ </span>
48
+ </div>
49
+ );
50
+ }
@@ -0,0 +1,78 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef } from "react";
4
+ import {
5
+ Link,
6
+ MetaTags,
7
+ useNavigation,
8
+ usePathname,
9
+ useReverse,
10
+ } from "@rangojs/router/client";
11
+ import { routes } from "../router.gen.js";
12
+ import styles from "../styles.css?url";
13
+
14
+ export function Document({ children }) {
15
+ const pathname = usePathname();
16
+ const reverse = useReverse(routes);
17
+ const isNavigating = useNavigation(
18
+ (navigation) => navigation.state !== "idle",
19
+ );
20
+ const contentRef = useRef(null);
21
+ const previousPathname = useRef(pathname);
22
+
23
+ useEffect(() => {
24
+ if (previousPathname.current !== pathname) {
25
+ contentRef.current?.focus();
26
+ previousPathname.current = pathname;
27
+ }
28
+ }, [pathname]);
29
+
30
+ const links = [
31
+ [reverse("home"), "Home"],
32
+ [reverse("about"), "About"],
33
+ [reverse("counter"), "Counter"],
34
+ ];
35
+
36
+ return (
37
+ <html lang="en">
38
+ <head>
39
+ <MetaTags />
40
+ <link rel="preload" as="style" href={styles} precedence="default" />
41
+ <link rel="stylesheet" href={styles} precedence="default" />
42
+ </head>
43
+ <body className="min-h-dvh bg-white text-zinc-900 antialiased dark:bg-zinc-950 dark:text-zinc-100">
44
+ <div className="mx-auto max-w-2xl px-6 py-10">
45
+ <a
46
+ href="#main-content"
47
+ className="sr-only rounded bg-white px-3 py-2 text-zinc-950 focus:not-sr-only focus:absolute focus:left-4 focus:top-4 focus:z-10"
48
+ >
49
+ Skip to content
50
+ </a>
51
+ <nav className="mb-10 flex items-center gap-6 border-b border-zinc-200 pb-4 text-sm font-medium dark:border-zinc-800">
52
+ {links.map(([to, label]) => (
53
+ <Link
54
+ key={to}
55
+ to={to}
56
+ aria-current={pathname === to ? "page" : undefined}
57
+ className="hover:text-blue-600 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-blue-600 dark:hover:text-blue-400 dark:focus-visible:outline-blue-400"
58
+ >
59
+ {label}
60
+ </Link>
61
+ ))}
62
+ </nav>
63
+ <span className="sr-only" aria-live="polite">
64
+ {isNavigating ? "Loading page" : ""}
65
+ </span>
66
+ <div
67
+ id="main-content"
68
+ ref={contentRef}
69
+ tabIndex={-1}
70
+ className="outline-none"
71
+ >
72
+ {children}
73
+ </div>
74
+ </div>
75
+ </body>
76
+ </html>
77
+ );
78
+ }
@@ -0,0 +1,36 @@
1
+ import { Link, href } from "@rangojs/router/client";
2
+
3
+ const linkClass =
4
+ "mt-6 inline-flex font-medium text-blue-600 underline-offset-4 hover:underline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-blue-600 dark:text-blue-400 dark:focus-visible:outline-blue-400";
5
+
6
+ export function ErrorFallback() {
7
+ return (
8
+ <main>
9
+ <div role="alert">
10
+ <h1 className="text-3xl font-semibold tracking-tight">
11
+ Something went wrong
12
+ </h1>
13
+ <p className="mt-4 leading-7 text-zinc-600 dark:text-zinc-400">
14
+ The page could not be rendered. Try again or return home.
15
+ </p>
16
+ </div>
17
+ <Link to={href("/")} className={linkClass}>
18
+ Return home
19
+ </Link>
20
+ </main>
21
+ );
22
+ }
23
+
24
+ export function NotFoundPage() {
25
+ return (
26
+ <main>
27
+ <h1 className="text-3xl font-semibold tracking-tight">Page not found</h1>
28
+ <p className="mt-4 leading-7 text-zinc-600 dark:text-zinc-400">
29
+ The page you requested does not exist.
30
+ </p>
31
+ <Link to={href("/")} className={linkClass}>
32
+ Return home
33
+ </Link>
34
+ </main>
35
+ );
36
+ }
@@ -0,0 +1,21 @@
1
+ import { Meta } from "@rangojs/router";
2
+
3
+ export function AboutPage(ctx) {
4
+ const meta = ctx.use(Meta);
5
+ meta({ title: "About — Rango" });
6
+ meta({
7
+ name: "description",
8
+ content: "How this Rango starter structures routes and server components",
9
+ });
10
+
11
+ return (
12
+ <main>
13
+ <h1 className="text-3xl font-semibold tracking-tight">About</h1>
14
+ <p className="mt-4 leading-7 text-zinc-600 dark:text-zinc-400">
15
+ Routes are plain functions receiving a handler context. Register them in{" "}
16
+ <code>src/router.jsx</code> with Django-style URL patterns; route names
17
+ generate <code>href()</code> lookups as you save.
18
+ </p>
19
+ </main>
20
+ );
21
+ }