Skip to content

Commit 32cee46

Browse files
feat: added support of next redirects
1 parent 52ae047 commit 32cee46

File tree

8 files changed

+123
-6
lines changed

8 files changed

+123
-6
lines changed

src/build/withNextDeploy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { NextConfig } from 'next/dist/server/config-shared'
1+
import type { NextConfig } from 'next/types'
22
import path from 'node:path'
33
import loadConfig from '../commands/helpers/loadConfig'
44

src/cdk/constructs/CloudFrontDistribution.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ interface CloudFrontPropsDistribution {
1313
requestEdgeFunction: cloudfront.experimental.EdgeFunction
1414
responseEdgeFunction: cloudfront.experimental.EdgeFunction
1515
viewerResponseEdgeFunction: cloudfront.experimental.EdgeFunction
16+
viewerRequestLambdaEdge: cloudfront.experimental.EdgeFunction
1617
cacheConfig: CacheConfig
1718
imageTTL?: number
1819
}
@@ -35,6 +36,7 @@ export class CloudFrontDistribution extends Construct {
3536
requestEdgeFunction,
3637
responseEdgeFunction,
3738
viewerResponseEdgeFunction,
39+
viewerRequestLambdaEdge,
3840
cacheConfig,
3941
renderServerDomain,
4042
imageTTL
@@ -99,6 +101,10 @@ export class CloudFrontDistribution extends Construct {
99101
{
100102
functionVersion: viewerResponseEdgeFunction.currentVersion,
101103
eventType: cloudfront.LambdaEdgeEventType.VIEWER_RESPONSE
104+
},
105+
{
106+
functionVersion: viewerRequestLambdaEdge.currentVersion,
107+
eventType: cloudfront.LambdaEdgeEventType.VIEWER_REQUEST
102108
}
103109
],
104110
cachePolicy: splitCachePolicy
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { Construct } from 'constructs'
2+
import * as cdk from 'aws-cdk-lib'
3+
import * as lambda from 'aws-cdk-lib/aws-lambda'
4+
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront'
5+
import * as logs from 'aws-cdk-lib/aws-logs'
6+
import * as iam from 'aws-cdk-lib/aws-iam'
7+
import path from 'node:path'
8+
import { buildLambda } from '../../build/edge'
9+
import { NextRedirects } from '../../types'
10+
11+
interface ViewerRequestLambdaEdgeProps extends cdk.StackProps {
12+
buildOutputPath: string
13+
nodejs?: string
14+
redirects?: NextRedirects
15+
}
16+
17+
const NodeJSEnvironmentMapping: Record<string, lambda.Runtime> = {
18+
'18': lambda.Runtime.NODEJS_18_X,
19+
'20': lambda.Runtime.NODEJS_20_X
20+
}
21+
22+
export class ViewerRequestLambdaEdge extends Construct {
23+
public readonly lambdaEdge: cloudfront.experimental.EdgeFunction
24+
25+
constructor(scope: Construct, id: string, props: ViewerRequestLambdaEdgeProps) {
26+
const { nodejs, buildOutputPath } = props
27+
super(scope, id)
28+
29+
const nodeJSEnvironment = NodeJSEnvironmentMapping[nodejs ?? ''] ?? NodeJSEnvironmentMapping['20']
30+
const name = 'viewerRequest'
31+
32+
buildLambda(name, buildOutputPath, {
33+
define: {
34+
'process.env.REDIRECTS': JSON.stringify(props.redirects ?? [])
35+
}
36+
})
37+
38+
const logGroup = new logs.LogGroup(this, 'ViewerRequestLambdaEdgeLogGroup', {
39+
logGroupName: `/aws/lambda/${id}-viewerRequest`,
40+
removalPolicy: cdk.RemovalPolicy.DESTROY,
41+
retention: logs.RetentionDays.ONE_DAY
42+
})
43+
44+
this.lambdaEdge = new cloudfront.experimental.EdgeFunction(this, 'ViewerRequestLambdaEdge', {
45+
runtime: nodeJSEnvironment,
46+
code: lambda.Code.fromAsset(path.join(buildOutputPath, 'server-functions', name)),
47+
handler: 'index.handler',
48+
logGroup
49+
})
50+
51+
logGroup.grantWrite(this.lambdaEdge)
52+
53+
const policyStatement = new iam.PolicyStatement({
54+
actions: ['logs:CreateLogStream', 'logs:PutLogEvents'],
55+
resources: [`${logGroup.logGroupArn}:*`]
56+
})
57+
58+
this.lambdaEdge.addToRolePolicy(policyStatement)
59+
}
60+
}

src/cdk/stacks/NextCloudfrontStack.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import { Construct } from 'constructs'
33
import * as s3 from 'aws-cdk-lib/aws-s3'
44
import { OriginRequestLambdaEdge } from '../constructs/OriginRequestLambdaEdge'
55
import { CloudFrontDistribution } from '../constructs/CloudFrontDistribution'
6-
import { CacheConfig } from '../../types'
76
import { OriginResponseLambdaEdge } from '../constructs/OriginResponseLambdaEdge'
87
import { ViewerResponseLambdaEdge } from '../constructs/ViewerResponseLambdaEdge'
8+
import { ViewerRequestLambdaEdge } from '../constructs/ViewerRequestLambdaEdge'
9+
import { CacheConfig, NextRedirects } from '../../types'
910

1011
export interface NextCloudfrontStackProps extends StackProps {
1112
nodejs?: string
@@ -17,12 +18,14 @@ export interface NextCloudfrontStackProps extends StackProps {
1718
buildOutputPath: string
1819
cacheConfig: CacheConfig
1920
imageTTL?: number
21+
redirects?: NextRedirects
2022
}
2123

2224
export class NextCloudfrontStack extends Stack {
2325
public readonly originRequestLambdaEdge: OriginRequestLambdaEdge
2426
public readonly originResponseLambdaEdge: OriginResponseLambdaEdge
2527
public readonly viewerResponseLambdaEdge: ViewerResponseLambdaEdge
28+
public readonly viewerRequestLambdaEdge: ViewerRequestLambdaEdge
2629
public readonly cloudfront: CloudFrontDistribution
2730

2831
constructor(scope: Construct, id: string, props: NextCloudfrontStackProps) {
@@ -36,7 +39,8 @@ export class NextCloudfrontStack extends Stack {
3639
renderWorkerQueueArn,
3740
region,
3841
cacheConfig,
39-
imageTTL
42+
imageTTL,
43+
redirects
4044
} = props
4145

4246
this.originRequestLambdaEdge = new OriginRequestLambdaEdge(this, `${id}-OriginRequestLambdaEdge`, {
@@ -57,6 +61,12 @@ export class NextCloudfrontStack extends Stack {
5761
region
5862
})
5963

64+
this.viewerRequestLambdaEdge = new ViewerRequestLambdaEdge(this, `${id}-ViewerRequestLambdaEdge`, {
65+
buildOutputPath,
66+
nodejs,
67+
redirects
68+
})
69+
6070
this.viewerResponseLambdaEdge = new ViewerResponseLambdaEdge(this, `${id}-ViewerResponseLambdaEdge`, {
6171
nodejs,
6272
buildOutputPath
@@ -73,6 +83,7 @@ export class NextCloudfrontStack extends Stack {
7383
requestEdgeFunction: this.originRequestLambdaEdge.lambdaEdge,
7484
responseEdgeFunction: this.originResponseLambdaEdge.lambdaEdge,
7585
viewerResponseEdgeFunction: this.viewerResponseLambdaEdge.lambdaEdge,
86+
viewerRequestLambdaEdge: this.viewerRequestLambdaEdge.lambdaEdge,
7687
cacheConfig,
7788
imageTTL
7889
})

src/commands/bootstrap.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ interface BootstrapProps {
1111
profile?: string
1212
}
1313

14-
const runTask = (command: string, env: Record<string, string | undefined>) => {
14+
const runTask = (command: string, env: NodeJS.ProcessEnv) => {
1515
const task = childProcess.spawn(command, {
16-
env: env,
16+
env,
1717
shell: true,
1818
stdio: 'pipe'
1919
})

src/commands/deploy.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ElasticBeanstalk } from '@aws-sdk/client-elastic-beanstalk'
22
import { S3 } from '@aws-sdk/client-s3'
33
import { CloudFront } from '@aws-sdk/client-cloudfront'
4+
import type { NextConfig } from 'next/types'
45
import fs from 'node:fs'
56
import childProcess from 'node:child_process'
67
import path from 'node:path'
@@ -83,7 +84,8 @@ export const deploy = async (config: DeployConfig) => {
8384

8485
const cacheConfig = await loadConfig()
8586

86-
const nextConfig = await loadFile(projectSettings.nextConfigPath)
87+
const nextConfig = (await loadFile(projectSettings.nextConfigPath)) as NextConfig
88+
const nextRedirects = nextConfig.redirects ? await nextConfig.redirects() : undefined
8789

8890
const outputPath = createOutputFolder()
8991

@@ -148,6 +150,7 @@ export const deploy = async (config: DeployConfig) => {
148150
region,
149151
cacheConfig,
150152
imageTTL: nextConfig.imageTTL,
153+
redirects: nextRedirects,
151154
env: {
152155
region: AWS_EDGE_REGION // required since Edge can be deployed only here.
153156
}

src/lambdas/viewerRequest.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { CloudFrontRequestCallback, Context, CloudFrontResponseEvent } from 'aws-lambda'
2+
import type { NextRedirects } from '../types'
3+
4+
/**
5+
* AWS Lambda@Edge Viewer Request handler for Next.js redirects
6+
* This function processes CloudFront viewer requests and handles redirects configured in Next.js
7+
*
8+
* @param {CloudFrontResponseEvent} event - The CloudFront event object containing request details
9+
* @param {Context} _context - AWS Lambda Context object (unused)
10+
* @param {CloudFrontRequestCallback} callback - Callback function to return the response
11+
* @returns {Promise<void>} - Returns either a redirect response or the original request
12+
*/
13+
export const handler = async (
14+
event: CloudFrontResponseEvent,
15+
_context: Context,
16+
callback: CloudFrontRequestCallback
17+
) => {
18+
const request = event.Records[0].cf.request
19+
const redirectsConfig = process.env.REDIRECTS as unknown as NextRedirects
20+
21+
const redirect = redirectsConfig.find((r) => r.source === request.uri)
22+
23+
if (redirect) {
24+
return callback(null, {
25+
status: redirect.statusCode ? String(redirect.statusCode) : redirect.permanent ? '308' : '307',
26+
headers: {
27+
location: [{ key: 'Location', value: redirect.destination }]
28+
}
29+
})
30+
}
31+
32+
return callback(null, request)
33+
}

src/types/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
import type { NextConfig } from 'next/types'
2+
13
export interface CacheConfig {
24
noCacheRoutes?: string[]
35
cacheCookies?: string[]
46
cacheQueries?: string[]
57
enableDeviceSplit?: boolean
68
}
9+
10+
export type NextRedirects = Awaited<ReturnType<Required<NextConfig>['redirects']>>

0 commit comments

Comments
 (0)