Skip to content

Commit 3331e94

Browse files
authored
Merge pull request #900 from redwoodjs/pp-test-route-types
Add typed links playground
2 parents 75dd0e3 + a5a3533 commit 3331e94

File tree

24 files changed

+950
-53
lines changed

24 files changed

+950
-53
lines changed

playground/typed-routes/README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Typed Routes Playground
2+
3+
This playground demonstrates and tests the typed routes functionality with `linkFor` that automatically infers routes from the app definition.
4+
5+
## Features Tested
6+
7+
- Static routes (e.g., `/`)
8+
- Routes with named parameters (e.g., `/users/:id`)
9+
- Routes with wildcards (e.g., `/files/*`)
10+
- Type-safe link generation with automatic route inference
11+
- Parameter validation at compile-time and runtime
12+
13+
## Running the dev server
14+
15+
```shell
16+
npm run dev
17+
```
18+
19+
Point your browser to the URL displayed in the terminal (e.g. `http://localhost:5173/`).
20+
21+
## Testing
22+
23+
```shell
24+
pnpm tsc --noEmit
25+
```
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
"name": "typed-routes",
3+
"version": "1.0.0",
4+
"description": "Test playground for typed routes with linkFor",
5+
"main": "index.js",
6+
"type": "module",
7+
"keywords": [],
8+
"author": "",
9+
"license": "MIT",
10+
"private": true,
11+
"scripts": {
12+
"build": "vite build",
13+
"dev": "vite dev",
14+
"dev:init": "rw-scripts dev-init",
15+
"preview": "vite preview",
16+
"worker:run": "rw-scripts worker-run",
17+
"clean": "npm run clean:vite",
18+
"clean:vite": "rm -rf ./node_modules/.vite",
19+
"release": "rw-scripts ensure-deploy-env && npm run clean && npm run build && wrangler deploy",
20+
"generate": "rw-scripts ensure-env && wrangler types",
21+
"check": "npm run generate && npm run types",
22+
"types": "tsc"
23+
},
24+
"dependencies": {
25+
"rwsdk": "workspace:*",
26+
"react": "19.3.0-canary-fb2177c1-20251114",
27+
"react-dom": "19.3.0-canary-fb2177c1-20251114",
28+
"react-server-dom-webpack": "19.3.0-canary-fb2177c1-20251114"
29+
},
30+
"devDependencies": {
31+
"@cloudflare/vite-plugin": "1.15.2",
32+
"@cloudflare/workers-types": "4.20251121.0",
33+
"@types/node": "22.18.8",
34+
"@types/react": "19.1.2",
35+
"@types/react-dom": "19.1.2",
36+
"typescript": "5.9.3",
37+
"vite": "7.2.4",
38+
"vitest": "^3.1.1",
39+
"wrangler": "4.50.0"
40+
},
41+
"pnpm": {
42+
"onlyBuiltDependencies": [
43+
"esbuild",
44+
"sharp",
45+
"workerd"
46+
]
47+
}
48+
}
Lines changed: 19 additions & 0 deletions
Loading
Lines changed: 23 additions & 0 deletions
Loading
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import stylesUrl from "./styles.css?url";
2+
export const Document: React.FC<{ children: React.ReactNode }> = ({
3+
children,
4+
}) => (
5+
<html lang="en">
6+
<head>
7+
<meta charSet="utf-8" />
8+
<meta name="viewport" content="width=device-width, initial-scale=1" />
9+
<title>Typed Routes Playground</title>
10+
<link rel="modulepreload" href="/src/client.tsx" />
11+
<link rel="stylesheet" href={stylesUrl} />
12+
<link
13+
rel="icon"
14+
type="image/svg+xml"
15+
href="/favicon-dark.svg"
16+
media="(prefers-color-scheme: dark)"
17+
/>
18+
<link
19+
rel="icon"
20+
type="image/svg+xml"
21+
href="/favicon-light.svg"
22+
media="(prefers-color-scheme: light)"
23+
/>
24+
</head>
25+
<body>
26+
<div id="root">{children}</div>
27+
<script>import("/src/client.tsx")</script>
28+
</body>
29+
</html>
30+
);
31+
32+
33+
34+
35+
36+
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { RouteMiddleware } from "rwsdk/router";
2+
3+
export const setCommonHeaders =
4+
(): RouteMiddleware =>
5+
({ response, rw: { nonce } }) => {
6+
if (!import.meta.env.VITE_IS_DEV_SERVER) {
7+
// Forces browsers to always use HTTPS for a specified time period (2 years)
8+
response.headers.set(
9+
"Strict-Transport-Security",
10+
"max-age=63072000; includeSubDomains; preload",
11+
);
12+
}
13+
14+
// Forces browser to use the declared content-type instead of trying to guess/sniff it
15+
response.headers.set("X-Content-Type-Options", "nosniff");
16+
17+
// Stops browsers from sending the referring webpage URL in HTTP headers
18+
response.headers.set("Referrer-Policy", "no-referrer");
19+
20+
// Explicitly disables access to specific browser features/APIs
21+
response.headers.set(
22+
"Permissions-Policy",
23+
"geolocation=(), microphone=(), camera=()",
24+
);
25+
26+
// Defines trusted sources for content loading and script execution:
27+
response.headers.set(
28+
"Content-Security-Policy",
29+
`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';`,
30+
);
31+
};
32+
33+
34+
35+
36+
37+
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { link } from "@/app/shared/links";
2+
import { RequestInfo } from "rwsdk/worker";
3+
4+
export function BlogPost({ ctx, request }: RequestInfo) {
5+
const url = new URL(request.url);
6+
const pathParts = url.pathname.split("/").filter(Boolean);
7+
const year = pathParts[1]; // Extract from /blog/:year/:slug
8+
const slug = pathParts[2];
9+
10+
// Test linking back to home
11+
const homeLink = link("/");
12+
// Test linking to another blog post
13+
const otherPostLink = link("/blog/:year/:slug", {
14+
year: "2025",
15+
slug: "new-post",
16+
});
17+
18+
return (
19+
<div className="page">
20+
<h1>Blog Post</h1>
21+
<p>
22+
Viewing post: <strong>{slug}</strong> from year <strong>{year}</strong>
23+
</p>
24+
25+
<nav>
26+
<a href={homeLink}>Home</a>
27+
<a href={otherPostLink}>Another Post</a>
28+
</nav>
29+
30+
<div className="code">
31+
<div>
32+
Current post: {year}/{slug}
33+
</div>
34+
<div>Home link: {homeLink}</div>
35+
<div>Other post link: {otherPostLink}</div>
36+
</div>
37+
</div>
38+
);
39+
}
40+
41+
42+
43+
44+
45+
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { link } from "@/app/shared/links";
2+
import { RequestInfo } from "rwsdk/worker";
3+
4+
export function FileViewer({ ctx, request }: RequestInfo) {
5+
const url = new URL(request.url);
6+
const filePath = url.pathname.replace("/files/", ""); // Extract from /files/*
7+
8+
// Test linking back to home
9+
const homeLink = link("/");
10+
// Test linking to another file
11+
const otherFileLink = link("/files/*", { $0: "images/photo.jpg" });
12+
13+
return (
14+
<div className="page">
15+
<h1>File Viewer</h1>
16+
<p>
17+
Viewing file: <strong>{filePath}</strong>
18+
</p>
19+
20+
<nav>
21+
<a href={homeLink}>Home</a>
22+
<a href={otherFileLink}>Another File</a>
23+
</nav>
24+
25+
<div className="code">
26+
<div>Current file: {filePath}</div>
27+
<div>Home link: {homeLink}</div>
28+
<div>Other file link: {otherFileLink}</div>
29+
</div>
30+
</div>
31+
);
32+
}
33+
34+
35+
36+
37+
38+
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { link } from "@/app/shared/links";
2+
import { RequestInfo } from "rwsdk/worker";
3+
4+
export function Home({ ctx }: RequestInfo) {
5+
// Test static route
6+
const homeLink = link("/");
7+
8+
// Test routes with parameters
9+
const userLink = link("/users/:id", { id: "123" });
10+
const fileLink = link("/files/*", { $0: "documents/readme.md" });
11+
const blogLink = link("/blog/:year/:slug", {
12+
year: "2024",
13+
slug: "hello-world",
14+
});
15+
16+
// TypeScript correctly catches invalid routes:
17+
// link("/user/"); // Error: Argument of type '"/user/"' is not assignable to parameter of type '"/" | "/users/:id" | "/files/*" | "/blog/:year/:slug"'
18+
19+
return (
20+
<div className="page">
21+
<h1>Typed Routes Playground</h1>
22+
<p>
23+
This playground tests typed routes with automatic route inference using{" "}
24+
<code>linkFor</code>.
25+
</p>
26+
27+
<nav>
28+
<a href={homeLink}>Home</a>
29+
<a href={userLink}>User Profile (ID: 123)</a>
30+
<a href={fileLink}>File Viewer</a>
31+
<a href={blogLink}>Blog Post</a>
32+
</nav>
33+
34+
<div className="code">
35+
<div>Home: {homeLink}</div>
36+
<div>User: {userLink}</div>
37+
<div>File: {fileLink}</div>
38+
<div>Blog: {blogLink}</div>
39+
</div>
40+
41+
<h2>Route Types Tested</h2>
42+
<ul>
43+
<li>
44+
<strong>Static route:</strong> <code>/</code>
45+
</li>
46+
<li>
47+
<strong>Named parameter:</strong> <code>/users/:id</code>
48+
</li>
49+
<li>
50+
<strong>Wildcard:</strong> <code>/files/*</code>
51+
</li>
52+
<li>
53+
<strong>Multiple parameters:</strong> <code>/blog/:year/:slug</code>
54+
</li>
55+
</ul>
56+
</div>
57+
);
58+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { link } from "@/app/shared/links";
2+
import { RequestInfo } from "rwsdk/worker";
3+
4+
export function UserProfile({ ctx, request }: RequestInfo) {
5+
const url = new URL(request.url);
6+
const userId = url.pathname.split("/")[2]; // Extract from /users/:id
7+
8+
// Test linking back to home
9+
const homeLink = link("/");
10+
// Test linking to another user
11+
const otherUserLink = link("/users/:id", { id: "456" });
12+
13+
return (
14+
<div className="page">
15+
<h1>User Profile</h1>
16+
<p>
17+
Viewing profile for user ID: <strong>{userId}</strong>
18+
</p>
19+
20+
<nav>
21+
<a href={homeLink}>Home</a>
22+
<a href={otherUserLink}>Another User (ID: 456)</a>
23+
</nav>
24+
25+
<div className="code">
26+
<div>Current user: {userId}</div>
27+
<div>Home link: {homeLink}</div>
28+
<div>Other user link: {otherUserLink}</div>
29+
</div>
30+
</div>
31+
);
32+
}
33+
34+
35+
36+
37+
38+

0 commit comments

Comments
 (0)