Skip to content

Commit 9bbf35d

Browse files
authored
fix: repair docs routing for proxied pages (#601)
1 parent bba6a6f commit 9bbf35d

6 files changed

Lines changed: 242 additions & 7 deletions

File tree

src/lib/docs-routing.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import fs from 'node:fs'
2+
import path from 'node:path'
3+
import { describe, expect, it } from 'vitest'
4+
5+
type Redirect = {
6+
source: string
7+
destination: string
8+
permanent?: boolean
9+
has?: unknown
10+
}
11+
12+
const vercelConfig = JSON.parse(
13+
fs.readFileSync(path.join(process.cwd(), 'vercel.json'), 'utf-8'),
14+
) as { redirects: Redirect[] }
15+
16+
const redirects = vercelConfig.redirects.filter((redirect) => !redirect.has)
17+
18+
function findRedirect(source: string) {
19+
return redirects.find((redirect) => redirect.source === source)
20+
}
21+
22+
describe('docs routing redirects', () => {
23+
it.each([
24+
['/tools', '/docs/tools'],
25+
['/tools/:path*', '/docs/tools/:path*'],
26+
['/partners', '/docs/partners'],
27+
['/api', '/docs/api'],
28+
['/api/authentication', '/docs/api/authentication'],
29+
['/api/conventions', '/docs/api/conventions'],
30+
['/api/errors', '/docs/api/errors'],
31+
['/api/indexer', '/docs/api/indexer'],
32+
['/api/indexer-api', '/docs/api/indexer-api'],
33+
['/api/json-rpc', '/docs/api/json-rpc'],
34+
['/api/pagination', '/docs/api/pagination'],
35+
['/api/rate-limits', '/docs/api/rate-limits'],
36+
['/api/transactions', '/docs/api/transactions'],
37+
['/api/transactions-and-transfers', '/docs/api/transactions-and-transfers'],
38+
['/api/transfers', '/docs/api/transfers'],
39+
['/api/versioning-policy', '/docs/api/versioning-policy'],
40+
])('redirects %s to %s', (source, destination) => {
41+
expect(findRedirect(source)).toMatchObject({
42+
source,
43+
destination,
44+
permanent: true,
45+
})
46+
})
47+
48+
it.each([
49+
['/api/:path*', 'real API functions such as /api/og must stay routable'],
50+
['/api/:path(.*)', 'real API functions such as /api/feedback must stay routable'],
51+
])('does not add broad API docs redirect %s because %s', (source) => {
52+
expect(findRedirect(source)).toBeUndefined()
53+
})
54+
})
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { normalizeProxiedRscFetch } from '../pages/_layout'
3+
import { normalizeRscFetchUrl } from './rsc-route-normalization'
4+
5+
const currentHref = 'https://docs.tempo.xyz/docs/guide/payments/send-a-payment'
6+
const origin = 'https://docs.tempo.xyz'
7+
8+
describe('normalizeRscFetchUrl', () => {
9+
it.each([
10+
[
11+
'keeps cross-origin RSC requests on the current origin',
12+
'https://tempo.xyz/RSC/R/docs/guide/payments.txt?query=',
13+
'https://docs.tempo.xyz/RSC/R/docs/guide/payments.txt?query=',
14+
],
15+
[
16+
'normalizes proxied developers route payloads',
17+
'https://tempo.xyz/RSC/R/developers/docs/tools.txt?query=',
18+
'https://docs.tempo.xyz/RSC/R/docs/tools.txt?query=',
19+
],
20+
[
21+
'normalizes the proxied developers root payload',
22+
'https://tempo.xyz/RSC/R/developers.txt?query=',
23+
'https://docs.tempo.xyz/RSC/R/_root.txt?query=',
24+
],
25+
[
26+
'preserves search and hash fragments',
27+
'https://tempo.xyz/RSC/R/docs/tools.txt?query=abc#flight',
28+
'https://docs.tempo.xyz/RSC/R/docs/tools.txt?query=abc#flight',
29+
],
30+
[
31+
'leaves non-RSC asset requests alone',
32+
'https://tempo.xyz/assets/index.js',
33+
'https://tempo.xyz/assets/index.js',
34+
],
35+
['leaves relative non-RSC requests alone', '/api/og?title=Tools', '/api/og?title=Tools'],
36+
])('%s', (_name, input, expected) => {
37+
expect(normalizeRscFetchUrl(input, currentHref, origin)).toBe(expected)
38+
})
39+
})
40+
41+
describe('normalizeProxiedRscFetch', () => {
42+
it.each([
43+
[
44+
'rewrites cross-origin RSC requests to the current origin',
45+
'https://tempo.xyz/RSC/R/docs/tools.txt?query=',
46+
'https://docs.tempo.xyz/RSC/R/docs/tools.txt?query=',
47+
],
48+
[
49+
'rewrites proxied developers RSC requests',
50+
'https://tempo.xyz/RSC/R/developers/docs/tools.txt?query=',
51+
'https://docs.tempo.xyz/RSC/R/docs/tools.txt?query=',
52+
],
53+
[
54+
'leaves non-RSC requests unchanged',
55+
'https://tempo.xyz/assets/index.js',
56+
'https://tempo.xyz/assets/index.js',
57+
],
58+
])('%s', async (_name, input, expected) => {
59+
const requests: unknown[] = []
60+
const fetch = (request: unknown) => {
61+
requests.push(request)
62+
return Promise.resolve(new Response())
63+
}
64+
Object.defineProperty(globalThis, 'window', {
65+
configurable: true,
66+
value: {
67+
__tempoNormalizeProxiedRscFetch: false,
68+
fetch,
69+
location: {
70+
href: currentHref,
71+
origin,
72+
},
73+
},
74+
})
75+
76+
try {
77+
Function(normalizeProxiedRscFetch)()
78+
await globalThis.window.fetch(input)
79+
expect(requests).toEqual([expected])
80+
} finally {
81+
Reflect.deleteProperty(globalThis, 'window')
82+
}
83+
})
84+
})

src/lib/rsc-route-normalization.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export function normalizeRscFetchUrl(url: string, currentHref: string, origin: string) {
2+
const requestUrl = new URL(url, currentHref)
3+
if (!requestUrl.pathname.startsWith('/RSC/R/')) return url
4+
5+
const pathname = requestUrl.pathname
6+
.replace(/\/RSC\/R\/developers\.txt$/, '/RSC/R/_root.txt')
7+
.replace(/\/RSC\/R\/developers\//, '/RSC/R/')
8+
9+
return new URL(pathname + requestUrl.search + requestUrl.hash, origin).toString()
10+
}

src/pages/_layout.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { PropsWithChildren } from 'react'
22

3-
const normalizeProxiedRscFetch = `
3+
export const normalizeProxiedRscFetch = `
44
(() => {
55
if (window.__tempoNormalizeProxiedRscFetch) return;
66
window.__tempoNormalizeProxiedRscFetch = true;
@@ -11,9 +11,17 @@ const normalizeProxiedRscFetch = `
1111
: input instanceof URL
1212
? input.toString()
1313
: input.url;
14-
const rewritten = url
15-
.replace(/\\/RSC\\/R\\/developers\\.txt(?=($|\\?))/, '/RSC/R/_root.txt')
16-
.replace(/\\/RSC\\/R\\/developers\\//, '/RSC/R/');
14+
const requestUrl = new URL(url, window.location.href);
15+
let rewritten = url;
16+
if (requestUrl.pathname.startsWith('/RSC/R/')) {
17+
const pathname = requestUrl.pathname
18+
.replace(/\\/RSC\\/R\\/developers\\.txt$/, '/RSC/R/_root.txt')
19+
.replace(/\\/RSC\\/R\\/developers\\//, '/RSC/R/');
20+
rewritten = new URL(
21+
pathname + requestUrl.search + requestUrl.hash,
22+
window.location.origin,
23+
).toString();
24+
}
1725
1826
if (rewritten === url) return originalFetch(input, init);
1927
if (typeof input === 'string' || input instanceof URL) return originalFetch(rewritten, init);

src/pages/docs/_layout.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import DocsHeader from '../../components/DocsHeader'
55
import DocsSectionNav from '../../components/DocsSectionNav'
66
import DocsSidebarDrawer from '../../components/DocsSidebarDrawer'
77
import { usePageSettled } from '../../lib/pageSettled'
8+
import { normalizeRscFetchUrl } from '../../lib/rsc-route-normalization'
89

910
const Analytics = lazy(() =>
1011
import('@vercel/analytics/react').then((module) => ({ default: module.Analytics })),
@@ -21,9 +22,7 @@ if (typeof window !== 'undefined') {
2122
window.fetch = (input, init) => {
2223
const url =
2324
typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url
24-
const rewritten = url
25-
.replace(/\/RSC\/R\/developers\.txt(?=($|\?))/, '/RSC/R/_root.txt')
26-
.replace(/\/RSC\/R\/developers\//, '/RSC/R/')
25+
const rewritten = normalizeRscFetchUrl(url, window.location.href, window.location.origin)
2726

2827
if (rewritten === url) return originalFetch(input, init)
2928
if (typeof input === 'string' || input instanceof URL) return originalFetch(rewritten, init)

vercel.json

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,71 @@
5454
"destination": "https://accounts.tempo.xyz/:path*",
5555
"permanent": true
5656
},
57+
{
58+
"source": "/api",
59+
"destination": "/docs/api",
60+
"permanent": true
61+
},
62+
{
63+
"source": "/api/authentication",
64+
"destination": "/docs/api/authentication",
65+
"permanent": true
66+
},
67+
{
68+
"source": "/api/conventions",
69+
"destination": "/docs/api/conventions",
70+
"permanent": true
71+
},
72+
{
73+
"source": "/api/errors",
74+
"destination": "/docs/api/errors",
75+
"permanent": true
76+
},
77+
{
78+
"source": "/api/indexer",
79+
"destination": "/docs/api/indexer",
80+
"permanent": true
81+
},
82+
{
83+
"source": "/api/indexer-api",
84+
"destination": "/docs/api/indexer-api",
85+
"permanent": true
86+
},
87+
{
88+
"source": "/api/json-rpc",
89+
"destination": "/docs/api/json-rpc",
90+
"permanent": true
91+
},
92+
{
93+
"source": "/api/pagination",
94+
"destination": "/docs/api/pagination",
95+
"permanent": true
96+
},
97+
{
98+
"source": "/api/rate-limits",
99+
"destination": "/docs/api/rate-limits",
100+
"permanent": true
101+
},
102+
{
103+
"source": "/api/transactions",
104+
"destination": "/docs/api/transactions",
105+
"permanent": true
106+
},
107+
{
108+
"source": "/api/transactions-and-transfers",
109+
"destination": "/docs/api/transactions-and-transfers",
110+
"permanent": true
111+
},
112+
{
113+
"source": "/api/transfers",
114+
"destination": "/docs/api/transfers",
115+
"permanent": true
116+
},
117+
{
118+
"source": "/api/versioning-policy",
119+
"destination": "/docs/api/versioning-policy",
120+
"permanent": true
121+
},
57122
{
58123
"source": "/guide/bridge-usdc-stargate",
59124
"destination": "/docs/guide/bridge-layerzero",
@@ -149,6 +214,16 @@
149214
"destination": "/docs/wallet/:path*",
150215
"permanent": true
151216
},
217+
{
218+
"source": "/tools",
219+
"destination": "/docs/tools",
220+
"permanent": true
221+
},
222+
{
223+
"source": "/tools/:path*",
224+
"destination": "/docs/tools/:path*",
225+
"permanent": true
226+
},
152227
{
153228
"source": "/ecosystem",
154229
"destination": "/docs/ecosystem",
@@ -184,6 +259,11 @@
184259
"destination": "/docs/changelog",
185260
"permanent": true
186261
},
262+
{
263+
"source": "/partners",
264+
"destination": "/docs/partners",
265+
"permanent": true
266+
},
187267
{
188268
"source": "/learn/partners",
189269
"destination": "/docs/partners",

0 commit comments

Comments
 (0)