Skip to content

Commit 7e71d20

Browse files
committed
rsc start
1 parent 6392918 commit 7e71d20

File tree

2 files changed

+209
-42
lines changed

2 files changed

+209
-42
lines changed

src/adapter/build/routing.ts

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,12 @@ export async function generateRoutingRules(
8888
netlifyAdapterContext: NetlifyAdapterContext,
8989
) {
9090
const hasMiddleware = Boolean(nextAdapterContext.outputs.middleware)
91-
const hasPages = nextAdapterContext.outputs.pages.length !== 0
91+
const hasPages =
92+
nextAdapterContext.outputs.pages.length !== 0 ||
93+
nextAdapterContext.outputs.pagesApi.length !== 0
94+
const hasApp =
95+
nextAdapterContext.outputs.appPages.length !== 0 ||
96+
nextAdapterContext.outputs.appRoutes.length !== 0
9297
const shouldDenormalizeJsonDataForMiddleware =
9398
hasMiddleware && hasPages && nextAdapterContext.config.skipMiddlewareUrlNormalize
9499

@@ -275,6 +280,51 @@ export async function generateRoutingRules(
275280
// non-segment prefetch rsc request rewriting
276281

277282
// full rsc request rewriting
283+
...(hasApp
284+
? [
285+
{
286+
description: 'Normalize RSC requests (index)',
287+
match: {
288+
path: `^${join('/', nextAdapterContext.config.basePath, '/?$')}`,
289+
has: [
290+
{
291+
type: 'header',
292+
key: 'rsc',
293+
value: '1',
294+
},
295+
],
296+
},
297+
apply: {
298+
type: 'rewrite',
299+
destination: `${join('/', nextAdapterContext.config.basePath, '/index.rsc')}`,
300+
headers: {
301+
vary: 'RSC, Next-Router-State-Tree, Next-Router-Prefetch',
302+
},
303+
},
304+
} satisfies RoutingRuleRewrite,
305+
{
306+
description: 'Normalize RSC requests',
307+
match: {
308+
path: `^${join('/', nextAdapterContext.config.basePath, '/((?!.+\\.rsc).+?)(?:/)?$')}`,
309+
has: [
310+
{
311+
type: 'header',
312+
key: 'rsc',
313+
value: '1',
314+
},
315+
],
316+
},
317+
apply: {
318+
type: 'rewrite',
319+
destination: `${join('/', nextAdapterContext.config.basePath, '/$1.rsc')}`,
320+
headers: {
321+
vary: 'RSC, Next-Router-State-Tree, Next-Router-Prefetch',
322+
},
323+
},
324+
} satisfies RoutingRuleRewrite,
325+
]
326+
: []),
327+
278328
{
279329
// originally: { handle: 'filesystem' },
280330
// this is no-op on its own, it's just marker to be able to run subset of routing rules
@@ -304,11 +354,49 @@ export async function generateRoutingRules(
304354

305355
...normalizeNextData, // originally: // normalize _next/data if middleware + pages
306356

307-
// normalize /index.rsc to just /
357+
...(hasApp
358+
? [
359+
{
360+
// originally: normalize /index.rsc to just /
361+
description: 'Normalize index.rsc to just /',
362+
match: {
363+
path: join('/', nextAdapterContext.config.basePath, '/index(\\.action|\\.rsc)'),
364+
},
365+
apply: {
366+
type: 'rewrite',
367+
destination: join('/', nextAdapterContext.config.basePath),
368+
},
369+
} satisfies RoutingRuleRewrite,
370+
]
371+
: []),
308372

309373
// ...convertedRewrites.afterFiles,
310374

311-
// ensure bad rewrites with /.rsc are fixed
375+
...(hasApp
376+
? [
377+
// originally: // ensure bad rewrites with /.rsc are fixed
378+
{
379+
description: 'Ensure index /.rsc is mapped to /index.rsc',
380+
match: {
381+
path: join('/', nextAdapterContext.config.basePath, '/\\.rsc$'),
382+
},
383+
apply: {
384+
type: 'rewrite',
385+
destination: join('/', nextAdapterContext.config.basePath, `/index.rsc`),
386+
},
387+
} satisfies RoutingRuleRewrite,
388+
{
389+
description: 'Ensure index <anything>/.rsc is mapped to <anything>.rsc',
390+
match: {
391+
path: join('/', nextAdapterContext.config.basePath, '(.+)/\\.rsc$'),
392+
},
393+
apply: {
394+
type: 'rewrite',
395+
destination: join('/', nextAdapterContext.config.basePath, `$1.rsc`),
396+
},
397+
} satisfies RoutingRuleRewrite,
398+
]
399+
: []),
312400

313401
{
314402
// originally: { handle: 'resource' },

src/adapter/run/routing.ts

Lines changed: 118 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ type Match = {
2121
has?: {
2222
type: 'header'
2323
key: string
24+
value?: string
2425
}[]
2526
}
2627

@@ -45,6 +46,8 @@ export type RoutingRuleRewrite = RoutingRuleBase & {
4546
statusCode?: 200 | 404 | 500
4647
/** Phases to re-run after matching this rewrite */
4748
rerunRoutingPhases?: RoutingPhase[]
49+
/** Headers to include in the response */
50+
headers?: Record<string, string>
4851
}
4952
}
5053

@@ -80,22 +83,33 @@ function selectRoutingPhasesRules(routingRules: RoutingRule[], phases: RoutingPh
8083

8184
let requestCounter = 0
8285

86+
const NOT_A_FETCH_RESPONSE = Symbol('Not a Fetch Response')
87+
type MaybeResponse = {
88+
response?: Response | undefined
89+
status?: number | undefined
90+
headers?: HeadersInit | undefined
91+
[NOT_A_FETCH_RESPONSE]: true
92+
}
93+
8394
// eslint-disable-next-line max-params
8495
async function match(
8596
request: Request,
8697
context: Context,
8798
routingRules: RoutingRule[],
8899
outputs: NetlifyAdapterContext['preparedOutputs'],
89-
prefix: string,
90-
) {
100+
prefix: string | undefined,
101+
initialResponse: MaybeResponse,
102+
): Promise<MaybeResponse> {
91103
let currentRequest = request
92-
let maybeResponse: Response | undefined
104+
let maybeResponse: MaybeResponse = initialResponse
93105

94106
const currentURL = new URL(currentRequest.url)
95107
let { pathname } = currentURL
96108

97109
for (const rule of routingRules) {
98-
console.log(prefix, 'Evaluating rule:', rule.description ?? JSON.stringify(rule))
110+
if (prefix) {
111+
console.log(prefix, 'Evaluating rule:', rule.description ?? JSON.stringify(rule))
112+
}
99113
if ('match' in rule) {
100114
if ('type' in rule.match) {
101115
if (rule.match.type === 'static-asset-or-function') {
@@ -117,16 +131,26 @@ async function match(
117131
}
118132

119133
if (matchedType) {
120-
console.log(
121-
prefix,
122-
`Matched static asset or function (${matchedType}): ${pathname} -> ${currentRequest.url}`,
123-
)
124-
maybeResponse = await context.next(currentRequest)
134+
if (prefix) {
135+
console.log(
136+
prefix,
137+
`Matched static asset or function (${matchedType}): ${pathname} -> ${currentRequest.url}`,
138+
)
139+
}
140+
maybeResponse = {
141+
...maybeResponse,
142+
response: await context.next(currentRequest),
143+
}
125144
}
126145
} else if (rule.match.type === 'image-cdn' && pathname.startsWith('/.netlify/image/')) {
127-
console.log(prefix, 'Matched image cdn:', pathname)
146+
if (prefix) {
147+
console.log(prefix, 'Matched image cdn:', pathname)
148+
}
128149

129-
maybeResponse = await context.next(currentRequest)
150+
maybeResponse = {
151+
...maybeResponse,
152+
response: await context.next(currentRequest),
153+
}
130154
}
131155
} else if ('apply' in rule) {
132156
const sourceRegexp = new RegExp(rule.match.path)
@@ -135,9 +159,16 @@ async function match(
135159
if (rule.match.has) {
136160
let hasAllMatch = true
137161
for (const condition of rule.match.has) {
138-
if (condition.type === 'header' && !currentRequest.headers.has(condition.key)) {
139-
hasAllMatch = false
140-
break
162+
if (condition.type === 'header') {
163+
if (typeof condition.value === 'undefined') {
164+
if (!currentRequest.headers.has(condition.key)) {
165+
hasAllMatch = false
166+
break
167+
}
168+
} else if (currentRequest.headers.get(condition.key) !== condition.value) {
169+
hasAllMatch = false
170+
break
171+
}
141172
}
142173
}
143174

@@ -146,38 +177,69 @@ async function match(
146177
}
147178
}
148179

180+
if (prefix) {
181+
console.log(prefix, 'Matched rule', pathname, rule)
182+
}
183+
149184
const replaced = pathname.replace(sourceRegexp, rule.apply.destination)
150185

151186
if (rule.apply.type === 'rewrite') {
152187
const destURL = new URL(replaced, currentURL)
153188
currentRequest = new Request(destURL, currentRequest)
154189

190+
if (rule.apply.headers) {
191+
maybeResponse = {
192+
...maybeResponse,
193+
headers: {
194+
...maybeResponse.headers,
195+
...rule.apply.headers,
196+
},
197+
}
198+
}
199+
200+
if (rule.apply.statusCode) {
201+
maybeResponse = {
202+
...maybeResponse,
203+
status: rule.apply.statusCode,
204+
}
205+
}
206+
155207
if (rule.apply.rerunRoutingPhases) {
156208
maybeResponse = await match(
157209
currentRequest,
158210
context,
159211
selectRoutingPhasesRules(routingRules, rule.apply.rerunRoutingPhases),
160212
outputs,
161213
prefix,
214+
maybeResponse,
162215
)
163216
}
164217
} else {
165-
console.log(prefix, `Redirecting ${pathname} to ${replaced}`)
166-
maybeResponse = new Response(null, {
167-
status: rule.apply.statusCode ?? 307,
168-
headers: {
169-
Location: replaced,
170-
},
171-
})
218+
if (prefix) {
219+
console.log(prefix, `Redirecting ${pathname} to ${replaced}`)
220+
}
221+
const status = rule.apply.statusCode ?? 307
222+
maybeResponse = {
223+
...maybeResponse,
224+
status,
225+
response: new Response(null, {
226+
status,
227+
headers: {
228+
Location: replaced,
229+
},
230+
}),
231+
}
172232
}
173233
}
174234
}
175235
}
176236

177-
if (maybeResponse) {
237+
if (maybeResponse?.response) {
238+
// once hit a response short circuit
178239
return maybeResponse
179240
}
180241
}
242+
return maybeResponse
181243
}
182244

183245
export async function runNextRouting(
@@ -191,28 +253,45 @@ export async function runNextRouting(
191253
return
192254
}
193255

194-
const prefix = `[${
195-
request.headers.get('x-nf-request-id') ??
196-
// for ntl serve, we use a combination of timestamp and pid to have a unique id per request as we don't have x-nf-request-id header then
197-
// eslint-disable-next-line no-plusplus
198-
`${Date.now()} - #${process.pid}:${++requestCounter}`
199-
}]`
200-
201-
console.log(prefix, 'Incoming request for routing:', request.url)
256+
const prefix = request.url.includes('_next/static')
257+
? undefined
258+
: `[${
259+
request.headers.get('x-nf-request-id') ??
260+
// for ntl serve, we use a combination of timestamp and pid to have a unique id per request as we don't have x-nf-request-id header then
261+
// eslint-disable-next-line no-plusplus
262+
`${Date.now()} - #${process.pid}:${++requestCounter}`
263+
}]`
264+
265+
if (prefix) {
266+
console.log(prefix, 'Incoming request for routing:', request.url)
267+
}
202268

203269
const currentRequest = new Request(request)
204270
currentRequest.headers.set('x-ntl-routing', '1')
205271

206-
let maybeResponse = await match(currentRequest, context, routingRules, outputs, prefix)
207-
208-
if (!maybeResponse) {
209-
console.log(prefix, 'No route matched - 404ing')
210-
maybeResponse = new Response('Not Found', { status: 404 })
211-
}
272+
const maybeResponse = await match(currentRequest, context, routingRules, outputs, prefix, {
273+
[NOT_A_FETCH_RESPONSE]: true,
274+
})
275+
276+
const response = maybeResponse.response
277+
? new Response(maybeResponse.response.body, {
278+
...maybeResponse.response,
279+
headers: {
280+
...maybeResponse.response.headers,
281+
...maybeResponse.headers,
282+
},
283+
status: maybeResponse.status ?? maybeResponse.response.status ?? 200,
284+
})
285+
: new Response('Not Found', {
286+
status: maybeResponse?.status ?? 404,
287+
headers: maybeResponse?.headers,
288+
})
212289

213290
// for debugging add log prefixes to response headers to make it easy to find logs for a given request
214-
maybeResponse.headers.set('x-ntl-log-prefix', prefix)
215-
console.log(prefix, 'Serving response', maybeResponse.status)
291+
if (prefix) {
292+
response.headers.set('x-ntl-log-prefix', prefix)
293+
console.log(prefix, 'Serving response', response.status)
294+
}
216295

217-
return maybeResponse
296+
return response
218297
}

0 commit comments

Comments
 (0)