Skip to content

Commit

Permalink
fix: image optimization function (#146)
Browse files Browse the repository at this point in the history
* fix: image optimization function

* chore: self mutation

Signed-off-by: github-actions <[email protected]>

* docs: remove references to NodejsFunction

* fix: add bundling options to .projenrc

---------

Signed-off-by: github-actions <[email protected]>
Co-authored-by: github-actions <[email protected]>
  • Loading branch information
bestickley and github-actions authored Oct 17, 2023
1 parent c41e1d1 commit 82ace48
Show file tree
Hide file tree
Showing 10 changed files with 33 additions and 89 deletions.
11 changes: 10 additions & 1 deletion .projenrc.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import { BuildOptions } from 'esbuild';
import { awscdk } from 'projen';
import { TypeScriptCompilerOptions, UpgradeDependenciesSchedule } from 'projen/lib/javascript';
import { commonBundlingOptions } from './src/utils/common-build-options';

const commonBundlingOptions = {
bundle: true,
external: ['@aws-sdk/*'],
minify: true,
platform: 'node',
sourcemap: true,
target: 'node18',
} satisfies BuildOptions;

const commonTscOptions: TypeScriptCompilerOptions = {
// isolatedModules: true, // why doesn't this work?
Expand Down
12 changes: 6 additions & 6 deletions API.md

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

5 changes: 1 addition & 4 deletions docs/code-deployment-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Deep dive into `Nextjs` constructs code deployment flow - how your Next.js code
1. `NextjsBuild` is instantiated which runs `npx open-next build` within user's Next.js repository. This command runs `next build` which creates a .next folder with build output. Then `open-next` copies the static assets and generates a cached files (ISR), server, image optimization, revalidation, and warmer lambda function handler code. When open-next's build is run, the process's environment variables, `NextjsProps.environment`, and `Nextjs.nodeEnv` are injected into the build process. However, any unresolved tokens in `NextjsProps.environment` are replaced with placeholders that look like `{{ BUCKET_NAME }}` as they're unresolved tokens so they're value looks like `${TOKEN[Bucket.Name.1234]}`. Learn more about AWS CDK Tokens [here](https://docs.aws.amazon.com/cdk/v2/guide/tokens.html). The placeholders will be replaced later in a [CloudFormation Custom Resource](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html).
1. `NextjsStaticAssets` is instantiated which creates an S3 bucket, an `Asset` for your Next.js static assets, and a `NextjsBucketDeployment`. [Asset](https://docs.aws.amazon.com/cdk/v2/guide/assets.html) is uploaded to the S3 Bucket created during CDK Bootstrap in your AWS account (not bucket created in `NextjsStaticAssets`). `NextjsBucketDeployment` is a CloudFormation Custom Resource that downloads files from the CDK Assets Bucket, updates placeholder values, and then deploys the files to the target bucket. Placeholder values were unresolved tokens at synthesis time (because they reference values where names/ids aren't known yet) but at the time the code runs in the Custom Resource Lambda Function, those values have been resolved and are passed into custom resource through `ResourceProperties`. Only the public environment variable (NEXT_PUBLIC) placeholders are passed to `NextjsBucketDeployment.substitutionConfig` because server variables shouldn't live in static assets. Learn more about Next.js environment variables [here](https://nextjs.org/docs/app/building-your-application/configuring/environment-variables). It's important to note the deployment order so that we don't write the static assets to the bucket until they're placeholders are replaced, otherwise we risk a user downloading a file with placeholders which would result in an error.
1. `NextjsServer` is instantiated which creates an `Asset`, `NextjsBucketDeployment`, and lambda function to run Next.js server code. `NextjsBucketDeployment` will replace all (public and private) unresolved tokens within open-next generated server function code. Additional environment variables to support cache ISR feature are added: CACHE_BUCKET_NAME, CACHE_BUCKET_KEY, CACHE_BUCKET_REGION. `NextjsServer` also bundles lambda code with `esbuild`. The same note above about the important of deployment order applies here.
1. `NextjsImage` and `NextjsRevalidation` are instantiated with each create a `NodejsFunction` which automatically does bundling and uploading of asset for us. We don't need to replace environment variable placeholders because they don't any (TODO: confirm this).
1. `NextjsImage` and `NextjsRevalidation` are instantiated with `Function` utilizing bundled code output from `open-next`. We don't need to replace environment variable placeholders because they don't any.
1. `NextjsInvalidation` is instantiated to invalidate CloudFront Distribution. This construct explicitly depends upon `NextjsStaticAssets`, `NextjsServer`, `NextjsImage` so that we ensure any resources that could impact cached resources (static assets, dynamic html, images) are up to date before invalidating CloudFront Distribution's cache.

## PNPM Monorepo Symlinks
Expand All @@ -17,6 +17,3 @@ PNPM Monorepos use symlinks between workspace node_modules and the top level nod
Relevant GitHub Issues:
- https://github.com/aws/aws-cdk/issues/9251
- https://github.com/Stuk/jszip/issues/386#issuecomment-634773343

## Reason Why We Cannot use NodejsFunction for all Lambdas
Originally I wanted to use NodejsFunction which manages bundling for us. However, we cannot couple bundling and deploying the function together when we don't know deploy time values because this would deploy the function without inject environment variables. The period of the function being in this state would be small, but isn't acceptable IMO. Therefore, we must deploy assets to S3, inject environment variables, then deploy the function - similar to how it works currently. There are still improvements to make, though. Server and Image functions must be inject with env vars where as Revalidation and in the future, Warmer can be bundled with NodejsLambda.
2 changes: 1 addition & 1 deletion open-next
31 changes: 7 additions & 24 deletions src/NextjsImage.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import { join } from 'path';
import { LogLevel, NodejsFunction, NodejsFunctionProps } from 'aws-cdk-lib/aws-lambda-nodejs';
import { Code, Function as LambdaFunction, FunctionOptions } from 'aws-cdk-lib/aws-lambda';
import { IBucket } from 'aws-cdk-lib/aws-s3';
import { Construct } from 'constructs';
import { NEXTJS_BUILD_INDEX_FILE } from './constants';
import { NextjsBaseProps } from './NextjsBase';
import type { NextjsBuild } from './NextjsBuild';
import { getCommonNodejsFunctionProps } from './utils/common-lambda-props';
import { fixPath } from './utils/convert-path';
import { getCommonFunctionProps } from './utils/common-lambda-props';

export interface NextjsImageProps extends NextjsBaseProps {
/**
Expand All @@ -16,7 +13,7 @@ export interface NextjsImageProps extends NextjsBaseProps {
/**
* Override function properties.
*/
readonly lambdaOptions?: NodejsFunctionProps;
readonly lambdaOptions?: FunctionOptions;
/**
* The `NextjsBuild` instance representing the built Nextjs application.
*/
Expand All @@ -26,28 +23,14 @@ export interface NextjsImageProps extends NextjsBaseProps {
/**
* This lambda handles image optimization.
*/
export class NextjsImage extends NodejsFunction {
export class NextjsImage extends LambdaFunction {
constructor(scope: Construct, id: string, props: NextjsImageProps) {
const { lambdaOptions, bucket } = props;

const nodejsFnProps = getCommonNodejsFunctionProps(scope);
const commonFnProps = getCommonFunctionProps(scope);
super(scope, id, {
...nodejsFnProps,
bundling: {
...nodejsFnProps.bundling,
logLevel: LogLevel.SILENT, // silence error on use of `eval` in node_module
commandHooks: {
afterBundling: () => [],
beforeBundling: (_inputDir, outputDir) => [
// copy non-bundled assets into zip. use node -e so cross-os compatible
`node -e "fs.cpSync('${fixPath(props.nextBuild.nextImageFnDir)}', '${fixPath(
outputDir
)}', { recursive: true, filter: (src) => !src.includes('/node_modules') && !src.endsWith('index.mjs') })"`,
],
beforeInstall: () => [],
},
},
entry: join(props.nextBuild.nextImageFnDir, NEXTJS_BUILD_INDEX_FILE),
...commonFnProps,
code: Code.fromAsset(props.nextBuild.nextImageFnDir),
handler: 'index.handler',
description: 'Next.js Image Optimization Function',
...lambdaOptions,
Expand Down
33 changes: 8 additions & 25 deletions src/NextjsRevalidation.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import { join } from 'path';
import { Duration, Stack } from 'aws-cdk-lib';
import { AnyPrincipal, Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam';
import { FunctionOptions } from 'aws-cdk-lib/aws-lambda';
import { Code, Function as LambdaFunction, FunctionOptions } from 'aws-cdk-lib/aws-lambda';
import { SqsEventSource } from 'aws-cdk-lib/aws-lambda-event-sources';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
import { Queue } from 'aws-cdk-lib/aws-sqs';
import { Construct } from 'constructs';
import { NEXTJS_BUILD_INDEX_FILE } from './constants';
import { NextjsBaseProps } from './NextjsBase';
import { NextjsBuild } from './NextjsBuild';
import { NextjsServer } from './NextjsServer';
import { getCommonNodejsFunctionProps } from './utils/common-lambda-props';
import { fixPath } from './utils/convert-path';
import { getCommonFunctionProps } from './utils/common-lambda-props';

export interface NextjsRevalidationProps extends NextjsBaseProps {
/**
Expand All @@ -38,7 +34,7 @@ export interface NextjsRevalidationProps extends NextjsBaseProps {
*/
export class NextjsRevalidation extends Construct {
queue: Queue;
function: NodejsFunction;
function: LambdaFunction;
private props: NextjsRevalidationProps;

constructor(scope: Construct, id: string, props: NextjsRevalidationProps) {
Expand Down Expand Up @@ -76,26 +72,13 @@ export class NextjsRevalidation extends Construct {
return queue;
}

private createFunction(): NodejsFunction {
const nodejsFnProps = getCommonNodejsFunctionProps(this);
const fn = new NodejsFunction(this, 'Fn', {
...nodejsFnProps,
bundling: {
...nodejsFnProps.bundling,
commandHooks: {
afterBundling: () => [],
beforeBundling: (_inputDir, outputDir) => [
// copy non-bundled assets into zip. use node -e so cross-os compatible
`node -e "fs.cpSync('${fixPath(this.props.nextBuild.nextRevalidateFnDir)}', '${fixPath(
outputDir
)}', { recursive: true, filter: (src) => !src.endsWith('index.mjs') })"`,
],
beforeInstall: () => [],
},
},
private createFunction(): LambdaFunction {
const commonFnProps = getCommonFunctionProps(this);
const fn = new LambdaFunction(this, 'Fn', {
...commonFnProps,
// open-next revalidation-function
// see: https://github.com/serverless-stack/open-next/blob/274d446ed7e940cfbe7ce05a21108f4c854ee37a/README.md?plain=1#L65
entry: join(this.props.nextBuild.nextRevalidateFnDir, NEXTJS_BUILD_INDEX_FILE),
code: Code.fromAsset(this.props.nextBuild.nextRevalidateFnDir),
handler: 'index.handler',
description: 'Next.js revalidation function',
timeout: Duration.seconds(30),
Expand Down
1 change: 0 additions & 1 deletion src/NextjsServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@ export class NextjsServer extends Construct {
}

private createFunction(asset: Asset) {
// cannot use NodejsFunction because we must wait to deploy the function
// until after the build time env vars in code zip asset are substituted
const fn = new Function(this, 'Fn', {
...getCommonFunctionProps(this),
Expand Down
1 change: 0 additions & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,3 @@ export const NEXTJS_CACHE_DIR = 'cache';
export const NEXTJS_BUILD_REVALIDATE_FN_DIR = 'revalidation-function';
export const NEXTJS_BUILD_IMAGE_FN_DIR = 'image-optimization-function';
export const NEXTJS_BUILD_SERVER_FN_DIR = 'server-function';
export const NEXTJS_BUILD_INDEX_FILE = 'index.mjs';
12 changes: 0 additions & 12 deletions src/utils/common-build-options.ts

This file was deleted.

14 changes: 0 additions & 14 deletions src/utils/common-lambda-props.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { Duration, PhysicalName, Stack } from 'aws-cdk-lib';
import { Architecture, FunctionProps, Runtime } from 'aws-cdk-lib/aws-lambda';
import { NodejsFunctionProps, OutputFormat } from 'aws-cdk-lib/aws-lambda-nodejs';
import { Construct } from 'constructs';
import { commonBundlingOptions } from './common-build-options';

export function getCommonFunctionProps(scope: Construct): Omit<FunctionProps, 'code' | 'handler'> {
return {
Expand All @@ -20,15 +18,3 @@ export function getCommonFunctionProps(scope: Construct): Omit<FunctionProps, 'c
functionName: Stack.of(scope).region !== 'us-east-1' ? PhysicalName.GENERATE_IF_NEEDED : undefined,
};
}

export function getCommonNodejsFunctionProps(scope: Construct): NodejsFunctionProps {
return {
...getCommonFunctionProps(scope),
bundling: {
...commonBundlingOptions,
// https://github.com/evanw/esbuild/issues/1921
banner: `import { createRequire } from 'module'; const require = createRequire(import.meta.url);`,
format: OutputFormat.ESM,
},
};
}

0 comments on commit 82ace48

Please sign in to comment.