Skip to content

Commit 813adbb

Browse files
jacob-ebeymarkmals
andauthored
Deno template (#131)
Co-authored-by: Mark Malstrom <[email protected]>
1 parent afd1263 commit 813adbb

22 files changed

+2456
-2
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ jobs:
3030
with:
3131
node-version: 22
3232
cache: "pnpm"
33+
- uses: denoland/setup-deno@v2
34+
with:
35+
deno-version: v2.3.3
3336
- name: Install dependencies
3437
run: pnpm install
3538
- name: Install Playwright Browsers

.tests/test.deno.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { expect, Page } from "@playwright/test";
2+
import getPort from "get-port";
3+
4+
import { matchLine, testTemplate, urlRegex } from "./utils";
5+
6+
const test = testTemplate("deno", "deno install");
7+
8+
test("typecheck", async ({ $ }) => {
9+
await $(`deno task typecheck`);
10+
});
11+
12+
test("dev", async ({ page, $ }) => {
13+
const port = await getPort();
14+
const dev = $(`deno task dev --port ${port}`);
15+
16+
const url = await matchLine(dev.stdout, urlRegex.viteDev);
17+
18+
await workflow({ page, url });
19+
const [, ...restLines] = dev.buffer.stderr.split("\n");
20+
expect(restLines.join("\n")).toBe("");
21+
});
22+
23+
test("build + start", async ({ page, $ }) => {
24+
await $(`deno task build`);
25+
26+
const port = await getPort();
27+
const start = $(`deno task start`, { env: { PORT: String(port) } });
28+
29+
const url = await matchLine(start.stderr, urlRegex.deno);
30+
const localURL = new URL(url);
31+
localURL.hostname = "localhost";
32+
33+
await workflow({ page, url: localURL.href });
34+
const [, ...restLines] = start.buffer.stderr.split("\n");
35+
expect(restLines.join("\n")).toBe(
36+
`Listening on ${url} (${localURL})\n`,
37+
);
38+
});
39+
40+
async function workflow({ page, url }: { page: Page; url: string }) {
41+
await page.goto(url);
42+
await page.getByRole("link", { name: "React Router Docs" }).waitFor();
43+
await page.getByRole("link", { name: "Join Discord" }).waitFor();
44+
await expect(page).toHaveTitle(/New React Router App/);
45+
expect(page.errors).toStrictEqual([]);
46+
}

.tests/utils.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ type Command = (
3535
buffer: { stdout: string; stderr: string };
3636
};
3737

38-
export const testTemplate = (template: string) =>
38+
export const testTemplate = (template: string, installCommand?: string) =>
3939
playwrightTest.extend<{
4040
cwd: string;
4141
edit: Edit;
@@ -57,7 +57,23 @@ export const testTemplate = (template: string) =>
5757
errorOnExist: true,
5858
filter: (src) => Path.normalize(src) !== nodeModulesPath,
5959
});
60-
fs.symlinkSync(nodeModulesPath, Path.join(cwd, "node_modules"));
60+
61+
if (installCommand) {
62+
const spawn = execa({
63+
cwd,
64+
env: {
65+
NO_COLOR: "1",
66+
FORCE_COLOR: "0",
67+
},
68+
reject: false,
69+
});
70+
71+
const [file, ...args] = parseCommandString(installCommand);
72+
73+
await spawn(file, args);
74+
} else {
75+
fs.symlinkSync(nodeModulesPath, Path.join(cwd, "node_modules"));
76+
}
6177

6278
await use(cwd);
6379

@@ -136,6 +152,7 @@ export const urlRegex = {
136152
viteDev: urlMatch({ prefix: /Local:\s+/ }),
137153
reactRouterServe: urlMatch({ prefix: /\[react-router-serve\]\s+/ }),
138154
custom: urlMatch({ prefix: /Server is running on / }),
155+
deno: urlMatch({ prefix: /Listening on / }),
139156
netlify: urlMatch({ prefix: / Server now ready on / }),
140157
wrangler: urlMatch({ prefix: /Ready on / }),
141158
};

