Skip to content
This repository was archived by the owner on Feb 1, 2025. It is now read-only.

Commit 848c999

Browse files
authored
ref(app): bundling, cdk (#24)
* refactor: nextjs packaging, use npm packages to bundle instead of shell script, parameters added * refactor(app): bundling process changed, simplified, CDK construct added * ref(build): leaned into rollup, zipping done as part of rollup process Co-authored-by: Jan Soukup <[email protected]>
1 parent 1ff75ee commit 848c999

12 files changed

+12495
-5471
lines changed

LICENSE

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c)
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6+
7+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8+
9+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

README.md

+10-146
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ And includes CLI and custom server handler to integrate with ApiGw.
99
- [next.config.js](#nextconfigjs)
1010
- [Server handler](#server-handler)
1111
- [Image handler](#image-handler)
12-
- [CDK](#cdk)
13-
- [Sharp](#sharp)
12+
- [Via CDK](#via-cdk)
13+
- [Sharp](#sharp)
1414
- [Packaging](#packaging)
1515
- [Server handler](#server-handler-1)
1616
- [Static assets](#static-assets)
@@ -39,7 +39,7 @@ See:
3939

4040
- https://github.com/vercel/next.js/issues/36386
4141
- https://github.com/vercel/next.js/discussions/32223
42-
-
42+
4343

4444
### Server handler
4545

@@ -59,7 +59,7 @@ const dependenciesLayer = new LayerVersion(this, 'DepsLayer', {
5959
const requestHandlerFn = new Function(this, 'LambdaFunction', {
6060
code: Code.fromAsset(`next.out/code.zip`, { followSymlinks: SymlinkFollowMode.NEVER }),
6161
runtime: Runtime.NODEJS_16_X,
62-
handler: 'handler.handler',
62+
handler: 'server-handler.handler',
6363
layers: [dependenciesLayer],
6464
memorySize: 512,
6565
timeout: Duration.seconds(10),
@@ -79,7 +79,7 @@ const imageHandlerZip = require.resolve('@sladg/nextjs-lambda/image-handler/zip'
7979
const imageOptimizerFn = new Function(this, 'LambdaFunction', {
8080
code: Code.fromAsset(imageHandlerZip),
8181
runtime: Runtime.NODEJS_16_X,
82-
handler: 'index.handler',
82+
handler: 'image-handler.handler',
8383
layers: [sharpLayer],
8484
memorySize: 1024,
8585
timeout: Duration.seconds(15),
@@ -93,149 +93,13 @@ Lambda consumes Api Gateway requests, so we need to create ApiGw proxy (v2) that
9393

9494
Lambda is designed to serve `_next/image*` route in NextJS stack and replaces the default handler so we can optimize caching and memory limits for page renders and image optimization.
9595

96-
### CDK
97-
98-
Here is complete example using CDK:
99-
100-
```
101-
import { HttpApi } from '@aws-cdk/aws-apigatewayv2-alpha'
102-
import { HttpLambdaIntegration } from '@aws-cdk/aws-apigatewayv2-integrations-alpha'
103-
import { AssetHashType, CfnOutput, Duration, RemovalPolicy, Stack, StackProps, SymlinkFollowMode } from 'aws-cdk-lib'
104-
import { CloudFrontAllowedMethods, CloudFrontWebDistribution } from 'aws-cdk-lib/aws-cloudfront'
105-
import { Code, Function, LayerVersion, Runtime } from 'aws-cdk-lib/aws-lambda'
106-
import { Bucket, BucketAccessControl } from 'aws-cdk-lib/aws-s3'
107-
import { BucketDeployment, Source } from 'aws-cdk-lib/aws-s3-deployment'
108-
import { Construct } from 'constructs'
109-
110-
const imageHandlerZip = require.resolve('@sladg/nextjs-lambda/image-handler/zip')
111-
const sharpLayerZip = require.resolve('@sladg/nextjs-lambda/sharp-layer/zip')
112-
113-
export class NextLambdaStack extends Stack {
114-
constructor(scope: Construct, id: string, props?: StackProps) {
115-
super(scope, id, props)
116-
117-
const depsLayer = new LayerVersion(this, 'DepsLayer', {
118-
code: Code.fromAsset(`next.out/dependenciesLayer.zip`),
119-
})
120-
121-
const sharpLayer = new LayerVersion(this, 'SharpLayer', {
122-
code: Code.fromAsset(sharpLayerZip, { assetHash: 'static', assetHashType: AssetHashType.CUSTOM }),
123-
})
124-
125-
const assetsBucket = new Bucket(this, 'NextAssetsBucket', {
126-
publicReadAccess: true,
127-
autoDeleteObjects: true,
128-
removalPolicy: RemovalPolicy.DESTROY,
129-
})
130-
131-
const lambdaFn = new Function(this, 'DefaultNextJs', {
132-
code: Code.fromAsset(`next.out/code.zip`, { followSymlinks: SymlinkFollowMode.NEVER }),
133-
runtime: Runtime.NODEJS_16_X,
134-
handler: 'handler.handler',
135-
layers: [depsLayer],
136-
// No need for big memory as image handling is done elsewhere.
137-
memorySize: 512,
138-
timeout: Duration.seconds(15),
139-
})
140-
141-
const imageOptimizationFn = new Function(this, 'ImageOptimizationNextJs', {
142-
code: Code.fromAsset(imageHandlerZip),
143-
runtime: Runtime.NODEJS_16_X,
144-
handler: 'index.handler',
145-
layers: [sharpLayer],
146-
memorySize: 1024,
147-
timeout: Duration.seconds(10),
148-
environment: {
149-
S3_SOURCE_BUCKET: assetsBucket.bucketName,
150-
},
151-
})
152-
153-
assetsBucket.grantRead(imageOptimizationFn)
96+
### Via CDK
15497

155-
const apiGwDefault = new HttpApi(this, 'NextJsLambdaProxy', {
156-
createDefaultStage: true,
157-
defaultIntegration: new HttpLambdaIntegration('LambdaApigwIntegration', lambdaFn),
158-
})
98+
See `NextStandaloneLambda` construct in `lib/construct.ts`.
99+
This is a complete definition which consumes output of `pack` CLI command. It requires 3 zips (assets, dependencies and code) and exposes get methods for all resources to provide a way to set custom routes, set environment variables, etc.
159100

160-
const apiGwImages = new HttpApi(this, 'ImagesLambdaProxy', {
161-
createDefaultStage: true,
162-
defaultIntegration: new HttpLambdaIntegration('ImagesApigwIntegration', imageOptimizationFn),
163-
})
164-
165-
const cfnDistro = new CloudFrontWebDistribution(this, 'TestApigwDistro', {
166-
// Must be set, because cloufront would use index.html which would not match in NextJS routes.
167-
defaultRootObject: '',
168-
comment: 'ApiGwLambda Proxy for NextJS',
169-
originConfigs: [
170-
{
171-
// Default behaviour, lambda handles.
172-
behaviors: [
173-
{
174-
allowedMethods: CloudFrontAllowedMethods.ALL,
175-
isDefaultBehavior: true,
176-
forwardedValues: { queryString: true },
177-
},
178-
{
179-
allowedMethods: CloudFrontAllowedMethods.ALL,
180-
pathPattern: '_next/data/*',
181-
},
182-
],
183-
customOriginSource: {
184-
domainName: `${apiGwDefault.apiId}.execute-api.${this.region}.amazonaws.com`,
185-
},
186-
},
187-
{
188-
// Our implementation of image optimization, we are tapping into Next's default route to avoid need for next.config.js changes.
189-
behaviors: [
190-
{
191-
// Should use caching based on query params.
192-
allowedMethods: CloudFrontAllowedMethods.ALL,
193-
pathPattern: '_next/image*',
194-
forwardedValues: { queryString: true },
195-
},
196-
],
197-
customOriginSource: {
198-
domainName: `${apiGwImages.apiId}.execute-api.${this.region}.amazonaws.com`,
199-
},
200-
},
201-
{
202-
// Remaining next files (safe-catch) and our assets that are not imported via `next/image`
203-
behaviors: [
204-
{
205-
allowedMethods: CloudFrontAllowedMethods.GET_HEAD_OPTIONS,
206-
pathPattern: '_next/*',
207-
},
208-
{
209-
allowedMethods: CloudFrontAllowedMethods.GET_HEAD_OPTIONS,
210-
pathPattern: 'assets/*',
211-
},
212-
],
213-
s3OriginSource: {
214-
s3BucketSource: assetsBucket,
215-
},
216-
},
217-
],
218-
})
219-
220-
// This can be handled by `aws s3 sync` but we need to ensure invalidation of Cfn after deploy.
221-
new BucketDeployment(this, 'PublicFilesDeployment', {
222-
destinationBucket: assetsBucket,
223-
sources: [Source.asset(`next.out/assetsLayer.zip`)],
224-
accessControl: BucketAccessControl.PUBLIC_READ,
225-
// Invalidate all paths after deployment.
226-
distributionPaths: ['/*'],
227-
distribution: cfnDistro,
228-
})
229-
230-
new CfnOutput(this, 'cfnDistroUrl', { value: cfnDistro.distributionDomainName })
231-
new CfnOutput(this, 'defaultApiGwUrl', { value: apiGwDefault.apiEndpoint })
232-
new CfnOutput(this, 'imagesApiGwUrl', { value: apiGwImages.apiEndpoint })
233-
new CfnOutput(this, 'assetsBucketUrl', { value: assetsBucket.bucketDomainName })
234-
}
235-
}
236-
```
237101

238-
## Sharp
102+
### Sharp
239103

240104
Besides handler (wrapper) itself, underlying NextJS also requires sharp binaries.
241105
To build those, we use `npm install` with some extra parametes. Then we zip all sharp dependencies and compress it to easily importable zip file.
@@ -258,7 +122,7 @@ To package everything, make sure to be in your project root folder and next fold
258122

259123
It will create `next.out/` folder with 3 zip packages. One zip Lambda's code, one is dependencies layer and one is assets layer.
260124

261-
- code zip: include all files generated by next that are required to run on Lambda behind ApiGateway. Original handler as well as new server handler are included. Use `handler.handler` for custom one or `server.handler` for original one.
125+
- code zip: include all files generated by next that are required to run on Lambda behind ApiGateway. Original handler as well as new server handler are included. Use `server-handler.handler` for custom one or `server.handler` for original one.
262126
- dependencies layer: all transpilied `node_modules`. Next includes only used files, dramatically reducing overall size.
263127
- assets layer: your public folder together with generated assets. Keep in mind that to public refer file, you need to include it in `public/assets/` folder, not just in public. This limitation dramatically simplifies whole setup. This zip file is uploaded to S3, it's not included in Lambda code.
264128

lib/cli.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
#!/usr/bin/env node
2-
31
import { Command } from 'commander'
42
import { mkdirSync, rmSync, symlinkSync, writeFileSync } from 'fs'
53
import { tmpdir } from 'os'
@@ -47,6 +45,10 @@ program
4745
.action(async (options) => {
4846
const { standaloneFolder, publicFolder, handlerPath, outputFolder } = options
4947

48+
// @TODO: Validate that output folder exists.
49+
// @TODO: Validate server.js exists and we can match data.
50+
// @TODO: Validate that public folder is using `assets` subfolder.
51+
5052
// Dependencies layer configuration
5153
const nodeModulesFolderPath = path.resolve(standaloneFolder, './node_modules')
5254
const depsLambdaFolder = 'nodejs/node_modules'

0 commit comments

Comments
 (0)