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
25 changes: 25 additions & 0 deletions playground/typed-routes/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Typed Routes Playground

This playground demonstrates and tests the typed routes functionality with `linkFor` that automatically infers routes from the app definition.

## Features Tested

- Static routes (e.g., `/`)
- Routes with named parameters (e.g., `/users/:id`)
- Routes with wildcards (e.g., `/files/*`)
- Type-safe link generation with automatic route inference
- Parameter validation at compile-time and runtime

## Running the dev server

```shell
npm run dev
```

Point your browser to the URL displayed in the terminal (e.g. `http://localhost:5173/`).

## Testing

```shell
pnpm tsc --noEmit
```
48 changes: 48 additions & 0 deletions playground/typed-routes/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"name": "typed-routes",
"version": "1.0.0",
"description": "Test playground for typed routes with linkFor",
"main": "index.js",
"type": "module",
"keywords": [],
"author": "",
"license": "MIT",
"private": true,
"scripts": {
"build": "vite build",
"dev": "vite dev",
"dev:init": "rw-scripts dev-init",
"preview": "vite preview",
"worker:run": "rw-scripts worker-run",
"clean": "npm run clean:vite",
"clean:vite": "rm -rf ./node_modules/.vite",
"release": "rw-scripts ensure-deploy-env && npm run clean && npm run build && wrangler deploy",
"generate": "rw-scripts ensure-env && wrangler types",
"check": "npm run generate && npm run types",
"types": "tsc"
},
"dependencies": {
"rwsdk": "workspace:*",
"react": "19.3.0-canary-fb2177c1-20251114",
"react-dom": "19.3.0-canary-fb2177c1-20251114",
"react-server-dom-webpack": "19.3.0-canary-fb2177c1-20251114"
},
"devDependencies": {
"@cloudflare/vite-plugin": "1.15.2",
"@cloudflare/workers-types": "4.20251121.0",
"@types/node": "22.18.8",
"@types/react": "19.1.2",
"@types/react-dom": "19.1.2",
"typescript": "5.9.3",
"vite": "7.2.4",
"vitest": "^3.1.1",
"wrangler": "4.50.0"
},
"pnpm": {
"onlyBuiltDependencies": [
"esbuild",
"sharp",
"workerd"
]
}
}
19 changes: 19 additions & 0 deletions playground/typed-routes/public/favicon-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
23 changes: 23 additions & 0 deletions playground/typed-routes/public/favicon-light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 36 additions & 0 deletions playground/typed-routes/src/app/Document.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import stylesUrl from "./styles.css?url";
export const Document: React.FC<{ children: React.ReactNode }> = ({
children,
}) => (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Typed Routes Playground</title>
<link rel="modulepreload" href="/src/client.tsx" />
<link rel="stylesheet" href={stylesUrl} />
<link
rel="icon"
type="image/svg+xml"
href="/favicon-dark.svg"
media="(prefers-color-scheme: dark)"
/>
<link
rel="icon"
type="image/svg+xml"
href="/favicon-light.svg"
media="(prefers-color-scheme: light)"
/>
</head>
<body>
<div id="root">{children}</div>
<script>import("/src/client.tsx")</script>
</body>
</html>
);






37 changes: 37 additions & 0 deletions playground/typed-routes/src/app/headers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { RouteMiddleware } from "rwsdk/router";

export const setCommonHeaders =
(): RouteMiddleware =>
({ response, rw: { nonce } }) => {
if (!import.meta.env.VITE_IS_DEV_SERVER) {
// Forces browsers to always use HTTPS for a specified time period (2 years)
response.headers.set(
"Strict-Transport-Security",
"max-age=63072000; includeSubDomains; preload",
);
}

// Forces browser to use the declared content-type instead of trying to guess/sniff it
response.headers.set("X-Content-Type-Options", "nosniff");

// Stops browsers from sending the referring webpage URL in HTTP headers
response.headers.set("Referrer-Policy", "no-referrer");

// Explicitly disables access to specific browser features/APIs
response.headers.set(
"Permissions-Policy",
"geolocation=(), microphone=(), camera=()",
);

// Defines trusted sources for content loading and script execution:
response.headers.set(
"Content-Security-Policy",
`default-src 'self'; script-src 'self' 'nonce-${nonce}' https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline'; frame-ancestors 'self'; frame-src 'self' https://challenges.cloudflare.com https://rwsdk.com; object-src 'none';`,
);
};






45 changes: 45 additions & 0 deletions playground/typed-routes/src/app/pages/BlogPost.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { link } from "@/app/shared/links";
import { RequestInfo } from "rwsdk/worker";

