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
51 changes: 42 additions & 9 deletions src/build/next.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +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'

interface BuildOptions {
packager: ProjectPackager
Expand Down Expand Up @@ -40,7 +41,32 @@ const copyAssets = async (outputPath: string, appPath: string, appRelativePath:
)
}

export const getNextCachedRoutesMatchers = async (outputPath: string, appRelativePath: string): Promise<string[]> => {
const getRewritesConfig = (manifestRules: RoutesManifest['rewrites']): NextRewrites => {
if (!manifestRules) {
return []
}

if (Array.isArray(manifestRules)) {
return manifestRules.map((rule) => ({
source: rule.source,
destination: rule.destination,
regex: rule.regex,
has: rule.has
}))
}

return [...manifestRules.beforeFiles, ...manifestRules.afterFiles, ...manifestRules.fallback].map((rule) => ({
source: rule.source,
destination: rule.destination,
regex: rule.regex,
has: rule.has
}))
}

export const getNextCachedRoutesConfig = async (
outputPath: string,
appRelativePath: string
): Promise<{ cachedRoutesMatchers: string[]; rewritesConfig: NextRewrites }> => {
const prerenderManifestJSON = await fs.readFile(
path.join(outputPath, '.next', 'standalone', appRelativePath, '.next', 'prerender-manifest.json'),
'utf-8'
Expand All @@ -53,13 +79,20 @@ export const getNextCachedRoutesMatchers = async (outputPath: string, appRelativ
const prerenderManifest = JSON.parse(prerenderManifestJSON) as PrerenderManifest
const routesManifest = JSON.parse(routesManifestJSON) as RoutesManifest

return [...routesManifest.dynamicRoutes, ...routesManifest.staticRoutes].reduce((prev, route) => {
if (prerenderManifest.routes?.[route.page] || prerenderManifest.dynamicRoutes?.[route.page]) {
prev.push(route.regex)
}
const cachedRoutesMatchers = [...routesManifest.dynamicRoutes, ...routesManifest.staticRoutes].reduce(
(prev, route) => {
if (prerenderManifest.routes?.[route.page] || prerenderManifest.dynamicRoutes?.[route.page]) {
prev.push(route.regex)
}

return prev
},
[] as string[]
)

const rewritesConfig = getRewritesConfig(routesManifest.rewrites)

return prev
}, [] as string[])
return { cachedRoutesMatchers, rewritesConfig }
}

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

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

return { nextCachedRoutesMatchers }
return { cachedRoutesMatchers, rewritesConfig }
}
11 changes: 7 additions & 4 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 } from '../../types'
import { CacheConfig, NextRewrites } from '../../types'

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

const NodeJSEnvironmentMapping: Record<string, lambda.Runtime> = {
Expand All @@ -34,7 +35,8 @@ export class OriginRequestLambdaEdge extends Construct {
nodejs,
buildOutputPath,
cacheConfig,
nextCachedRoutesMatchers
cachedRoutesMatchers,
rewritesConfig
} = props
super(scope, id)

Expand All @@ -47,7 +49,8 @@ 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(nextCachedRoutesMatchers ?? [])
'process.env.NEXT_CACHED_ROUTES_MATCHERS': JSON.stringify(cachedRoutesMatchers ?? []),
'process.env.NEXT_REWRITES_CONFIG': JSON.stringify(rewritesConfig ?? [])
}
})

Expand Down
13 changes: 8 additions & 5 deletions src/cdk/stacks/NextCloudfrontStack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { OriginRequestLambdaEdge } from '../constructs/OriginRequestLambdaEdge'
import { CloudFrontDistribution } from '../constructs/CloudFrontDistribution'
import { ViewerResponseLambdaEdge } from '../constructs/ViewerResponseLambdaEdge'
import { ViewerRequestLambdaEdge } from '../constructs/ViewerRequestLambdaEdge'
import { DeployConfig, NextRedirects, NextI18nConfig } from '../../types'
import { DeployConfig, NextRedirects, NextI18nConfig, NextRewrites } from '../../types'

export interface NextCloudfrontStackProps extends StackProps {
nodejs?: string
Expand All @@ -17,7 +17,8 @@ export interface NextCloudfrontStackProps extends StackProps {
imageTTL?: number
redirects?: NextRedirects
nextI18nConfig?: NextI18nConfig
nextCachedRoutesMatchers: string[]
cachedRoutesMatchers: string[]
rewritesConfig: NextRewrites
}

export class NextCloudfrontStack extends Stack {
Expand All @@ -37,8 +38,9 @@ export class NextCloudfrontStack extends Stack {
deployConfig,
imageTTL,
redirects,
nextCachedRoutesMatchers,
nextI18nConfig
cachedRoutesMatchers,
nextI18nConfig,
rewritesConfig
} = props

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

this.viewerRequestLambdaEdge = new ViewerRequestLambdaEdge(this, `${id}-ViewerRequestLambdaEdge`, {
Expand Down
5 changes: 3 additions & 2 deletions src/commands/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export const deploy = async (config: DeployConfig) => {
const siteNameLowerCased = siteName.toLowerCase()

// Build and zip app.
const { nextCachedRoutesMatchers } = await buildApp({
const { cachedRoutesMatchers, rewritesConfig } = await buildApp({
projectSettings,
outputPath
})
Expand Down Expand Up @@ -157,7 +157,8 @@ export const deploy = async (config: DeployConfig) => {
imageTTL: nextConfig.imageTTL,
nextI18nConfig,
redirects: nextRedirects,
nextCachedRoutesMatchers,
cachedRoutesMatchers,
rewritesConfig,
env: {
region: AWS_EDGE_REGION // required since Edge can be deployed only here.
}
Expand Down
56 changes: 55 additions & 1 deletion 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 } from '../types'
import { CacheConfig, NextRewrites, NextRewriteEntity } from '../types'
import {
transformQueryToObject,
transformCookiesToObject,
Expand Down Expand Up @@ -93,6 +93,57 @@ 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 @@ -102,9 +153,12 @@ 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
10 changes: 10 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { NextConfig } from 'next/types'
import type { RouteHas } from 'next/dist/lib/load-custom-routes'

export interface CacheConfig {
noCacheRoutes?: string[]
Expand All @@ -14,3 +15,12 @@ export interface DeployConfig {
export type NextRedirects = Awaited<ReturnType<Required<NextConfig>['redirects']>>

export type NextI18nConfig = NextConfig['i18n']

export type NextRewriteEntity = {
source: string
destination: string
regex: string
has?: RouteHas[]
}

export type NextRewrites = NextRewriteEntity[]
Loading