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

Commit bbe5b69

Browse files
authored
feat: server-handler and packaging for nextjs (#8)
* feat: server-handler and packaging for nextjs * doc: readme heading improved Co-authored-by: Jan Soukup <[email protected]>
1 parent fa5db07 commit bbe5b69

8 files changed

+250
-65
lines changed

Diff for: .nvmrc

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
16

Diff for: README.md

+8-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
# NextJS Image Optimizer Handler
1+
# NextJS Lambda Utils
22

3-
This is a wrapper for `next/server/image-optimizer` allowing to use S3.
4-
5-
It is intended to be used with `nextjs` deployments to Lambda.
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.
66

77
## Usage
88

@@ -13,7 +13,7 @@ Use
1313
const sharpLayer: LayerVersion
1414
const assetsBucket: Bucket
1515
16-
const code = require.resolve('@sladg/nextjs-image-optimizer-handler/zip')
16+
const code = require.resolve('@sladg/nextjs-lambda/image-handler/zip')
1717
1818
const imageOptimizerFn = new Function(this, 'LambdaFunction', {
1919
code: Code.fromAsset(code),
@@ -38,7 +38,7 @@ Besides handler (wrapper) itself, underlying NextJS also requires sharp binaries
3838
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.
3939

4040
```
41-
const code = require.resolve('@sladg/nextjs-image-optimizer-handler/sharp-layer')
41+
const code = require.resolve('@sladg/nextjs-lambda/sharp-layer')
4242
4343
const sharpLayer = new LayerVersion(this, 'SharpLayer', {
4444
code: Code.fromAsset(code)
@@ -48,3 +48,5 @@ const sharpLayer = new LayerVersion(this, 'SharpLayer', {
4848
## Notes
4949

5050
This is part of NextJS to Lambda deployment process. More info to follow.
51+
52+
## @TODO: Add Server-handler description

Diff for: lib/cli.ts

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#!/usr/bin/env node
2+
import { exec as child_exec } from "child_process"
3+
import util from "util"
4+
5+
const exec = util.promisify(child_exec)
6+
7+
// @TODO: Ensure path exists.
8+
// @TODO: Ensure.next folder exists with standalone folder inside.
9+
10+
const run = async () => {
11+
console.log("Starting packaging of your NextJS project!")
12+
await exec("chmod +x ./pack-nextjs.sh && ./pack-nextjs.sh").catch(console.error)
13+
console.log("Your NextJS project was succefully prepared for Lambda.")
14+
}
15+
16+
run()

Diff for: lib/index.ts renamed to lib/image-handler.ts

+18-14
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
// Ensure NODE_ENV is set to production
2-
process.env.NODE_ENV = 'production'
1+
process.env.NODE_ENV = "production"
32
// Set NEXT_SHARP_PATH environment variable
43
// ! Make sure this comes before the fist import
5-
process.env.NEXT_SHARP_PATH = require.resolve('sharp')
4+
process.env.NEXT_SHARP_PATH = require.resolve("sharp")
65

7-
import type { APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2 } from 'aws-lambda'
8-
import { defaultConfig, NextConfigComplete } from 'next/dist/server/config-shared'
9-
import { imageOptimizer as nextImageOptimizer, ImageOptimizerCache } from 'next/dist/server/image-optimizer'
10-
import { ImageConfigComplete } from 'next/dist/shared/lib/image-config'
11-
import { normalizeHeaders, requestHandler } from './utils'
6+
import type { APIGatewayProxyEventV2, APIGatewayProxyStructuredResultV2 } from "aws-lambda"
7+
import { defaultConfig, NextConfigComplete } from "next/dist/server/config-shared"
8+
import { imageOptimizer as nextImageOptimizer, ImageOptimizerCache } from "next/dist/server/image-optimizer"
9+
import { ImageConfigComplete } from "next/dist/shared/lib/image-config"
10+
import { normalizeHeaders, requestHandler } from "./utils"
1211

1312
const sourceBucket = process.env.S3_SOURCE_BUCKET ?? undefined
1413

@@ -29,12 +28,17 @@ const nextConfig = {
2928
const optimizer = async (event: APIGatewayProxyEventV2): Promise<APIGatewayProxyStructuredResultV2> => {
3029
try {
3130
if (!sourceBucket) {
32-
throw new Error('Bucket name must be defined!')
31+
throw new Error("Bucket name must be defined!")
3332
}
3433

35-
const imageParams = ImageOptimizerCache.validateParams({ headers: event.headers } as any, event.queryStringParameters!, nextConfig, false)
34+
const imageParams = ImageOptimizerCache.validateParams(
35+
{ headers: event.headers } as any,
36+
event.queryStringParameters!,
37+
nextConfig,
38+
false
39+
)
3640

37-
if ('errorMessage' in imageParams) {
41+
if ("errorMessage" in imageParams) {
3842
throw new Error(imageParams.errorMessage)
3943
}
4044

@@ -43,16 +47,16 @@ const optimizer = async (event: APIGatewayProxyEventV2): Promise<APIGatewayProxy
4347
{} as any, // res object is not necessary as it's not actually used.
4448
imageParams,
4549
nextConfig,
46-
requestHandler(sourceBucket),
50+
requestHandler(sourceBucket)
4751
)
4852

4953
console.log(optimizedResult)
5054

5155
return {
5256
statusCode: 200,
53-
body: optimizedResult.buffer.toString('base64'),
57+
body: optimizedResult.buffer.toString("base64"),
5458
isBase64Encoded: true,
55-
headers: { Vary: 'Accept', 'Content-Type': optimizedResult.contentType },
59+
headers: { Vary: "Accept", "Content-Type": optimizedResult.contentType },
5660
}
5761
} catch (error: any) {
5862
console.error(error)

Diff for: lib/server-handler.ts

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
process.env.NODE_ENV = "production"
2+
process.chdir(__dirname)
3+
4+
import NextServer, { Options } from "next/dist/server/next-server"
5+
import type { NextIncomingMessage } from "next/dist/server/request-meta"
6+
import slsHttp from "serverless-http"
7+
import path from "path"
8+
import { ServerResponse } from "http"
9+
10+
// This will be loaded from custom config parsed via CLI.
11+
const nextConf = require(`${process.env.NEXT_CONFIG_FILE ?? "./config.json"}`)
12+
13+
// Make sure commands gracefully respect termination signals (e.g. from Docker)
14+
// Allow the graceful termination to be manually configurable
15+
if (!process.env.NEXT_MANUAL_SIG_HANDLE) {
16+
process.on("SIGTERM", () => process.exit(0))
17+
process.on("SIGINT", () => process.exit(0))
18+
}
19+
20+
const config: Options = {
21+
hostname: "localhost",
22+
port: Number(process.env.PORT) || 3000,
23+
dir: path.join(__dirname),
24+
dev: false,
25+
customServer: false,
26+
conf: nextConf,
27+
}
28+
29+
const nextHandler = new NextServer(config).getRequestHandler()
30+
31+
const server = slsHttp(async (req: NextIncomingMessage, res: ServerResponse) => {
32+
await nextHandler(req, res)
33+
// @TODO: Add error handler.
34+
})
35+
36+
export const handler = server

Diff for: pack-nextjs.sh

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
#!/bin/sh
2+
set -e
3+
4+
# Current root for reference.
5+
MY_ROOT=$(pwd)
6+
7+
# Folder where zip files will be outputed for CDK to pickup.
8+
OUTPUT_PATH=next.out
9+
10+
# Name of folder where public files are located.
11+
# Keep in mind that in order to be able to serve those files without references in next (so files)
12+
# such as webmanifest, icons, etc. you need to nest them in public/assets folder as asset is key
13+
# used to distinguist pages from public assets.
14+
PUBLIC_FOLDER=public
15+
16+
HANDLER_PATH=$MY_ROOT/dist/server-handler.js
17+
STANDALONE_PATH=$MY_ROOT/.next/standalone
18+
19+
# This is a folder prefix where layers are mounted in Lambda.
20+
# Dependencies are mounted in /opt/nodejs and assets in /opt/assets.
21+
LAMBDA_LAYER_FOLDER=opt
22+
23+
# This is a setup for parsing next server configuration from standalone server.js file.
24+
# Webpack is used as a keywork for identifying correct line to pick.
25+
NEXT_CONFIG=
26+
GREP_BY=webpack
27+
28+
echo "My root is: $MY_ROOT"
29+
30+
echo "Cleaning possible left-overs."
31+
rm -rf $MY_ROOT/$OUTPUT_PATH
32+
33+
echo "Creating output folder."
34+
mkdir -p $MY_ROOT/$OUTPUT_PATH
35+
36+
#
37+
# -------------------------- Create deps layer --------------------------
38+
echo "Creating dependencies layer."
39+
DEPS_FOLDER=$MY_ROOT/$OUTPUT_PATH/nodejs
40+
NODE_FOLDER=$STANDALONE_PATH/node_modules
41+
42+
mkdir -p $DEPS_FOLDER
43+
44+
cp -r $NODE_FOLDER $DEPS_FOLDER
45+
46+
echo "Zipping dependencies."
47+
cd $MY_ROOT/$OUTPUT_PATH
48+
49+
# Zip dependendencies, recursive & quite.
50+
zip -r -q -m ./dependenciesLayer.zip ./nodejs
51+
52+
#
53+
# -------------------------- Create assets layer --------------------------
54+
echo "Creating assets layer."
55+
ASSETS_FOLDER=$MY_ROOT/$OUTPUT_PATH/assets
56+
57+
mkdir -p $ASSETS_FOLDER
58+
mkdir -p $ASSETS_FOLDER/_next/static
59+
cp -r $MY_ROOT/.next/static/* $ASSETS_FOLDER/_next/static/
60+
cp -r $MY_ROOT/$PUBLIC_FOLDER/* $ASSETS_FOLDER/
61+
62+
echo "Zipping assets."
63+
cd $ASSETS_FOLDER
64+
65+
# Zip assets, recursive & quite.
66+
zip -r -q -m $MY_ROOT/$OUTPUT_PATH/assetsLayer.zip ./
67+
68+
#
69+
# -------------------------- Create code layer --------------------------
70+
71+
echo "Creating code layer."
72+
CODE_FOLDER=$MY_ROOT/$OUTPUT_PATH/code
73+
74+
mkdir -p $CODE_FOLDER
75+
76+
# Copy code files and other helpers.
77+
# Don't include * in the end of rsync path as it would omit .next folder.
78+
rsync -a --exclude='node_modules' --exclude '*.zip' $STANDALONE_PATH/ $CODE_FOLDER
79+
cp $HANDLER_PATH $CODE_FOLDER/handler.js
80+
81+
# Create layer symlink
82+
ln -s /$LAMBDA_LAYER_FOLDER/nodejs/node_modules $CODE_FOLDER/node_modules
83+
ln -s /$LAMBDA_LAYER_FOLDER/assets/public $CODE_FOLDER/public
84+
85+
#
86+
# -------------------------- Extract nextjs config --------------------------
87+
NEXT_SERVER_PATH=$STANDALONE_PATH/server.js
88+
while read line; do
89+
if echo "$line" | grep -p -q $GREP_BY; then NEXT_CONFIG=$line; fi
90+
done <$NEXT_SERVER_PATH
91+
92+
# Remove trailing "," and beginning of line "conf:"
93+
echo $NEXT_CONFIG | sed 's/.$//' | sed 's/conf:/ /g' >$CODE_FOLDER/config.json
94+
95+
echo "Zipping code."
96+
cd $CODE_FOLDER
97+
98+
# Zip code, recursive, don't resolve symlinks.
99+
zip -r -q -m --symlinks $MY_ROOT/$OUTPUT_PATH/code.zip ./

Diff for: package-lock.json

+53-34
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)