export function BlogPost({ ctx, request }: RequestInfo) {
const url = new URL(request.url);
const pathParts = url.pathname.split("/").filter(Boolean);
const year = pathParts[1]; // Extract from /blog/:year/:slug
const slug = pathParts[2];

// Test linking back to home
const homeLink = link("/");
// Test linking to another blog post
const otherPostLink = link("/blog/:year/:slug", {
year: "2025",
slug: "new-post",
});

return (
<div className="page">
<h1>Blog Post</h1>
<p>
Viewing post: <strong>{slug}</strong> from year <strong>{year}</strong>
</p>

<nav>
<a href={homeLink}>Home</a>
<a href={otherPostLink}>Another Post</a>
</nav>

<div className="code">
<div>
Current post: {year}/{slug}
</div>
<div>Home link: {homeLink}</div>
<div>Other post link: {otherPostLink}</div>
</div>
</div>
);
}






38 changes: 38 additions & 0 deletions playground/typed-routes/src/app/pages/FileViewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { link } from "@/app/shared/links";
import { RequestInfo } from "rwsdk/worker";

export function FileViewer({ ctx, request }: RequestInfo) {
const url = new URL(request.url);
const filePath = url.pathname.replace("/files/", ""); // Extract from /files/*

// Test linking back to home
const homeLink = link("/");
// Test linking to another file
const otherFileLink = link("/files/*", { $0: "images/photo.jpg" });

return (
<div className="page">
<h1>File Viewer</h1>
<p>
Viewing file: <strong>{filePath}</strong>
</p>

<nav>
<a href={homeLink}>Home</a>
<a href={otherFileLink}>Another File</a>
</nav>

<div className="code">
<div>Current file: {filePath}</div>
<div>Home link: {homeLink}</div>
<div>Other file link: {otherFileLink}</div>
</div>
</div>
);
}






58 changes: 58 additions & 0 deletions playground/typed-routes/src/app/pages/Home.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { link } from "@/app/shared/links";
import { RequestInfo } from "rwsdk/worker";

export function Home({ ctx }: RequestInfo) {
// Test static route
const homeLink = link("/");

// Test routes with parameters
const userLink = link("/users/:id", { id: "123" });
const fileLink = link("/files/*", { $0: "documents/readme.md" });
const blogLink = link("/blog/:year/:slug", {
year: "2024",
slug: "hello-world",
});

// TypeScript correctly catches invalid routes:
// link("/user/"); // Error: Argument of type '"/user/"' is not assignable to parameter of type '"/" | "/users/:id" | "/files/*" | "/blog/:year/:slug"'

return (
<div className="page">
<h1>Typed Routes Playground</h1>
<p>
This playground tests typed routes with automatic route inference using{" "}
<code>linkFor</code>.
</p>

<nav>
<a href={homeLink}>Home</a>
<a href={userLink}>User Profile (ID: 123)</a>
<a href={fileLink}>File Viewer</a>
<a href={blogLink}>Blog Post</a>
</nav>

<div className="code">
<div>Home: {homeLink}</div>
<div>User: {userLink}</div>
<div>File: {fileLink}</div>
<div>Blog: {blogLink}</div>
</div>

<h2>Route Types Tested</h2>
<ul>
<li>
<strong>Static route:</strong> <code>/</code>
</li>
<li>
<strong>Named parameter:</strong> <code>/users/:id</code>
</li>
<li>
<strong>Wildcard:</strong> <code>/files/*</code>
</li>
<li>
<strong>Multiple parameters:</strong> <code>/blog/:year/:slug</code>
</li>
</ul>
</div>
);
}
38 changes: 38 additions & 0 deletions playground/typed-routes/src/app/pages/UserProfile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { link } from "@/app/shared/links";
import { RequestInfo } from "rwsdk/worker";

export function UserProfile({ ctx, request }: RequestInfo) {
const url = new URL(request.url);
const userId = url.pathname.split("/")[2]; // Extract from /users/:id

// Test linking back to home
const homeLink = link("/");
// Test linking to another user
const otherUserLink = link("/users/:id", { id: "456" });

return (
<div className="page">
<h1>User Profile</h1>
<p>
Viewing profile for user ID: <strong>{userId}</strong>
</p>

<nav>
<a href={homeLink}>Home</a>
<a href={otherUserLink}>Another User (ID: 456)</a>
</nav>

<div className="code">
<div>Current user: {userId}</div>
<div>Home link: {homeLink}</div>
<div>Other user link: {otherUserLink}</div>
</div>
</div>
);
}






4 changes: 4 additions & 0 deletions playground/typed-routes/src/app/shared/links.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { linkFor } from "rwsdk/router";
import type { App } from "rwsdk/worker";

export const link = linkFor<App>();
Loading
Loading