Skip to content

Commit

Permalink
feat: server-handler and packaging for nextjs (#8)
Browse files Browse the repository at this point in the history
* feat: server-handler and packaging for nextjs

* doc: readme heading improved

Co-authored-by: Jan Soukup <soukup@u.plus>
  • Loading branch information
sladg and sladg authored Aug 22, 2022
1 parent fa5db07 commit bbe5b69
Show file tree
Hide file tree
Showing 8 changed files with 250 additions and 65 deletions.
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
16
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# NextJS Image Optimizer Handler
# NextJS Lambda Utils

This is a wrapper for `next/server/image-optimizer` allowing to use S3.

It is intended to be used with `nextjs` deployments to Lambda.
This is a set of utils needed for deploying NextJS into AWS Lambda.
It includes a wrapper for `next/server/image-optimizer` allowing to use S3.
And includes CLI and custom server handler to integrate with ApiGw.

## Usage

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

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

This is part of NextJS to Lambda deployment process. More info to follow.

## @TODO: Add Server-handler description
16 changes: 16 additions & 0 deletions lib/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#!/usr/bin/env node
import { exec as child_exec } from "child_process"
import util from "util"

const exec = util.promisify(child_exec)

// @TODO: Ensure path exists.
// @TODO: Ensure.next folder exists with standalone folder inside.

const run = async () => {
console.log("Starting packaging of your NextJS project!")
await exec("chmod +x ./pack-nextjs.sh && ./pack-nextjs.sh").catch(console.error)
console.log("Your NextJS project was succefully prepared for Lambda.")
}

run()
32 changes: 18 additions & 14 deletions lib/index.ts → lib/image-handler.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
// Ensure NODE_ENV is set to production
process.env.NODE_ENV = 'production'
process.env.NODE_ENV = "production"
// Set NEXT_SHARP_PATH environment variable
// ! Make sure this comes before the fist import
process.env.NEXT_SHARP_PATH = require.resolve('sharp')
process.env.NEXT_SHARP_PATH = require.resolve("sharp")

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

const sourceBucket = process.env.S3_SOURCE_BUCKET ?? undefined

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

const imageParams = ImageOptimizerCache.validateParams({ headers: event.headers } as any, event.queryStringParameters!, nextConfig, false)
const imageParams = ImageOptimizerCache.validateParams(
{ headers: event.headers } as any,
event.queryStringParameters!,
nextConfig,
false
)

if ('errorMessage' in imageParams) {
if ("errorMessage" in imageParams) {
throw new Error(imageParams.errorMessage)
}

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

console.log(optimizedResult)

return {
statusCode: 200,
body: optimizedResult.buffer.toString('base64'),
body: optimizedResult.buffer.toString("base64"),
isBase64Encoded: true,
headers: { Vary: 'Accept', 'Content-Type': optimizedResult.contentType },
headers: { Vary: "Accept", "Content-Type": optimizedResult.contentType },
}
} catch (error: any) {
console.error(error)
Expand Down
36 changes: 36 additions & 0 deletions lib/server-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
process.env.NODE_ENV = "production"
process.chdir(__dirname)

import NextServer, { Options } from "next/dist/server/next-server"
import type { NextIncomingMessage } from "next/dist/server/request-meta"
import slsHttp from "serverless-http"
import path from "path"
import { ServerResponse } from "http"

// This will be loaded from custom config parsed via CLI.
const nextConf = require(`${process.env.NEXT_CONFIG_FILE ?? "./config.json"}`)

// Make sure commands gracefully respect termination signals (e.g. from Docker)
// Allow the graceful termination to be manually configurable
if (!process.env.NEXT_MANUAL_SIG_HANDLE) {
process.on("SIGTERM", () => process.exit(0))
process.on("SIGINT", () => process.exit(0))
}

const config: Options = {
hostname: "localhost",
port: Number(process.env.PORT) || 3000,
dir: path.join(__dirname),
dev: false,
customServer: false,
conf: nextConf,
}

const nextHandler = new NextServer(config).getRequestHandler()

const server = slsHttp(async (req: NextIncomingMessage, res: ServerResponse) => {
await nextHandler(req, res)
// @TODO: Add error handler.
})

export const handler = server
99 changes: 99 additions & 0 deletions pack-nextjs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#!/bin/sh
set -e

# Current root for reference.
MY_ROOT=$(pwd)

# Folder where zip files will be outputed for CDK to pickup.
OUTPUT_PATH=next.out

# Name of folder where public files are located.
# Keep in mind that in order to be able to serve those files without references in next (so files)
# such as webmanifest, icons, etc. you need to nest them in public/assets folder as asset is key
# used to distinguist pages from public assets.
PUBLIC_FOLDER=public

HANDLER_PATH=$MY_ROOT/dist/server-handler.js
STANDALONE_PATH=$MY_ROOT/.next/standalone

# This is a folder prefix where layers are mounted in Lambda.
# Dependencies are mounted in /opt/nodejs and assets in /opt/assets.
LAMBDA_LAYER_FOLDER=opt

# This is a setup for parsing next server configuration from standalone server.js file.
# Webpack is used as a keywork for identifying correct line to pick.
NEXT_CONFIG=
GREP_BY=webpack

echo "My root is: $MY_ROOT"

echo "Cleaning possible left-overs."
rm -rf $MY_ROOT/$OUTPUT_PATH

echo "Creating output folder."
mkdir -p $MY_ROOT/$OUTPUT_PATH

#
# -------------------------- Create deps layer --------------------------
echo "Creating dependencies layer."
DEPS_FOLDER=$MY_ROOT/$OUTPUT_PATH/nodejs
NODE_FOLDER=$STANDALONE_PATH/node_modules

mkdir -p $DEPS_FOLDER

cp -r $NODE_FOLDER $DEPS_FOLDER

echo "Zipping dependencies."
cd $MY_ROOT/$OUTPUT_PATH

# Zip dependendencies, recursive & quite.
zip -r -q -m ./dependenciesLayer.zip ./nodejs

#
# -------------------------- Create assets layer --------------------------
echo "Creating assets layer."
ASSETS_FOLDER=$MY_ROOT/$OUTPUT_PATH/assets

mkdir -p $ASSETS_FOLDER
mkdir -p $ASSETS_FOLDER/_next/static
cp -r $MY_ROOT/.next/static/* $ASSETS_FOLDER/_next/static/
cp -r $MY_ROOT/$PUBLIC_FOLDER/* $ASSETS_FOLDER/

echo "Zipping assets."
cd $ASSETS_FOLDER

# Zip assets, recursive & quite.
zip -r -q -m $MY_ROOT/$OUTPUT_PATH/assetsLayer.zip ./

#
# -------------------------- Create code layer --------------------------

echo "Creating code layer."
CODE_FOLDER=$MY_ROOT/$OUTPUT_PATH/code

mkdir -p $CODE_FOLDER

# Copy code files and other helpers.
# Don't include * in the end of rsync path as it would omit .next folder.
rsync -a --exclude='node_modules' --exclude '*.zip' $STANDALONE_PATH/ $CODE_FOLDER
cp $HANDLER_PATH $CODE_FOLDER/handler.js

# Create layer symlink
ln -s /$LAMBDA_LAYER_FOLDER/nodejs/node_modules $CODE_FOLDER/node_modules
ln -s /$LAMBDA_LAYER_FOLDER/assets/public $CODE_FOLDER/public

#
# -------------------------- Extract nextjs config --------------------------
NEXT_SERVER_PATH=$STANDALONE_PATH/server.js
while read line; do
if echo "$line" | grep -p -q $GREP_BY; then NEXT_CONFIG=$line; fi
done <$NEXT_SERVER_PATH

# Remove trailing "," and beginning of line "conf:"
echo $NEXT_CONFIG | sed 's/.$//' | sed 's/conf:/ /g' >$CODE_FOLDER/config.json

echo "Zipping code."
cd $CODE_FOLDER

# Zip code, recursive, don't resolve symlinks.
zip -r -q -m --symlinks $MY_ROOT/$OUTPUT_PATH/code.zip ./
87 changes: 53 additions & 34 deletions package-lock.json

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

Loading

0 comments on commit bbe5b69

Please sign in to comment.