Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ jobs:
with:
node-version: 22
cache: "pnpm"
- uses: denoland/setup-deno@v2
with:
deno-version: v2.3.3
- name: Install dependencies
run: pnpm install
- name: Install Playwright Browsers
Expand Down
46 changes: 46 additions & 0 deletions .tests/test.deno.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { expect, Page } from "@playwright/test";
import getPort from "get-port";

import { matchLine, testTemplate, urlRegex } from "./utils";

const test = testTemplate("deno", "deno install");

test("typecheck", async ({ $ }) => {
await $(`deno task typecheck`);
});

test("dev", async ({ page, $ }) => {
const port = await getPort();
const dev = $(`deno task dev --port ${port}`);

const url = await matchLine(dev.stdout, urlRegex.viteDev);

await workflow({ page, url });
const [, ...restLines] = dev.buffer.stderr.split("\n");
expect(restLines.join("\n")).toBe("");
});

test("build + start", async ({ page, $ }) => {
await $(`deno task build`);

const port = await getPort();
const start = $(`deno task start`, { env: { PORT: String(port) } });

const url = await matchLine(start.stderr, urlRegex.deno);
const localURL = new URL(url);
localURL.hostname = "localhost";

await workflow({ page, url: localURL.href });
const [, ...restLines] = start.buffer.stderr.split("\n");
expect(restLines.join("\n")).toBe(
`Listening on ${url} (${localURL})\n`,
);
});

async function workflow({ page, url }: { page: Page; url: string }) {
await page.goto(url);
await page.getByRole("link", { name: "React Router Docs" }).waitFor();
await page.getByRole("link", { name: "Join Discord" }).waitFor();
await expect(page).toHaveTitle(/New React Router App/);
expect(page.errors).toStrictEqual([]);
}
21 changes: 19 additions & 2 deletions .tests/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ type Command = (
buffer: { stdout: string; stderr: string };
};

export const testTemplate = (template: string) =>
export const testTemplate = (template: string, installCommand?: string) =>
playwrightTest.extend<{
cwd: string;
edit: Edit;
Expand All @@ -57,7 +57,23 @@ export const testTemplate = (template: string) =>
errorOnExist: true,
filter: (src) => Path.normalize(src) !== nodeModulesPath,
});
fs.symlinkSync(nodeModulesPath, Path.join(cwd, "node_modules"));

if (installCommand) {
const spawn = execa({
cwd,
env: {
NO_COLOR: "1",
FORCE_COLOR: "0",
},
reject: false,
});

const [file, ...args] = parseCommandString(installCommand);

await spawn(file, args);
} else {
fs.symlinkSync(nodeModulesPath, Path.join(cwd, "node_modules"));
}

await use(cwd);

Expand Down Expand Up @@ -136,6 +152,7 @@ export const urlRegex = {
viteDev: urlMatch({ prefix: /Local:\s+/ }),
reactRouterServe: urlMatch({ prefix: /\[react-router-serve\]\s+/ }),
custom: urlMatch({ prefix: /Server is running on / }),
deno: urlMatch({ prefix: /Listening on / }),
netlify: urlMatch({ prefix: /◈ Server now ready on / }),
wrangler: urlMatch({ prefix: /Ready on / }),
};
Expand Down
7 changes: 7 additions & 0 deletions deno/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.DS_Store
.env
/node_modules/

# React Router
/.react-router/
/build/
3 changes: 3 additions & 0 deletions deno/.vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"recommendations": ["denoland.vscode-deno"]
}
29 changes: 29 additions & 0 deletions deno/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"deno.enable": true,

"editor.codeActionsOnSave": {
"source.fixAll": "explicit"
},

"editor.defaultFormatter": "denoland.vscode-deno",
"editor.formatOnSave": true,

"[javascript]": {
"editor.defaultFormatter": "denoland.vscode-deno"
},
"[typescript]": {
"editor.defaultFormatter": "denoland.vscode-deno"
},
"[jsx]": {
"editor.defaultFormatter": "denoland.vscode-deno"
},
"[tsx]": {
"editor.defaultFormatter": "denoland.vscode-deno"
},
"[json]": {
"editor.defaultFormatter": "denoland.vscode-deno"
},
"[jsonc]": {
"editor.defaultFormatter": "denoland.vscode-deno"
}
}
78 changes: 78 additions & 0 deletions deno/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Welcome to React Router!

A modern, production-ready template for building full-stack React applications
using React Router.

## Features