deno/.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.DS_Store
2+
.env
3+
/node_modules/
4+
5+
# React Router
6+
/.react-router/
7+
/build/

deno/.vscode/extensions.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"recommendations": ["denoland.vscode-deno"]
3+
}

deno/.vscode/settings.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"deno.enable": true,
3+
4+
"editor.codeActionsOnSave": {
5+
"source.fixAll": "explicit"
6+
},
7+
8+
"editor.defaultFormatter": "denoland.vscode-deno",
9+
"editor.formatOnSave": true,
10+
11+
"[javascript]": {
12+
"editor.defaultFormatter": "denoland.vscode-deno"
13+
},
14+
"[typescript]": {
15+
"editor.defaultFormatter": "denoland.vscode-deno"
16+
},
17+
"[jsx]": {
18+
"editor.defaultFormatter": "denoland.vscode-deno"
19+
},
20+
"[tsx]": {
21+
"editor.defaultFormatter": "denoland.vscode-deno"
22+
},
23+
"[json]": {
24+
"editor.defaultFormatter": "denoland.vscode-deno"
25+
},
26+
"[jsonc]": {
27+
"editor.defaultFormatter": "denoland.vscode-deno"
28+
}
29+
}

deno/README.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Welcome to React Router!
2+
3+
A modern, production-ready template for building full-stack React applications
4+
using React Router.
5+
6+
## Features
7+
8+
- 🚀 Server-side rendering
9+
- ⚡️ Hot Module Replacement (HMR)
10+
- 📦 Asset bundling and optimization
11+
- 🔄 Data loading and mutations
12+
- 🔒 TypeScript by default
13+
- 🎉 TailwindCSS for styling
14+
- 📖 [React Router docs](https://reactrouter.com/)
15+
16+
## Getting Started
17+
18+
### Installation
19+
20+
Install the dependencies:
21+
22+
```bash
23+
deno install
24+
```
25+
26+
### Development
27+
28+
Start the development server with HMR:
29+
30+
```bash
31+
deno task dev
32+
```
33+
34+
Your application will be available at `http://localhost:5173`.
35+
36+
## Building for Production
37+
38+
Create a production build:
39+
40+
```bash
41+
deno task build
42+
```
43+
44+
## Deployment
45+
46+
### Deno Deploy
47+
48+
After running a build, deploy to https://deno.com/deploy with the following command:
49+
50+
```bash
51+
deno run -A jsr:@deno/deployctl deploy --entrypoint server.ts
52+
```
53+
54+
### DIY Deployment
55+
56+
If you're familiar with deploying Deno applications, the built-in app server is
57+
production-ready.
58+
59+
Make sure to deploy the output of `deno task build`
60+
61+
```
62+
├── deno.jsonc
63+
├── deno.lock
64+
├── server.ts
65+
├── build/
66+
│ ├── client/ # Static assets
67+
│ └── server/ # Server-side code
68+
```
69+
70+
## Styling
71+
72+
This template comes with [Tailwind CSS](https://tailwindcss.com/) already
73+
configured for a simple default starting experience. You can use whatever CSS
74+
framework you prefer.
75+
76+
---
77+
78+
Built with ❤️ using React Router.

deno/app/app.css

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
@import "tailwindcss";
2+
3+
@theme {
4+
--font-sans:
5+
"Inter", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
6+
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
7+
}
8+
9+
html,
10+
body {
11+
@apply bg-white dark:bg-gray-950;
12+
13+
@media (prefers-color-scheme: dark) {
14+
color-scheme: dark;
15+
}
16+
}

deno/app/entry.server.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import type { AppLoadContext, EntryContext } from "react-router";
2+
import { ServerRouter } from "react-router";
3+
import { isbot } from "isbot";
4+
import { renderToReadableStream } from "react-dom/server";
5+
6+
export default async function handleRequest(
7+
request: Request,
8+
responseStatusCode: number,
9+
responseHeaders: Headers,
10+
routerContext: EntryContext,
11+
_loadContext: AppLoadContext,
12+
) {
13+
let shellRendered = false;
14+
const userAgent = request.headers.get("user-agent");
15+
16+
const body = await renderToReadableStream(
17+
<ServerRouter context={routerContext} url={request.url} />,
18+
{
19+
onError(error: unknown) {
20+
responseStatusCode = 500;
21+
// Log streaming rendering errors from inside the shell. Don't log
22+
// errors encountered during initial shell rendering since they'll
23+
// reject and get logged in handleDocumentRequest.
24+
if (shellRendered) {
25+
console.error(error);
26+
}
27+
},
28+
},
29+
);
30+
shellRendered = true;
31+
32+
// Ensure requests from bots and SPA Mode renders wait for all content to load before responding
33+
// https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
34+
if ((userAgent && isbot(userAgent)) || routerContext.isSpaMode) {
35+
await body.allReady;
36+
}
37+
38+
responseHeaders.set("Content-Type", "text/html");
39+
return new Response(body, {
40+
headers: responseHeaders,
41+
status: responseStatusCode,
42+
});
43+
}

deno/app/root.tsx

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import {
2+
isRouteErrorResponse,
3+
Links,
4+
Meta,
5+
Outlet,
6+
Scripts,
7+
ScrollRestoration,
8+
} from "react-router";
9+
10+
import type { Route } from "./+types/root.ts";
11+
import "./app.css";
12+
13+
export const links: Route.LinksFunction = () => [
14+
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
15+
{
16+
rel: "preconnect",
17+
href: "https://fonts.gstatic.com",
18+
crossOrigin: "anonymous",
19+
},
20+
{
21+
rel: "stylesheet",
22+
href:
23+
"https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
24+
},
25+
];
26+
27+
export function Layout({ children }: { children: React.ReactNode }) {
28+
return (
29+
<html lang="en">
30+
<head>
31+
<meta charSet="utf-8" />
32+
<meta name="viewport" content="width=device-width, initial-scale=1" />
33+
<Meta />
34+
<Links />
35+
</head>
36+
<body>
37+
{children}
38+
<ScrollRestoration />
39+
<Scripts />
40+
</body>
41+
</html>
42+
);
43+
}
44+
45+
export default function App() {
46+
return <Outlet />;
47+
}
48+
49+
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
50+
let message = "Oops!";
51+
let details = "An unexpected error occurred.";
52+
let stack: string | undefined;
53+
54+
if (isRouteErrorResponse(error)) {
55+
message = error.status === 404 ? "404" : "Error";
56+
details = error.status === 404
57+
? "The requested page could not be found."
58+
: error.statusText || details;
59+
} else if (import.meta.env.DEV && error && error instanceof Error) {
60+
details = error.message;
61+
stack = error.stack;
62+
}
63+
64+
return (
65+
<main className="pt-16 p-4 container mx-auto">
66+
<h1>{message}</h1>
67+
<p>{details}</p>
68+
{stack && (
69+
<pre className="w-full p-4 overflow-x-auto">
70+
<code>{stack}</code>
71+
</pre>
72+
)}
73+
</main>
74+
);
75+
}

deno/app/routes.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { index, type RouteConfig } from "@react-router/dev/routes";
2+
3+
export default [index("routes/home.tsx")] satisfies RouteConfig;

deno/app/routes/home.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { Route } from "./+types/home.ts";
2+
import { Welcome } from "../welcome/welcome.tsx";
3+
4+
export function meta({}: Route.MetaArgs) {
5+
return [
6+
{ title: "New React Router App" },
7+
{ name: "description", content: "Welcome to React Router!" },
8+
];
9+
}
10+
11+
export function loader() {
12+
return {
13+
denoVersion: Deno.version.deno,
14+
};
15+
}
16+
17+
export default function Home({ loaderData }: Route.ComponentProps) {
18+
return <Welcome denoVersion={loaderData.denoVersion} />;
19+
}

0 commit comments

Comments
 (0)