Skip to content

Commit

Permalink
feat: Allow functionAssociations overrides
Browse files Browse the repository at this point in the history
  • Loading branch information
coryasilva committed Dec 18, 2024
1 parent 97cc0f8 commit e293b4b
Show file tree
Hide file tree
Showing 5 changed files with 202 additions and 49 deletions.
1 change: 1 addition & 0 deletions .projenrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ new ProjenStruct(project, {
filePath: getFilePath('OptionalCloudFrontFunctionProps'),
})
.mixin(Struct.fromFqn('aws-cdk-lib.aws_cloudfront.FunctionProps'))
.omit('code')
.allOptional();
new ProjenStruct(project, {
name: 'OptionalDistributionProps',
Expand Down
131 changes: 107 additions & 24 deletions API.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

107 changes: 88 additions & 19 deletions src/NextjsDistribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,25 @@ import { NextjsProps } from './Nextjs';
import { NextjsBuild } from './NextjsBuild';
import { NextjsDomain } from './NextjsDomain';

export interface ViewerRequestFunctionProps extends OptionalCloudFrontFunctionProps {
/**
* Cloudfront function code that runs on VIEWER_REQUEST.
* The following comments will be replaced with code snippets
* so you can customize this function.
*
* INJECT_CLOUDFRONT_FUNCTION_HOST_HEADER: Add the required x-forwarded-host header.
* INJECT_CLOUDFRONT_FUNCTION_CACHE_HEADER_KEY: Improves open-next cache key.
*
* @default
* async function handler(event) {
* // INJECT_CLOUDFRONT_FUNCTION_HOST_HEADER
* // INJECT_CLOUDFRONT_FUNCTION_CACHE_HEADER_KEY
* }
*/
readonly code?: cloudfront.FunctionCode;
}
export interface NextjsDistributionOverrides {
readonly cloudFrontFunctionProps?: OptionalCloudFrontFunctionProps;
readonly viewerRequestFunctionProps?: ViewerRequestFunctionProps;
readonly distributionProps?: OptionalDistributionProps;
readonly edgeFunctionProps?: OptionalEdgeFunctionProps;
readonly imageBehaviorOptions?: AddBehaviorOptions;
Expand Down Expand Up @@ -272,14 +289,7 @@ export class NextjsDistribution extends Construct {
serverBehaviorOptions?.cachePolicy ??
new cloudfront.CachePolicy(this, 'ServerCachePolicy', {
queryStringBehavior: cloudfront.CacheQueryStringBehavior.all(),
headerBehavior: cloudfront.CacheHeaderBehavior.allowList(
'accept',
'rsc',
'next-router-prefetch',
'next-router-state-tree',
'next-url',
'x-prerender-revalidate'
),
headerBehavior: cloudfront.CacheHeaderBehavior.allowList('x-open-next-cache-key'),
cookieBehavior: cloudfront.CacheCookieBehavior.all(),
defaultTtl: Duration.seconds(0),
maxTtl: Duration.days(365),
Expand All @@ -301,7 +311,7 @@ export class NextjsDistribution extends Construct {
override: false,
// MDN Cache-Control Use Case: Up-to-date contents always
// @see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#up-to-date_contents_always
value: `no-cache`,
value: 'no-cache',
},
],
},
Expand All @@ -323,19 +333,78 @@ export class NextjsDistribution extends Construct {
};
}

private useCloudFrontFunctionHostHeader() {
return `event.request.headers["x-forwarded-host"] = event.request.headers.host;`;
}

private useCloudFrontFunctionCacheHeaderKey() {
// This function is used to improve cache hit ratio by setting the cache key
// based on the request headers and the path. `next/image` only needs the
// accept header, and this header is not useful for the rest of the query
return `
const getHeader = (key) => {
const header = event.request.headers[key];
if (header) {
if (header.multiValue) {
return header.multiValue.map((header) => header.value).join(",");
}
if (header.value) {
return header.value;
}
}
return "";
}
let cacheKey = "";
if (event.request.uri.startsWith("/_next/image")) {
cacheKey = getHeader("accept");
} else {
cacheKey =
getHeader("rsc") +
getHeader("next-router-prefetch") +
getHeader("next-router-state-tree") +
getHeader("next-url") +
getHeader("x-prerender-revalidate");
}
if (event.request.cookies["__prerender_bypass"]) {
cacheKey += event.request.cookies["__prerender_bypass"]
? event.request.cookies["__prerender_bypass"].value
: "";
}
const crypto = require("crypto")
const hashedKey = crypto.createHash("md5").update(cacheKey).digest("hex");
event.request.headers["x-open-next-cache-key"] = { value: hashedKey };
`;
}

/**
* If this doesn't run, then Next.js Server's `request.url` will be Lambda Function
* URL instead of domain
*/
private createCloudFrontFnAssociations() {
let code =
this.props.overrides?.viewerRequestFunctionProps?.code?.render() ??
`
async function handler(event) {
// INJECT_CLOUDFRONT_FUNCTION_HOST_HEADER
// INJECT_CLOUDFRONT_FUNCTION_CACHE_HEADER_KEY
}
`;
code = code.replace(
/^\s*\/\/\s*INJECT_CLOUDFRONT_FUNCTION_HOST_HEADER.*$/i,
this.useCloudFrontFunctionHostHeader()
);
code = code.replace(
/^\s*\/\/\s*INJECT_CLOUDFRONT_FUNCTION_CACHE_HEADER_KEY.*$/i,
this.useCloudFrontFunctionCacheHeaderKey()
);
const cloudFrontFn = new cloudfront.Function(this, 'CloudFrontFn', {
code: cloudfront.FunctionCode.fromInline(`
function handler(event) {
var request = event.request;
request.headers["x-forwarded-host"] = request.headers.host;
return request;
}
`),
runtime: cloudfront.FunctionRuntime.JS_2_0,
...this.props.overrides?.viewerRequestFunctionProps,
// Override code last to get injections
code: cloudfront.FunctionCode.fromInline(code),
});
return [{ eventType: cloudfront.FunctionEventType.VIEWER_REQUEST, function: cloudFrontFn }];
}
Expand Down Expand Up @@ -376,7 +445,7 @@ export class NextjsDistribution extends Construct {
override: false,
// MDN Cache-Control Use Case: Up-to-date contents always
// @see: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#up-to-date_contents_always
value: `no-cache`,
value: 'no-cache',
},
],
},
Expand Down Expand Up @@ -474,7 +543,7 @@ export class NextjsDistribution extends Construct {
});
if (publicFiles.length >= 25) {
throw new Error(
`Too many public/ files in Next.js build. CloudFront limits Distributions to 25 Cache Behaviors. See documented limit here: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-limits.html#limits-web-distributions`
'Too many public/ files in Next.js build. CloudFront limits Distributions to 25 Cache Behaviors. See documented limit here: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-limits.html#limits-web-distributions'
);
}
for (const publicFile of publicFiles) {
Expand Down
5 changes: 0 additions & 5 deletions src/generated-structs/OptionalCloudFrontFunctionProps.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ export {
NextjsBucketDeploymentProps,
NextjsBucketDeploymentOverrides,
} from './NextjsBucketDeployment';
export { NextjsDistribution, NextjsDistributionProps, NextjsDistributionOverrides } from './NextjsDistribution';
export {
NextjsDistribution,
NextjsDistributionProps,
NextjsDistributionOverrides,
ViewerRequestFunctionProps,
} from './NextjsDistribution';
export { NextjsInvalidation, NextjsInvalidationProps, NextjsInvalidationOverrides } from './NextjsInvalidation';
export { NextjsDomain, NextjsDomainProps, NextjsDomainOverrides } from './NextjsDomain';
export { Nextjs, NextjsProps, NextjsConstructOverrides } from './Nextjs';
Expand Down

0 comments on commit e293b4b

Please sign in to comment.