- 🚀 Server-side rendering
- ⚡️ Hot Module Replacement (HMR)
- 📦 Asset bundling and optimization
- 🔄 Data loading and mutations
- 🔒 TypeScript by default
- 🎉 TailwindCSS for styling
- 📖 [React Router docs](https://reactrouter.com/)

## Getting Started

### Installation

Install the dependencies:

```bash
deno install
```

### Development

Start the development server with HMR:

```bash
deno task dev
```

Your application will be available at `http://localhost:5173`.

## Building for Production

Create a production build:

```bash
deno task build
```

## Deployment

### Deno Deploy

After running a build, deploy to https://deno.com/deploy with the following command:

```bash
deno run -A jsr:@deno/deployctl deploy --entrypoint server.ts
```

### DIY Deployment

If you're familiar with deploying Deno applications, the built-in app server is
production-ready.

Make sure to deploy the output of `deno task build`

```
├── deno.jsonc
├── deno.lock
├── server.ts
├── build/
│ ├── client/ # Static assets
│ └── server/ # Server-side code
```

## Styling

This template comes with [Tailwind CSS](https://tailwindcss.com/) already
configured for a simple default starting experience. You can use whatever CSS
framework you prefer.

---

Built with ❤️ using React Router.
16 changes: 16 additions & 0 deletions deno/app/app.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
@import "tailwindcss";

@theme {
--font-sans:
"Inter", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}

html,
body {
@apply bg-white dark:bg-gray-950;

@media (prefers-color-scheme: dark) {
color-scheme: dark;
}
}
43 changes: 43 additions & 0 deletions deno/app/entry.server.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { AppLoadContext, EntryContext } from "react-router";
import { ServerRouter } from "react-router";
import { isbot } from "isbot";
import { renderToReadableStream } from "react-dom/server";

export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
routerContext: EntryContext,
_loadContext: AppLoadContext,
) {
let shellRendered = false;
const userAgent = request.headers.get("user-agent");

const body = await renderToReadableStream(
<ServerRouter context={routerContext} url={request.url} />,
{
onError(error: unknown) {
responseStatusCode = 500;
// Log streaming rendering errors from inside the shell. Don't log
// errors encountered during initial shell rendering since they'll
// reject and get logged in handleDocumentRequest.
if (shellRendered) {
console.error(error);
}
},
},
);
shellRendered = true;

// Ensure requests from bots and SPA Mode renders wait for all content to load before responding
// https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
if ((userAgent && isbot(userAgent)) || routerContext.isSpaMode) {
await body.allReady;
}

responseHeaders.set("Content-Type", "text/html");
return new Response(body, {
headers: responseHeaders,
status: responseStatusCode,
});
}
75 changes: 75 additions & 0 deletions deno/app/root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import {
isRouteErrorResponse,
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "react-router";

import type { Route } from "./+types/root.ts";
import "./app.css";

export const links: Route.LinksFunction = () => [
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
{
rel: "preconnect",
href: "https://fonts.gstatic.com",
crossOrigin: "anonymous",
},
{
rel: "stylesheet",
href:
"https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
},
];

export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
{children}
<ScrollRestoration />
<Scripts />
</body>
</html>
);
}

export default function App() {
return <Outlet />;
}

export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
let message = "Oops!";
let details = "An unexpected error occurred.";
let stack: string | undefined;

if (isRouteErrorResponse(error)) {
message = error.status === 404 ? "404" : "Error";
details = error.status === 404
? "The requested page could not be found."
: error.statusText || details;
} else if (import.meta.env.DEV && error && error instanceof Error) {
details = error.message;
stack = error.stack;
}

return (
<main className="pt-16 p-4 container mx-auto">
<h1>{message}</h1>
<p>{details}</p>
{stack && (
<pre className="w-full p-4 overflow-x-auto">
<code>{stack}</code>
</pre>
)}
</main>
);
}
3 changes: 3 additions & 0 deletions deno/app/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { index, type RouteConfig } from "@react-router/dev/routes";

export default [index("routes/home.tsx")] satisfies RouteConfig;
19 changes: 19 additions & 0 deletions deno/app/routes/home.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { Route } from "./+types/home.ts";
import { Welcome } from "../welcome/welcome.tsx";

export function meta({}: Route.MetaArgs) {
return [
{ title: "New React Router App" },
{ name: "description", content: "Welcome to React Router!" },
];
}

export function loader() {
return {
denoVersion: Deno.version.deno,
};
}

export default function Home({ loaderData }: Route.ComponentProps) {
return <Welcome denoVersion={loaderData.denoVersion} />;
}
Loading