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
10 changes: 7 additions & 3 deletions src/cdk/constructs/ViewerRequestLambdaEdge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ 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'
import { NextRedirects, DeployConfig } from '../../types'

interface ViewerRequestLambdaEdgeProps extends cdk.StackProps {
buildOutputPath: string
nodejs?: string
redirects?: NextRedirects
internationalizationConfig?: DeployConfig['internationalization']
trailingSlash?: boolean
}

const NodeJSEnvironmentMapping: Record<string, lambda.Runtime> = {
Expand All @@ -23,15 +25,17 @@ export class ViewerRequestLambdaEdge extends Construct {
public readonly lambdaEdge: cloudfront.experimental.EdgeFunction

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

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

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

Expand Down
20 changes: 12 additions & 8 deletions src/cdk/stacks/NextCloudfrontStack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { CloudFrontDistribution } from '../constructs/CloudFrontDistribution'
import { OriginResponseLambdaEdge } from '../constructs/OriginResponseLambdaEdge'
import { ViewerResponseLambdaEdge } from '../constructs/ViewerResponseLambdaEdge'
import { ViewerRequestLambdaEdge } from '../constructs/ViewerRequestLambdaEdge'
import { CacheConfig, NextRedirects } from '../../types'
import { DeployConfig, NextRedirects } from '../../types'

export interface NextCloudfrontStackProps extends StackProps {
nodejs?: string
Expand All @@ -16,9 +16,10 @@ export interface NextCloudfrontStackProps extends StackProps {
renderWorkerQueueUrl: string
renderWorkerQueueArn: string
buildOutputPath: string
cacheConfig: CacheConfig
deployConfig: DeployConfig
imageTTL?: number
redirects?: NextRedirects
trailingSlash?: boolean
}

export class NextCloudfrontStack extends Stack {
Expand All @@ -38,33 +39,36 @@ export class NextCloudfrontStack extends Stack {
renderWorkerQueueUrl,
renderWorkerQueueArn,
region,
cacheConfig,
deployConfig,
imageTTL,
redirects
redirects,
trailingSlash = false
} = props

this.originRequestLambdaEdge = new OriginRequestLambdaEdge(this, `${id}-OriginRequestLambdaEdge`, {
nodejs,
bucketName: staticBucketName,
renderServerDomain,
buildOutputPath,
cacheConfig,
cacheConfig: deployConfig.cache,
bucketRegion: region
})

this.originResponseLambdaEdge = new OriginResponseLambdaEdge(this, `${id}-OriginResponseLambdaEdge`, {
nodejs,
renderWorkerQueueUrl,
buildOutputPath,
cacheConfig,
cacheConfig: deployConfig.cache,
renderWorkerQueueArn,
region
})

this.viewerRequestLambdaEdge = new ViewerRequestLambdaEdge(this, `${id}-ViewerRequestLambdaEdge`, {
buildOutputPath,
nodejs,
redirects
redirects,
internationalizationConfig: deployConfig.internationalization,
trailingSlash
})

this.viewerResponseLambdaEdge = new ViewerResponseLambdaEdge(this, `${id}-ViewerResponseLambdaEdge`, {
Expand All @@ -84,7 +88,7 @@ export class NextCloudfrontStack extends Stack {
responseEdgeFunction: this.originResponseLambdaEdge.lambdaEdge,
viewerResponseEdgeFunction: this.viewerResponseLambdaEdge.lambdaEdge,
viewerRequestLambdaEdge: this.viewerRequestLambdaEdge.lambdaEdge,
cacheConfig,
cacheConfig: deployConfig.cache,
imageTTL
})

Expand Down
5 changes: 3 additions & 2 deletions src/commands/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export const deploy = async (config: DeployConfig) => {
throw new Error('Was not able to find project settings.')
}

const cacheConfig = await loadConfig()
const deployConfig = await loadConfig()

const nextConfig = (await loadFile(projectSettings.nextConfigPath)) as NextConfig
const nextRedirects = nextConfig.redirects ? await nextConfig.redirects() : undefined
Expand Down Expand Up @@ -148,8 +148,9 @@ export const deploy = async (config: DeployConfig) => {
buildOutputPath: outputPath,
crossRegionReferences: true,
region,
cacheConfig,
deployConfig,
imageTTL: nextConfig.imageTTL,
trailingSlash: nextConfig.trailingSlash,
redirects: nextRedirects,
env: {
region: AWS_EDGE_REGION // required since Edge can be deployed only here.
Expand Down
13 changes: 7 additions & 6 deletions src/commands/helpers/createConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import { findConfig } from './loadConfig'

const CONFIG_FILE_NAME = 'next-serverless.config.js'
const CONFIG_TEMPLATE = `/**
* @type {import('@dbbs/next-serverless-deployment').CacheConfig}
* @type {import('@dbbs/next-serverless-deployment').DeployConfig}
*/
const config = {
noCacheRoutes: [],
cacheCookies: [],
cacheQueries: [],
enableDeviceSplit: false
}
cache: {
noCacheRoutes: [],
cacheCookies: [],
cacheQueries: [],
enableDeviceSplit: false}
}

module.exports = config
`
Expand Down
4 changes: 2 additions & 2 deletions src/commands/helpers/loadConfig.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import fs from 'node:fs'
import path from 'node:path'
import { CacheConfig } from '../../types'
import type { DeployConfig } from '../../types'

export const findConfig = (configPath: string): string | undefined => {
return ['next-serverless.config.js', 'next-serverless.config.mjs', 'next-serverless.config.ts'].find((config) =>
fs.existsSync(path.join(configPath, config))
)
}

async function loadConfig(): Promise<CacheConfig> {
async function loadConfig(): Promise<DeployConfig> {
try {
const serverConfig = findConfig(process.cwd())

Expand Down
48 changes: 44 additions & 4 deletions src/lambdas/viewerRequest.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { CloudFrontRequestCallback, Context, CloudFrontResponseEvent } from 'aws-lambda'
import type { NextRedirects } from '../types'
import type { NextRedirects, DeployConfig } from '../types'
import path from 'node:path'

/**
* AWS Lambda@Edge Viewer Request handler for Next.js redirects
Expand All @@ -17,14 +18,53 @@ export const handler = async (
) => {
const request = event.Records[0].cf.request
const redirectsConfig = process.env.REDIRECTS as unknown as NextRedirects
const localesConfig = process.env.LOCALES_CONFIG as unknown as DeployConfig['internationalization'] | null
const isTrailingSlash = process.env.IS_TRAILING_SLASH as unknown as boolean
const pathHasTrailingSlash = request.uri.endsWith('/')

const redirect = redirectsConfig.find((r) => r.source === request.uri)
if (pathHasTrailingSlash && !isTrailingSlash) {
request.uri = request.uri.slice(0, -1)
} else if (!pathHasTrailingSlash && isTrailingSlash) {
request.uri += '/'
}

let shouldRedirectWithLocale = false
let pagePath = request.uri
let locale = ''
let redirectTo = ''
let redirectStatus = '307'

if (localesConfig) {
const [requestLocale, ...restPath] = request.uri.substring(1).split('/')
shouldRedirectWithLocale = !localesConfig.locales.find((locale) => locale === requestLocale)

if (!shouldRedirectWithLocale) {
pagePath = `/${restPath.join('/')}`
locale = requestLocale
} else {
locale = localesConfig.defaultLocale
}
}

const redirect = redirectsConfig.find((r) => r.source === pagePath)

if (redirect) {
redirectTo = locale ? `/${path.join(locale, redirect.destination)}` : redirect.destination
redirectStatus = redirect.statusCode ? String(redirect.statusCode) : redirect.permanent ? '308' : '307'
} else if (shouldRedirectWithLocale) {
redirectTo = `/${path.join(locale, pagePath)}`
}

if (redirectTo) {
return callback(null, {
status: redirect.statusCode ? String(redirect.statusCode) : redirect.permanent ? '308' : '307',
status: redirectStatus,
headers: {
location: [{ key: 'Location', value: redirect.destination }]
location: [
{
key: 'Location',
value: `${redirectTo}${request.querystring ? `?${request.querystring}` : ''}`
}
]
}
})
}
Expand Down
8 changes: 8 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,12 @@ export interface CacheConfig {
enableDeviceSplit?: boolean
}

export interface DeployConfig {
internationalization?: {
locales: string[]
defaultLocale: string
}
cache: CacheConfig
}

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