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
31 changes: 26 additions & 5 deletions src/build/next.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import fs from 'fs/promises'
import path from 'node:path'
import type { PrerenderManifest, RoutesManifest } from 'next/dist/build'
import { type ProjectPackager, type ProjectSettings } from '../common/project'
import { NextRewrites } from '../types'
import { NextRewrites, NextRedirects } from '../types'

interface BuildOptions {
packager: ProjectPackager
Expand Down Expand Up @@ -66,10 +66,26 @@ const getRewritesConfig = (manifestRules: RoutesManifest['rewrites']): NextRewri
}))
}

const getRedirectsConfig = (manifestRedirects: RoutesManifest['redirects']): NextRedirects => {
if (!manifestRedirects) {
return []
}

return manifestRedirects.map((rule) => ({
source: rule.source,
destination: rule.destination,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
regex: rule.regex, // nextjs still generates for manifest file regex to match the route
statusCode: rule.statusCode ?? 307,
has: rule.has
}))
}

export const getNextCachedRoutesConfig = async (
outputPath: string,
appRelativePath: string
): Promise<{ cachedRoutesMatchers: string[]; rewritesConfig: NextRewrites }> => {
): Promise<{ cachedRoutesMatchers: string[]; rewritesConfig: NextRewrites; redirectsConfig: NextRedirects }> => {
const prerenderManifestJSON = await fs.readFile(
path.join(outputPath, '.next', 'standalone', appRelativePath, '.next', 'prerender-manifest.json'),
'utf-8'
Expand All @@ -95,7 +111,9 @@ export const getNextCachedRoutesConfig = async (

const rewritesConfig = getRewritesConfig(routesManifest.rewrites)

return { cachedRoutesMatchers, rewritesConfig }
const redirectsConfig = getRedirectsConfig(routesManifest.redirects)

return { cachedRoutesMatchers, rewritesConfig, redirectsConfig }
}

export const buildApp = async (options: BuildAppOptions) => {
Expand All @@ -110,7 +128,10 @@ export const buildApp = async (options: BuildAppOptions) => {
const appRelativePath = isMonorepo ? path.relative(root, projectPath) : ''

await copyAssets(outputPath, projectPath, appRelativePath)
const { cachedRoutesMatchers, rewritesConfig } = await getNextCachedRoutesConfig(outputPath, appRelativePath)
const { cachedRoutesMatchers, rewritesConfig, redirectsConfig } = await getNextCachedRoutesConfig(
outputPath,
appRelativePath
)

return { cachedRoutesMatchers, rewritesConfig }
return { cachedRoutesMatchers, rewritesConfig, redirectsConfig }
}
18 changes: 4 additions & 14 deletions src/cdk/constructs/OriginRequestLambdaEdge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ 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 '../../common/esbuild'
import { CacheConfig, NextRewrites } from '../../types'
import { CacheConfig } from '../../types'

interface OriginRequestLambdaEdgeProps extends cdk.StackProps {
bucketName: string
Expand All @@ -16,7 +16,6 @@ interface OriginRequestLambdaEdgeProps extends cdk.StackProps {
cacheConfig: CacheConfig
bucketRegion?: string
cachedRoutesMatchers: string[]
rewritesConfig: NextRewrites
}

const NodeJSEnvironmentMapping: Record<string, lambda.Runtime> = {
Expand All @@ -28,16 +27,8 @@ export class OriginRequestLambdaEdge extends Construct {
public readonly lambdaEdge: cloudfront.experimental.EdgeFunction

constructor(scope: Construct, id: string, props: OriginRequestLambdaEdgeProps) {
const {
bucketName,
bucketRegion,
renderServerDomain,
nodejs,
buildOutputPath,
cacheConfig,
cachedRoutesMatchers,
rewritesConfig
} = props
const { bucketName, bucketRegion, renderServerDomain, nodejs, buildOutputPath, cacheConfig, cachedRoutesMatchers } =
props
super(scope, id)

const nodeJSEnvironment = NodeJSEnvironmentMapping[nodejs ?? ''] ?? NodeJSEnvironmentMapping['20']
Expand All @@ -49,8 +40,7 @@ export class OriginRequestLambdaEdge extends Construct {
'process.env.S3_BUCKET_REGION': JSON.stringify(bucketRegion ?? ''),
'process.env.EB_APP_URL': JSON.stringify(renderServerDomain),
'process.env.CACHE_CONFIG': JSON.stringify(cacheConfig),
'process.env.NEXT_CACHED_ROUTES_MATCHERS': JSON.stringify(cachedRoutesMatchers ?? []),
'process.env.NEXT_REWRITES_CONFIG': JSON.stringify(rewritesConfig ?? [])
'process.env.NEXT_CACHED_ROUTES_MATCHERS': JSON.stringify(cachedRoutesMatchers ?? [])
}
})

Expand Down
4 changes: 2 additions & 2 deletions src/cdk/constructs/RenderServerDistribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ interface RenderServerDistributionProps {
}

const NodeJSEnvironmentMapping: Record<string, string> = {
'18': '64bit Amazon Linux 2023 v6.2.2 running Node.js 18',
'20': '64bit Amazon Linux 2023 v6.2.2 running Node.js 20'
'18': '64bit Amazon Linux 2023 v6.4.3 running Node.js 18',
'20': '64bit Amazon Linux 2023 v6.4.3 running Node.js 20'
}

export class RenderServerDistribution extends Construct {
Expand Down
4 changes: 2 additions & 2 deletions src/cdk/constructs/RenderWorkerDistribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import { addOutput } from '../../common/cdk'
* Maps version numbers to their corresponding Amazon Linux 2023 solution stack names
*/
const NODE_VERSIONS: Record<string, string> = {
'18': '64bit Amazon Linux 2023 v6.2.2 running Node.js 18',
'20': '64bit Amazon Linux 2023 v6.2.2 running Node.js 20'
'18': '64bit Amazon Linux 2023 v6.4.3 running Node.js 18',
'20': '64bit Amazon Linux 2023 v6.4.3 running Node.js 20'
} as const

/**
Expand Down
14 changes: 9 additions & 5 deletions src/cdk/constructs/ViewerRequestLambdaEdge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ 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 '../../common/esbuild'
import { NextRedirects, NextI18nConfig } from '../../types'
import { NextRedirects, NextI18nConfig, NextRewrites } from '../../types'

interface ViewerRequestLambdaEdgeProps extends cdk.StackProps {
buildOutputPath: string
nodejs?: string
redirects?: NextRedirects
redirectsConfig?: NextRedirects
nextI18nConfig?: NextI18nConfig
isTrailingSlashEnabled: boolean
rewritesConfig: NextRewrites
}

const NodeJSEnvironmentMapping: Record<string, lambda.Runtime> = {
Expand All @@ -24,16 +26,18 @@ export class ViewerRequestLambdaEdge extends Construct {
public readonly lambdaEdge: cloudfront.experimental.EdgeFunction

constructor(scope: Construct, id: string, props: ViewerRequestLambdaEdgeProps) {
const { nodejs, buildOutputPath, redirects, nextI18nConfig } = props
const { nodejs, buildOutputPath, redirectsConfig, nextI18nConfig, isTrailingSlashEnabled, rewritesConfig } = props
super(scope, id)

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

buildLambda(name, buildOutputPath, {
define: {
'process.env.REDIRECTS': JSON.stringify(redirects ?? []),
'process.env.LOCALES_CONFIG': JSON.stringify(nextI18nConfig ?? null)
'process.env.REDIRECTS': JSON.stringify(redirectsConfig ?? []),
'process.env.LOCALES_CONFIG': JSON.stringify(nextI18nConfig ?? null),
'process.env.IS_TRAILING_SLASH_ENABLED': JSON.stringify(isTrailingSlashEnabled),
'process.env.NEXT_REWRITES_CONFIG': JSON.stringify(rewritesConfig ?? [])
}
})

Expand Down
17 changes: 10 additions & 7 deletions src/cdk/stacks/NextCloudfrontStack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ export interface NextCloudfrontStackProps extends StackProps {
buildOutputPath: string
deployConfig: DeployConfig
imageTTL?: number
redirects?: NextRedirects
redirectsConfig?: NextRedirects
nextI18nConfig?: NextI18nConfig
cachedRoutesMatchers: string[]
rewritesConfig: NextRewrites
isTrailingSlashEnabled: boolean
}

export class NextCloudfrontStack extends Stack {
Expand All @@ -37,10 +38,11 @@ export class NextCloudfrontStack extends Stack {
region,
deployConfig,
imageTTL,
redirects,
redirectsConfig,
cachedRoutesMatchers,
nextI18nConfig,
rewritesConfig
rewritesConfig,
isTrailingSlashEnabled
} = props

this.originRequestLambdaEdge = new OriginRequestLambdaEdge(this, `${id}-OriginRequestLambdaEdge`, {
Expand All @@ -50,15 +52,16 @@ export class NextCloudfrontStack extends Stack {
buildOutputPath,
cacheConfig: deployConfig.cache,
bucketRegion: region,
cachedRoutesMatchers,
rewritesConfig
cachedRoutesMatchers
})

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

this.viewerResponseLambdaEdge = new ViewerResponseLambdaEdge(this, `${id}-ViewerResponseLambdaEdge`, {
Expand Down
7 changes: 4 additions & 3 deletions src/commands/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ export const deploy = async (config: DeployConfig) => {
const deployConfig = await loadConfig()

const nextConfig = (await loadFile(projectSettings.nextConfigPath)) as NextConfig
const nextRedirects = nextConfig.redirects ? await nextConfig.redirects() : undefined
const nextI18nConfig = nextConfig.i18n
const isTrailingSlashEnabled = nextConfig.trailingSlash ?? false

const outputPath = createOutputFolder()

Expand All @@ -113,7 +113,7 @@ export const deploy = async (config: DeployConfig) => {
const siteNameLowerCased = siteName.toLowerCase()

// Build and zip app.
const { cachedRoutesMatchers, rewritesConfig } = await buildApp({
const { cachedRoutesMatchers, rewritesConfig, redirectsConfig } = await buildApp({
projectSettings,
outputPath
})
Expand Down Expand Up @@ -156,9 +156,10 @@ export const deploy = async (config: DeployConfig) => {
deployConfig,
imageTTL: nextConfig.imageTTL,
nextI18nConfig,
redirects: nextRedirects,
redirectsConfig,
cachedRoutesMatchers,
rewritesConfig,
isTrailingSlashEnabled,
env: {
region: AWS_EDGE_REGION // required since Edge can be deployed only here.
}
Expand Down
56 changes: 1 addition & 55 deletions src/lambdas/originRequest.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { S3Client, HeadObjectCommand } from '@aws-sdk/client-s3'
import type { CloudFrontRequestEvent, CloudFrontRequestCallback, CloudFrontRequest, Context } from 'aws-lambda'
import crypto from 'node:crypto'
import { CacheConfig, NextRewrites, NextRewriteEntity } from '../types'
import { CacheConfig } from '../types'
import {
transformQueryToObject,
transformCookiesToObject,
Expand Down Expand Up @@ -93,57 +93,6 @@ const shouldRevalidateFile = (s3FileMeta: { LastModified: Date | string; CacheCo
return isFileExpired
}

/**
* Validates if a CloudFront request matches the conditions specified in the 'has' property of a rewrite rule
* @param request - The CloudFront request object to validate
* @param has - Array of conditions to check (header, query param, or cookie)
* @returns True if all conditions match or if no conditions specified, false otherwise
*/
const validateRouteHasMatch = (request: CloudFrontRequest, has: NextRewriteEntity['has']) => {
if (!has) return true

return has.every((h) => {
if (h.type === 'header') {
const header = request.headers[h.key]

return h.value ? header?.some((header) => header.value === h.value) : !!header
}

if (h.type === 'query' && request.querystring) {
const searchParams = new URLSearchParams(request.querystring)

return h.value ? searchParams.get(h.key) === h.value : searchParams.has(h.key)
}

if (h.type === 'cookie') {
const cookies = request.headers.cookie?.[0].value

return cookies?.includes(`${h.key}=${h.value ?? ''}`)
}

return false
})
}

/**
* Checks if a CloudFront request matches any rewrite rules and updates the URI if matched
* @param request - The CloudFront request object to validate and potentially modify
* @param rewritesConfig - Array of rewrite rules to check against
*/
const validateRewriteRoute = (request: CloudFrontRequest, rewritesConfig: NextRewrites) => {
const rewriteRoute = rewritesConfig.find((rewrite) => {
const { regex, has } = rewrite

const hasMatches = validateRouteHasMatch(request, has)

return hasMatches && new RegExp(regex).test(request.uri)
})

if (rewriteRoute) {
request.uri = rewriteRoute.destination
}
}

export const handler = async (
event: CloudFrontRequestEvent,
_context: Context,
Expand All @@ -153,12 +102,9 @@ export const handler = async (
const s3Bucket = process.env.S3_BUCKET!
const cacheConfig = process.env.CACHE_CONFIG as CacheConfig
const nextCachedRoutesMatchers = process.env.NEXT_CACHED_ROUTES_MATCHERS as unknown as string[]
const nextRewritesConfig = process.env.NEXT_REWRITES_CONFIG as unknown as NextRewrites
const { s3Key } = getS3ObjectPath(request, cacheConfig)
const ebAppUrl = process.env.EB_APP_URL!

validateRewriteRoute(request, nextRewritesConfig)

const isCachedRoute = nextCachedRoutesMatchers.some((matcher) => RegExp(matcher).test(request.uri))

try {
Expand Down
Loading
Loading