Skip to content

Commit b360e4d

Browse files
committed
feat: middlewares can return new Response, and web.rewrites option for rewriting paths
1 parent 9bfd20c commit b360e4d

38 files changed

+2365
-43
lines changed

.claude/settings.local.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
"Bash(mkdir:*)",
1717
"Bash(TEST_ONLY=dev yarn vitest --config ./vitest.config.rolldown.ts --run --reporter=dot --color=false api-rolldown.test.ts)",
1818
"Bash(bun llink:*)",
19-
"Bash(bun:*)"
19+
"Bash(bun:*)",
20+
"WebSearch"
2021
],
2122
"deny": []
2223
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# Testing URL Rewriting Features
2+
3+
This document describes how to test the URL rewriting and enhanced middleware features in One.
4+
5+
## Quick Start
6+
7+
1. **Start the dev server:**
8+
```bash
9+
cd apps/onestack.dev
10+
yarn dev
11+
```
12+
13+
2. **Visit the test page:**
14+
Open http://localhost:6173/test-rewrites
15+
16+
3. **Test subdomain routing:**
17+
Try these URLs in your browser (`.localhost` domains work on modern systems):
18+
- http://app1.localhost:6173
19+
- http://app2.localhost:6173
20+
- http://docs.localhost:6173
21+
22+
## Features to Test
23+
24+
### 1. URL Rewriting
25+
26+
The following rewrite rules are configured in `vite.config.ts`:
27+
28+
```typescript
29+
{
30+
web: {
31+
rewrites: {
32+
'*.localhost': '/subdomain/*',
33+
'docs.localhost': '/docs',
34+
'/old-docs/*': '/docs/*'
35+
}
36+
}
37+
}
38+
```
39+
40+
### 2. Middleware Capabilities
41+
42+
The middleware in `app/_middleware.tsx` demonstrates:
43+
44+
- **Request rewriting**: Modifying the URL before it reaches route handlers
45+
- **Response interception**: Returning responses directly from middleware
46+
- **Subdomain handling**: Detecting and routing based on subdomains
47+
48+
### 3. Link Component Integration
49+
50+
Links with internal paths like `/subdomain/app1` should automatically render with external URLs like `http://app1.localhost` when rewrites are configured.
51+
52+
## Running Integration Tests
53+
54+
```bash
55+
# Run all tests
56+
yarn test
57+
58+
# Run rewrite tests specifically
59+
yarn vitest run rewrite-integration.test.tsx
60+
61+
# Run with debugging (opens browser)
62+
DEBUG=1 yarn vitest run rewrite-integration.test.tsx
63+
```
64+
65+
## Test Files
66+
67+
- **Test page**: `/app/test-rewrites.tsx` - Interactive test page
68+
- **Middleware**: `/app/_middleware.tsx` - Request/response handling
69+
- **Subdomain pages**: `/app/subdomain/[name]/index.tsx` - Dynamic subdomain routing
70+
- **Integration tests**: `/tests/rewrite-integration.test.tsx` - Automated tests
71+
72+
## Manual Testing Checklist
73+
74+
### Basic Functionality
75+
76+
- [ ] Visit http://localhost:6173/test-rewrites
77+
- [ ] Check that current URL is displayed correctly
78+
- [ ] Verify links show their rendered hrefs
79+
80+
### Subdomain Routing
81+
82+
- [ ] Visit http://app1.localhost:6173
83+
- [ ] Verify it shows "Subdomain: app1"
84+
- [ ] Navigate to http://app1.localhost:6173/about
85+
- [ ] Verify navigation between subdomain pages works
86+
87+
### Link Transformation
88+
89+
- [ ] Hover over subdomain links on test page
90+
- [ ] Verify browser shows subdomain URLs in status bar
91+
- [ ] Click subdomain links
92+
- [ ] Verify navigation works correctly
93+
94+
### Middleware Response
95+
96+
- [ ] Click "Test Middleware Response" button
97+
- [ ] Verify JSON response appears
98+
- [ ] Check response contains expected fields
99+
100+
### Path Rewrites
101+
102+
- [ ] Visit http://localhost:6173/old-docs/intro
103+
- [ ] Verify it redirects or rewrites to /docs/intro
104+
105+
## Troubleshooting
106+
107+
### Subdomain Not Resolving
108+
109+
If `*.localhost` doesn't work on your system:
110+
111+
1. **Check your OS**: Modern macOS and Linux support `.localhost` by default
112+
2. **Try 127.0.0.1**: Use `http://127.0.0.1:6173` instead
113+
3. **Add to hosts file** (if needed):
114+
```bash
115+
sudo echo "127.0.0.1 app1.localhost" >> /etc/hosts
116+
sudo echo "127.0.0.1 app2.localhost" >> /etc/hosts
117+
```
118+
119+
### Links Not Transforming
120+
121+
1. Check that rewrites are configured in `vite.config.ts`
122+
2. Verify environment variable is set: `process.env.ONE_URL_REWRITES`
123+
3. Check browser console for errors
124+
4. Ensure you've rebuilt after config changes
125+
126+
### Middleware Not Running
127+
128+
1. Verify `_middleware.tsx` is in the correct location
129+
2. Check that it exports a default function
130+
3. Look for errors in server console
131+
4. Ensure middleware is created with `createMiddleware()`
132+
133+
## Implementation Details
134+
135+
### How It Works
136+
137+
1. **Configuration**: Rewrite rules are defined in `vite.config.ts`
138+
2. **Environment**: Rules are passed to client via `process.env.ONE_URL_REWRITES`
139+
3. **Middleware**: Requests are modified before routing
140+
4. **Links**: The Link component applies reverse rewrites for display
141+
5. **Navigation**: React Navigation handles routing with rewritten paths
142+
143+
### Key Files
144+
145+
- `/packages/one/src/utils/rewrite.ts` - Rewrite utilities
146+
- `/packages/one/src/createMiddleware.ts` - Middleware types
147+
- `/packages/one/src/createHandleRequest.ts` - Request handling
148+
- `/packages/one/src/link/href.ts` - Link URL resolution
149+
- `/packages/one/src/router/getLinkingConfig.ts` - React Navigation integration
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { createMiddleware } from 'one'
2+
3+
/**
4+
* Middleware for handling URL rewrites in onestack.dev
5+
* This handles subdomain routing for testing purposes
6+
*/
7+
export default createMiddleware(async ({ request, next }) => {
8+
const url = new URL(request.url)
9+
const host = request.headers.get('host') || ''
10+
11+
// Handle subdomain rewrites for .localhost domains
12+
if (host.includes('.localhost')) {
13+
const parts = host.split('.')
14+
const subdomain = parts[0]
15+
16+
// Special case for docs.localhost
17+
if (subdomain === 'docs') {
18+
const newUrl = new URL(request.url)
19+
newUrl.pathname = `/docs${url.pathname === '/' ? '' : url.pathname}`
20+
return next(new Request(newUrl, request))
21+
}
22+
23+
// General subdomain handling
24+
if (subdomain && subdomain !== 'www') {
25+
const newUrl = new URL(request.url)
26+
newUrl.pathname = `/subdomain/${subdomain}${url.pathname}`
27+
return next(new Request(newUrl, request))
28+
}
29+
}
30+
31+
// Handle path rewrites
32+
if (url.pathname.startsWith('/old-docs/')) {
33+
const newUrl = new URL(request.url)
34+
newUrl.pathname = url.pathname.replace('/old-docs/', '/docs/')
35+
return next(new Request(newUrl, request))
36+
}
37+
38+
// Test response interception
39+
if (url.pathname === '/test-middleware-response') {
40+
return new Response(JSON.stringify({
41+
message: 'Direct response from middleware',
42+
timestamp: Date.now(),
43+
host,
44+
}), {
45+
status: 200,
46+
headers: { 'Content-Type': 'application/json' }
47+
})
48+
}
49+
50+
return next()
51+
})
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export function GET() {
2+
return Response.json({
3+
status: 'healthy',
4+
timestamp: Date.now(),
5+
middleware: 'rewrite'
6+
})
7+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Link } from 'one'
2+
3+
export default function SubdomainAboutPage({ params }: { params: { name: string } }) {
4+
return (
5+
<div style={{ padding: 20 }}>
6+
<h1>About - {params.name}</h1>
7+
<p>This is the about page for subdomain: {params.name}</p>
8+
9+
<div style={{ marginTop: 20 }}>
10+
<Link href={`/subdomain/${params.name}`}>
11+
← Back to {params.name} home
12+
</Link>
13+
</div>
14+
15+
<div style={{ marginTop: 20 }}>
16+
<h2>Navigation Test</h2>
17+
<p>The link above should render with the subdomain URL.</p>
18+
<p>Current path: /subdomain/{params.name}/about</p>
19+
<p>Should display as: {params.name}.localhost/about</p>
20+
</div>
21+
</div>
22+
)
23+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { Link } from 'one'
2+
3+
export default function SubdomainPage({ params }: { params: { name: string } }) {
4+
return (
5+
<div style={{ padding: 20 }}>
6+
<h1>Subdomain: {params.name}</h1>
7+
<p>This page is served from the rewritten path /subdomain/{params.name}</p>
8+
9+
<div style={{ marginTop: 20 }}>
10+
<h2>Test Links (should show external URLs):</h2>
11+
<ul>
12+
<li>
13+
<Link href={`/subdomain/${params.name}/about`}>
14+
About Page (should render as {params.name}.localhost/about)
15+
</Link>
16+
</li>
17+
<li>
18+
<Link href="/subdomain/other/page">
19+
Other Subdomain (should render as other.localhost/page)
20+
</Link>
21+
</li>
22+
<li>
23+
<Link href="/docs/intro">
24+
Docs (regular link, no rewrite)
25+
</Link>
26+
</li>
27+
</ul>
28+
</div>
29+
30+
<div style={{ marginTop: 20 }}>
31+
<h3>Debug Info:</h3>
32+
<pre>{JSON.stringify({ params }, null, 2)}</pre>
33+
</div>
34+
</div>
35+
)
36+
}
37+
38+
export async function loader({ params }: { params: { name: string } }) {
39+
return {
40+
subdomain: params.name,
41+
timestamp: Date.now(),
42+
message: `Loaded subdomain: ${params.name}`
43+
}
44+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export default function MyAppAboutPage() {
2+
return (
3+
<div>
4+
<h1>About myapp</h1>
5+
<p>This is the about page for myapp subdomain</p>
6+
</div>
7+
)
8+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export default function MyAppSubdomainPage() {
2+
return (
3+
<div>
4+
<h1>myapp Subdomain Home</h1>
5+
<p>This is the home page for myapp subdomain</p>
6+
<a href="/subdomain/myapp/about">About</a>
7+
</div>
8+
)
9+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export default function TestAppAboutPage() {
2+
return (
3+
<div>
4+
<h1>About testapp</h1>
5+
<p>This is the about page for testapp subdomain</p>
6+
</div>
7+
)
8+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export default function TestAppSubdomainPage() {
2+
return (
3+
<div>
4+
<h1>testapp Subdomain Home</h1>
5+
<p>This is the home page for testapp subdomain</p>
6+
<a href="/subdomain/testapp/about">About</a>
7+
</div>
8+
)
9+
}

0 commit comments

Comments
 (0)