Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/build/withNextDeploy.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { NextConfig } from 'next/dist/server/config-shared'
import type { NextConfig } from 'next/types'
import path from 'node:path'
import loadConfig from '../commands/helpers/loadConfig'

Expand Down
6 changes: 6 additions & 0 deletions src/cdk/constructs/CloudFrontDistribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ interface CloudFrontPropsDistribution {
requestEdgeFunction: cloudfront.experimental.EdgeFunction
responseEdgeFunction: cloudfront.experimental.EdgeFunction
viewerResponseEdgeFunction: cloudfront.experimental.EdgeFunction
viewerRequestLambdaEdge: cloudfront.experimental.EdgeFunction
cacheConfig: CacheConfig
imageTTL?: number
}
Expand All @@ -35,6 +36,7 @@ export class CloudFrontDistribution extends Construct {
requestEdgeFunction,
responseEdgeFunction,
viewerResponseEdgeFunction,
viewerRequestLambdaEdge,
cacheConfig,
renderServerDomain,
imageTTL
Expand Down Expand Up @@ -99,6 +101,10 @@ export class CloudFrontDistribution extends Construct {
{
functionVersion: viewerResponseEdgeFunction.currentVersion,
eventType: cloudfront.LambdaEdgeEventType.VIEWER_RESPONSE
},
{
functionVersion: viewerRequestLambdaEdge.currentVersion,
eventType: cloudfront.LambdaEdgeEventType.VIEWER_REQUEST
}
],
cachePolicy: splitCachePolicy
Expand Down
60 changes: 60 additions & 0 deletions src/cdk/constructs/ViewerRequestLambdaEdge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Construct } from 'constructs'
import * as cdk from 'aws-cdk-lib'
import * as lambda from 'aws-cdk-lib/aws-lambda'
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront'
import * as logs from 'aws-cdk-lib/aws-logs'
import * as iam from 'aws-cdk-lib/aws-iam'
import path from 'node:path'
import { buildLambda } from '../../build/edge'
import { NextRedirects } from '../../types'

interface ViewerRequestLambdaEdgeProps extends cdk.StackProps {
buildOutputPath: string
nodejs?: string
redirects?: NextRedirects
}

const NodeJSEnvironmentMapping: Record<string, lambda.Runtime> = {
'18': lambda.Runtime.NODEJS_18_X,
'20': lambda.Runtime.NODEJS_20_X
}

export class ViewerRequestLambdaEdge extends Construct {
public readonly lambdaEdge: cloudfront.experimental.EdgeFunction

constructor(scope: Construct, id: string, props: ViewerRequestLambdaEdgeProps) {
const { nodejs, buildOutputPath } = props
super(scope, id)

const nodeJSEnvironment = NodeJSEnvironmentMapping[nodejs ?? ''] ?? NodeJSEnvironmentMapping['20']
const name = 'viewerRequest'

buildLambda(name, buildOutputPath, {
define: {
'process.env.REDIRECTS': JSON.stringify(props.redirects ?? [])
}
})

const logGroup = new logs.LogGroup(this, 'ViewerRequestLambdaEdgeLogGroup', {
logGroupName: `/aws/lambda/${id}-viewerRequest`,
removalPolicy: cdk.RemovalPolicy.DESTROY,
retention: logs.RetentionDays.ONE_DAY
})

this.lambdaEdge = new cloudfront.experimental.EdgeFunction(this, 'ViewerRequestLambdaEdge', {
runtime: nodeJSEnvironment,
code: lambda.Code.fromAsset(path.join(buildOutputPath, 'server-functions', name)),
handler: 'index.handler',
logGroup
})

logGroup.grantWrite(this.lambdaEdge)

const policyStatement = new iam.PolicyStatement({
actions: ['logs:CreateLogStream', 'logs:PutLogEvents'],
resources: [`${logGroup.logGroupArn}:*`]
})

this.lambdaEdge.addToRolePolicy(policyStatement)
}
}
15 changes: 13 additions & 2 deletions src/cdk/stacks/NextCloudfrontStack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { Construct } from 'constructs'
import * as s3 from 'aws-cdk-lib/aws-s3'
import { OriginRequestLambdaEdge } from '../constructs/OriginRequestLambdaEdge'
import { CloudFrontDistribution } from '../constructs/CloudFrontDistribution'
import { CacheConfig } from '../../types'
import { OriginResponseLambdaEdge } from '../constructs/OriginResponseLambdaEdge'
import { ViewerResponseLambdaEdge } from '../constructs/ViewerResponseLambdaEdge'
import { ViewerRequestLambdaEdge } from '../constructs/ViewerRequestLambdaEdge'
import { CacheConfig, NextRedirects } from '../../types'

