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

Commit b10bd8c

Browse files
authored
ref(rollup): bundling redefined, correctly generate standalone (#29)
* refactor(rollup): properly bundle different entry points, upgrade to node18 * ref(layers): nextjs layer to be pre-build so it can be used by server and image handler * doc: polished and updated documentation * doc: improved description of the project Co-authored-by: Jan Soukup <[email protected]>
1 parent e3b702a commit b10bd8c

12 files changed

+1490
-7046
lines changed

Diff for: .autorc

-8
This file was deleted.

Diff for: README.md

+62-61
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,35 @@
11
# NextJS Lambda Utils
22

3-
This is a set of utils needed for deploying NextJS into AWS Lambda.
4-
It includes a wrapper for `next/server/image-optimizer` allowing to use S3.
5-
And includes CLI and custom server handler to integrate with ApiGw.
3+
This is a project allowing to deploy Next applications (standalone options turned on) to AWS Lambda without hassle.
4+
5+
This is an alternative to existing Lambda@Edge implementation ([see](https://www.npmjs.com/package/@sls-next/lambda-at-edge)) as it has too many limitations (primarily inability to use env vars) and deployments take too long.
6+
7+
This library uses Cloudfront, S3, ApiGateway and Lambdas to deploy easily in seconds (hotswap supported).
8+
69

710
- [NextJS Lambda Utils](#nextjs-lambda-utils)
11+
- [TL;DR](#tldr)
812
- [Usage](#usage)
913
- [next.config.js](#nextconfigjs)
1014
- [Server handler](#server-handler)
1115
- [Image handler](#image-handler)
1216
- [Via CDK](#via-cdk)
13-
- [Sharp](#sharp)
17+
- [Sharp layer](#sharp-layer)
18+
- [Next layer](#next-layer)
1419
- [Packaging](#packaging)
1520
- [Server handler](#server-handler-1)
1621
- [Static assets](#static-assets)
1722
- [Versioning](#versioning)
1823
- [Guess](#guess)
1924
- [Shipit](#shipit)
20-
- [TODO](#todo)
2125
- [Disclaimer](#disclaimer)
2226

27+
## TL;DR
28+
- In your NextJS project, set output to standalone.
29+
- Run `npx @sladg/nextjs-lambda pack`
30+
- Prepare CDK ([see](#via-cdk))
31+
- Run `cdk deploy`
32+
2333
## Usage
2434

2535
We need to create 2 lambdas in order to use NextJS. First one is handling pages/api rendering, second is solving image optimization.
@@ -43,72 +53,68 @@ See:
4353

4454
### Server handler
4555

46-
```
47-
next build
56+
This is a Lambda entrypoint to handle non-asset requests. We need a way to start Next in lambda-friendly way and translate ApiGateway event into typical HTTP event. This is handled by server-handler, which sits alongside of next's `server.js` in standalone output.
4857

49-
npx @sladg/nextjs-lambda pack
50-
```
58+
### Image handler
5159

52-
Create new lambda function with NODE_16 runtime in AWS.
60+
Lambda consumes Api Gateway requests, so we need to create ApiGw proxy (v2) that will trigger Lambda.
5361

54-
```
55-
const dependenciesLayer = new LayerVersion(this, 'DepsLayer', {
56-
code: Code.fromAsset(`next.out/dependenciesLayer.zip`),
57-
})
58-
59-
const requestHandlerFn = new Function(this, 'LambdaFunction', {
60-
code: Code.fromAsset(`next.out/code.zip`, { followSymlinks: SymlinkFollowMode.NEVER }),
61-
runtime: Runtime.NODEJS_16_X,
62-
handler: 'server-handler.handler',
63-
layers: [dependenciesLayer],
64-
memorySize: 512,
65-
timeout: Duration.seconds(10),
66-
})
67-
```
62+
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.
6863

69-
### Image handler
64+
### Via CDK
7065

71-
Create new lambda function with NODE_16 runtime in AWS.
66+
See `NextStandaloneStack` construct in `lib/construct.ts`.
7267

68+
You can easily create `cdk/app.ts` and use following code:
7369
```
74-
const sharpLayer: LayerVersion
75-
const assetsBucket: Bucket
76-
77-
const imageHandlerZip = require.resolve('@sladg/nextjs-lambda/image-handler/zip')
78-
79-
const imageOptimizerFn = new Function(this, 'LambdaFunction', {
80-
code: Code.fromAsset(imageHandlerZip),
81-
runtime: Runtime.NODEJS_16_X,
82-
handler: 'image-handler.handler',
83-
layers: [sharpLayer],
84-
memorySize: 1024,
85-
timeout: Duration.seconds(15),
86-
environment: {
87-
S3_BUCKET_SOURCE: assetsBucket.bucketName
88-
}
89-
})
90-
```
70+
#!/usr/bin/env node
71+
import 'source-map-support/register'
72+
import * as cdk from 'aws-cdk-lib'
73+
import * as path from 'path'
9174
92-
Lambda consumes Api Gateway requests, so we need to create ApiGw proxy (v2) that will trigger Lambda.
75+
import { NextStandaloneStack } from '@sladg/nextjs-lambda'
9376
94-
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.
77+
const assetsZipPath = path.resolve(__dirname, '../next.out/assetsLayer.zip')
78+
const codeZipPath = path.resolve(__dirname, '../next.out/code.zip')
79+
const dependenciesZipPath = path.resolve(__dirname, '../next.out/dependenciesLayer.zip')
9580
96-
### Via CDK
81+
const app = new cdk.App()
9782
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.
83+
new NextStandaloneStack(app, 'StandaloneNextjsStack-2', {
84+
assetsZipPath,
85+
codeZipPath,
86+
dependenciesZipPath,
87+
})
88+
```
89+
90+
This imports pre-made construct, you only need to worry about paths to outputed zip files from CLI `pack` command.
91+
92+
> More granular CDK construct coming soon.
10093
10194

102-
### Sharp
95+
### Sharp layer
10396

10497
Besides handler (wrapper) itself, underlying NextJS also requires sharp binaries.
10598
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.
10699

107100
```
108-
const code = require.resolve('@sladg/nextjs-lambda/sharp-layer')
101+
import { sharpLayerZipPath } from '@sladg/nextjs-lambda'
102+
103+
const sharpLayer = new LayerVersion(this, 'SharpDependenciesLayer', {
104+
code: Code.fromAsset(sharpLayerZipPath)
105+
})
106+
```
107+
108+
### Next layer
109+
110+
To provide both image and server handlers with all depdencies (next is using `require.resolve` inside, so it cannot be bundled standalone for now).
111+
112+
We pre-package this layer so it can be included in Lambda without hassle.
113+
```
114+
import { nextLayerZipPath } from '@sladg/nextjs-lambda'
109115
110-
const sharpLayer = new LayerVersion(this, 'SharpLayer', {
111-
code: Code.fromAsset(code)
116+
const nextLayer = new LayerVersion(this, 'NextDependenciesLayer', {
117+
code: Code.fromAsset(nextLayerZipPath)
112118
})
113119
```
114120

@@ -147,6 +153,8 @@ Cloudfront paths used:
147153
- `_next/*`
148154
- `assets/*`
149155

156+
Keep in minda, Cloudfront does not allow for multiple regex patterns in single origin, so using extensions to distinguish image/server handlers is not doable.
157+
150158
# Versioning
151159

152160
This package exposes two CLI functions intended to deal with versioning your application and releasing.
@@ -164,15 +172,6 @@ Similar to guess command, however, it automatically tags a commit on current bra
164172
Simply call `@sladg/next-lambda shipit` on any branch and be done.
165173

166174

167-
# TODO
168-
169-
- Explain scripts used for packaging Next app,
170-
- Add CDK examples on how to set it up,
171-
- Export CDK contruct for simple plug-n-play use,
172-
- Use lib/index.ts as single entry and export all paths/functions from it (including zip paths).
173-
- Consider using \*.svg, \*.png, \*.jpeg etc. as routing rule for Cloudfront to distinguish between assets and pages.
174-
- Add command for guessing version bumps from git commits & keywords, existing solutions are horendously huge, we just need a simple version bumping.
175-
176175
# Disclaimer
177176

178177
At this point, advanced features were not tested with this setup. This includes:
@@ -184,3 +183,5 @@ At this point, advanced features were not tested with this setup. This includes:
184183
- custom babel configuration.
185184

186185
I am looking for advanced projects implementing those features, so we can test them out! Reach out to me!
186+
187+

Diff for: lib/cli.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ program
3535
.option(
3636
'--handlerPath',
3737
'Path to custom handler to be used to handle ApiGw events. By default this is provided for you.',
38-
path.resolve(scriptDir, './../server-handler/index.js'),
38+
path.resolve(scriptDir, './server-handler.js'),
3939
)
4040
.option(
4141
'--outputFolder',
@@ -110,6 +110,12 @@ program
110110
path: '**/*',
111111
ignore: ['**/node_modules/**', '*.zip'],
112112
},
113+
{
114+
// Ensure hidden files are included.
115+
isGlob: true,
116+
cwd: standaloneFolder,
117+
path: '.*/**/*',
118+
},
113119
{
114120
isFile: true,
115121
path: handlerPath,

Diff for: lib/contruct.ts renamed to lib/construct.ts

+17-4
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import { Code, Function, LayerVersion, Runtime } from 'aws-cdk-lib/aws-lambda'
66
import { Bucket, BucketAccessControl } from 'aws-cdk-lib/aws-s3'
77
import { BucketDeployment, Source } from 'aws-cdk-lib/aws-s3-deployment'
88
import { Construct } from 'constructs'
9+
import packageJson from '../package.json'
910

10-
import { imageHandlerZipPath, sharpLayerZipPath } from './consts'
11+
import { imageHandlerZipPath, sharpLayerZipPath, nextLayerZipPath } from './consts'
1112

1213
interface NextConstructProps extends StackProps {
1314
customServerHandler?: string
@@ -17,6 +18,7 @@ interface NextConstructProps extends StackProps {
1718

1819
imageHandlerZipPath?: string
1920
sharpLayerZipPath?: string
21+
nextLayerZipPath?: string
2022
codeZipPath: string
2123
dependenciesZipPath: string
2224
assetsZipPath: string
@@ -37,10 +39,21 @@ export class NextStandaloneStack extends Stack {
3739
})
3840

3941
const sharpLayer = new LayerVersion(this, 'SharpLayer', {
40-
code: Code.fromAsset(props.sharpLayerZipPath ?? sharpLayerZipPath, { assetHash: 'static', assetHashType: AssetHashType.CUSTOM }),
42+
code: Code.fromAsset(props.sharpLayerZipPath ?? sharpLayerZipPath, {
43+
assetHash: `static-sharp-${packageJson.version}`,
44+
assetHashType: AssetHashType.CUSTOM,
45+
}),
46+
})
47+
48+
const nextLayer = new LayerVersion(this, 'NextLayer', {
49+
code: Code.fromAsset(props.nextLayerZipPath ?? nextLayerZipPath, {
50+
assetHash: `static-next-${packageJson.version}`,
51+
assetHashType: AssetHashType.CUSTOM,
52+
}),
4153
})
4254

4355
const assetsBucket = new Bucket(this, 'NextAssetsBucket', {
56+
// @NOTE: Considering not having public ACL.
4457
publicReadAccess: true,
4558
autoDeleteObjects: true,
4659
removalPolicy: RemovalPolicy.DESTROY,
@@ -50,7 +63,7 @@ export class NextStandaloneStack extends Stack {
5063
code: Code.fromAsset(props.codeZipPath, { followSymlinks: SymlinkFollowMode.NEVER }),
5164
runtime: Runtime.NODEJS_16_X,
5265
handler: props.customServerHandler ?? 'handler.handler',
53-
layers: [depsLayer],
66+
layers: [depsLayer, nextLayer],
5467
// No need for big memory as image handling is done elsewhere.
5568
memorySize: 512,
5669
timeout: Duration.seconds(15),
@@ -60,7 +73,7 @@ export class NextStandaloneStack extends Stack {
6073
code: Code.fromAsset(props.imageHandlerZipPath ?? imageHandlerZipPath),
6174
runtime: Runtime.NODEJS_16_X,
6275
handler: props.customImageHandler ?? 'index.handler',
63-
layers: [sharpLayer],
76+
layers: [sharpLayer, nextLayer],
6477
memorySize: 1024,
6578
timeout: Duration.seconds(10),
6679
environment: {

Diff for: lib/consts.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import packageJson from '../package.json'
1+
import path from 'path'
22

3-
export const sharpLayerZipPath = require.resolve(packageJson.name + './sharp-layer/zip')
4-
export const imageHandlerZipPath = require.resolve(packageJson.name + './image-handler/zip')
5-
export const serverHandlerZipPath = require.resolve(packageJson.name + './server-handler/zip')
3+
export const sharpLayerZipPath = path.resolve(__dirname, './sharp-layer.zip')
4+
export const imageHandlerZipPath = path.resolve(__dirname, './image-handler.zip')
5+
export const serverHandlerZipPath = path.resolve(__dirname, './server-handler.zip')
6+
export const nextLayerZipPath = path.resolve(__dirname, './next-layer.zip')

Diff for: lib/index.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
1-
export { NextStandaloneStack } from './contruct'
1+
export { NextStandaloneStack } from './construct'
22
export { imageHandlerZipPath, serverHandlerZipPath, sharpLayerZipPath } from './consts'
3+
4+
export { handler as serverHandler } from './standalone/server-handler'
5+
export { handler as imageHandler } from './standalone/image-handler'

0 commit comments

Comments
 (0)