export interface NextCloudfrontStackProps extends StackProps {
nodejs?: string
Expand All @@ -17,12 +18,14 @@ export interface NextCloudfrontStackProps extends StackProps {
buildOutputPath: string
cacheConfig: CacheConfig
imageTTL?: number
redirects?: NextRedirects
}

export class NextCloudfrontStack extends Stack {
public readonly originRequestLambdaEdge: OriginRequestLambdaEdge
public readonly originResponseLambdaEdge: OriginResponseLambdaEdge
public readonly viewerResponseLambdaEdge: ViewerResponseLambdaEdge
public readonly viewerRequestLambdaEdge: ViewerRequestLambdaEdge
public readonly cloudfront: CloudFrontDistribution

constructor(scope: Construct, id: string, props: NextCloudfrontStackProps) {
Expand All @@ -36,7 +39,8 @@ export class NextCloudfrontStack extends Stack {
renderWorkerQueueArn,
region,
cacheConfig,
imageTTL
imageTTL,
redirects
} = props

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

this.viewerRequestLambdaEdge = new ViewerRequestLambdaEdge(this, `${id}-ViewerRequestLambdaEdge`, {
buildOutputPath,
nodejs,
redirects
})

this.viewerResponseLambdaEdge = new ViewerResponseLambdaEdge(this, `${id}-ViewerResponseLambdaEdge`, {
nodejs,
buildOutputPath
Expand All @@ -73,6 +83,7 @@ export class NextCloudfrontStack extends Stack {
requestEdgeFunction: this.originRequestLambdaEdge.lambdaEdge,
responseEdgeFunction: this.originResponseLambdaEdge.lambdaEdge,
viewerResponseEdgeFunction: this.viewerResponseLambdaEdge.lambdaEdge,
viewerRequestLambdaEdge: this.viewerRequestLambdaEdge.lambdaEdge,
cacheConfig,
imageTTL
})
Expand Down
4 changes: 2 additions & 2 deletions src/commands/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ interface BootstrapProps {
profile?: string
}

const runTask = (command: string, env: Record<string, string | undefined>) => {
const runTask = (command: string, env: NodeJS.ProcessEnv) => {
const task = childProcess.spawn(command, {
env: env,
env,
shell: true,
stdio: 'pipe'
})
Expand Down
5 changes: 4 additions & 1 deletion src/commands/deploy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ElasticBeanstalk } from '@aws-sdk/client-elastic-beanstalk'
import { S3 } from '@aws-sdk/client-s3'
import { CloudFront } from '@aws-sdk/client-cloudfront'
import type { NextConfig } from 'next/types'
import fs from 'node:fs'
import childProcess from 'node:child_process'
import path from 'node:path'
Expand Down Expand Up @@ -83,7 +84,8 @@ export const deploy = async (config: DeployConfig) => {

const cacheConfig = await loadConfig()

const nextConfig = await loadFile(projectSettings.nextConfigPath)
const nextConfig = (await loadFile(projectSettings.nextConfigPath)) as NextConfig
const nextRedirects = nextConfig.redirects ? await nextConfig.redirects() : undefined

const outputPath = createOutputFolder()

Expand Down Expand Up @@ -148,6 +150,7 @@ export const deploy = async (config: DeployConfig) => {
region,
cacheConfig,
imageTTL: nextConfig.imageTTL,
redirects: nextRedirects,
env: {
region: AWS_EDGE_REGION // required since Edge can be deployed only here.
}
Expand Down
33 changes: 33 additions & 0 deletions src/lambdas/viewerRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type { CloudFrontRequestCallback, Context, CloudFrontResponseEvent } from 'aws-lambda'
import type { NextRedirects } from '../types'

/**
* AWS Lambda@Edge Viewer Request handler for Next.js redirects
* This function processes CloudFront viewer requests and handles redirects configured in Next.js
*
* @param {CloudFrontResponseEvent} event - The CloudFront event object containing request details
* @param {Context} _context - AWS Lambda Context object (unused)
* @param {CloudFrontRequestCallback} callback - Callback function to return the response
* @returns {Promise<void>} - Returns either a redirect response or the original request
*/
export const handler = async (
event: CloudFrontResponseEvent,
_context: Context,
callback: CloudFrontRequestCallback
) => {
const request = event.Records[0].cf.request
const redirectsConfig = process.env.REDIRECTS as unknown as NextRedirects

const redirect = redirectsConfig.find((r) => r.source === request.uri)

if (redirect) {
return callback(null, {
status: redirect.statusCode ? String(redirect.statusCode) : redirect.permanent ? '308' : '307',
headers: {
location: [{ key: 'Location', value: redirect.destination }]
}
})
}

return callback(null, request)
}
4 changes: 4 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import type { NextConfig } from 'next/types'

export interface CacheConfig {
noCacheRoutes?: string[]
cacheCookies?: string[]
cacheQueries?: string[]
enableDeviceSplit?: boolean
}

export type NextRedirects = Awaited<ReturnType<Required<NextConfig>['redirects']>>