'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

var awsApigatewayv2Alpha = require('@aws-cdk/aws-apigatewayv2-alpha');
var awsApigatewayv2IntegrationsAlpha = require('@aws-cdk/aws-apigatewayv2-integrations-alpha');
var awsCdkLib = require('aws-cdk-lib');
var awsCloudfront = require('aws-cdk-lib/aws-cloudfront');
var awsLambda = require('aws-cdk-lib/aws-lambda');
var awsS3 = require('aws-cdk-lib/aws-s3');
var awsS3Deployment = require('aws-cdk-lib/aws-s3-deployment');
var path = require('path');
var NextServer = require('next/dist/server/next-server');
var slsHttp = require('serverless-http');
var configShared = require('next/dist/server/config-shared');
var imageOptimizer = require('next/dist/server/image-optimizer');
var clientS3 = require('@aws-sdk/client-s3');
require('archiver');
require('fs');
require('replace-in-file');

function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }

var path__default = /*#__PURE__*/_interopDefaultLegacy(path);
var NextServer__default = /*#__PURE__*/_interopDefaultLegacy(NextServer);
var slsHttp__default = /*#__PURE__*/_interopDefaultLegacy(slsHttp);

var name = "@sladg/nextjs-lambda";
var version = "3.2.3";
var description = "Plug-and-play lambda for replacing default NextJS image optimization handler.";
var repository = {
	type: "git",
	url: "https://github.com/sladg/nextjs-lambda"
};
var license = "MIT";
var author = "Jan Soukup <jan@ssoukup.com>";
var exports$1 = "./dist/main.js";
var types = "./dist/main.d.ts";
var bin = {
	"next-utils": "./dist/cli.js"
};
var files = [
	"dist",
	"scripts"
];
var scripts = {
	prebuild: "rm -rf dist",
	build: "npm run build:main && npm run build:sharp && npm run build:next",
	"build:main": "rollup -c",
	"build:next": "./scripts/build-next-layer.sh",
	"build:sharp": "./scripts/build-sharp-layer.sh",
	release: "node ./dist/cli.js shipit --gitUser @sladg --gitEmail jan@ssoukup.com"
};
var dependencies = {
	"@types/archiver": "^5.3.1",
	"@types/aws-lambda": "^8.10.101",
	"@types/node": "^18.6.3",
	archiver: "^5.3.1",
	commander: "^9.4.0",
	"replace-in-file": "^6.3.5",
	"serverless-http": "^3.0.2",
	"simple-git": "^3.13.0"
};
var devDependencies = {
	"@rollup/plugin-json": "^4.1.0",
	"@tsconfig/node16": "^1.0.3",
	"adm-zip": "^0.5.9",
	critters: "^0.0.16",
	esbuild: "^0.15.6",
	rollup: "^2.79.0",
	"rollup-plugin-ts": "^3.0.2"
};
var peerDependencies = {
	"@aws-cdk/aws-apigatewayv2-alpha": "^2.39.1-alpha",
	"@aws-cdk/aws-apigatewayv2-integrations-alpha": "^2.39.1-alpha",
	"@aws-sdk/client-s3": "^3.142",
	"aws-cdk-lib": "^2.39",
	constructs: "^10",
	next: "^12",
	typescript: "^4.8"
};
var engines = {
	node: ">=16"
};
var engineStrict = true;
var packageJson = {
	name: name,
	version: version,
	description: description,
	repository: repository,
	license: license,
	author: author,
	exports: exports$1,
	types: types,
	bin: bin,
	files: files,
	scripts: scripts,
	dependencies: dependencies,
	devDependencies: devDependencies,
	peerDependencies: peerDependencies,
	engines: engines,
	engineStrict: engineStrict
};

const sharpLayerZipPath = path__default["default"].resolve(__dirname, './sharp-layer.zip');
const imageHandlerZipPath = path__default["default"].resolve(__dirname, './image-handler.zip');
const serverHandlerZipPath = path__default["default"].resolve(__dirname, './server-handler.zip');
const nextLayerZipPath = path__default["default"].resolve(__dirname, './next-layer.zip');

class NextStandaloneStack extends awsCdkLib.Stack {
    cfnDistro;
    serverLambda;
    imageLambda;
    serverApigatewayProxy;
    imageApigatewayProxy;
    constructor(scope, id, props) {
        super(scope, id, props);
        const depsLayer = new awsLambda.LayerVersion(this, 'DepsLayer', {
            code: awsLambda.Code.fromAsset(props.dependenciesZipPath),
        });
        const sharpLayer = new awsLambda.LayerVersion(this, 'SharpLayer', {
            code: awsLambda.Code.fromAsset(props.sharpLayerZipPath ?? sharpLayerZipPath, {
                assetHash: `static-sharp-${packageJson.version}`,
                assetHashType: awsCdkLib.AssetHashType.CUSTOM,
            }),
        });
        const nextLayer = new awsLambda.LayerVersion(this, 'NextLayer', {
            code: awsLambda.Code.fromAsset(props.nextLayerZipPath ?? nextLayerZipPath, {
                assetHash: `static-next-${packageJson.version}`,
                assetHashType: awsCdkLib.AssetHashType.CUSTOM,
            }),
        });
        const assetsBucket = new awsS3.Bucket(this, 'NextAssetsBucket', {
            // @NOTE: Considering not having public ACL.
            publicReadAccess: true,
            autoDeleteObjects: true,
            removalPolicy: awsCdkLib.RemovalPolicy.DESTROY,
        });
        this.serverLambda = new awsLambda.Function(this, 'DefaultNextJs', {
            code: awsLambda.Code.fromAsset(props.codeZipPath, { followSymlinks: awsCdkLib.SymlinkFollowMode.NEVER }),
            runtime: awsLambda.Runtime.NODEJS_16_X,
            handler: props.customServerHandler ?? 'handler.handler',
            layers: [depsLayer, nextLayer],
            // No need for big memory as image handling is done elsewhere.
            memorySize: 512,
            timeout: awsCdkLib.Duration.seconds(15),
        });
        this.imageLambda = new awsLambda.Function(this, 'ImageOptimizationNextJs', {
            code: awsLambda.Code.fromAsset(props.imageHandlerZipPath ?? imageHandlerZipPath),
            runtime: awsLambda.Runtime.NODEJS_16_X,
            handler: props.customImageHandler ?? 'index.handler',
            layers: [sharpLayer, nextLayer],
            memorySize: 1024,
            timeout: awsCdkLib.Duration.seconds(10),
            environment: {
                S3_SOURCE_BUCKET: assetsBucket.bucketName,
            },
        });
        assetsBucket.grantRead(this.imageLambda);
        this.serverApigatewayProxy = new awsApigatewayv2Alpha.HttpApi(this, 'NextJsLambdaProxy', {
            createDefaultStage: true,
            defaultIntegration: new awsApigatewayv2IntegrationsAlpha.HttpLambdaIntegration('LambdaApigwIntegration', this.serverLambda),
        });
        this.imageApigatewayProxy = new awsApigatewayv2Alpha.HttpApi(this, 'ImagesLambdaProxy', {
            createDefaultStage: true,
            defaultIntegration: new awsApigatewayv2IntegrationsAlpha.HttpLambdaIntegration('ImagesApigwIntegration', this.imageLambda),
        });
        this.cfnDistro = new awsCloudfront.CloudFrontWebDistribution(this, 'TestApigwDistro', {
            // Must be set, because cloufront would use index.html which would not match in NextJS routes.
            defaultRootObject: '',
            comment: 'ApiGwLambda Proxy for NextJS',
            viewerCertificate: props.cfnViewerCertificate,
            originConfigs: [
                {
                    // Default behaviour, lambda handles.
                    behaviors: [
                        {
                            allowedMethods: awsCloudfront.CloudFrontAllowedMethods.ALL,
                            isDefaultBehavior: true,
                            forwardedValues: { queryString: true },
                        },
                        {
                            allowedMethods: awsCloudfront.CloudFrontAllowedMethods.ALL,
                            pathPattern: '_next/data/*',
                        },
                    ],
                    customOriginSource: {
                        domainName: `${this.serverApigatewayProxy.apiId}.execute-api.${this.region}.amazonaws.com`,
                    },
                },
                {
                    // Our implementation of image optimization, we are tapping into Next's default route to avoid need for next.config.js changes.
                    behaviors: [
                        {
                            // Should use caching based on query params.
                            allowedMethods: awsCloudfront.CloudFrontAllowedMethods.ALL,
                            pathPattern: '_next/image*',
                            forwardedValues: { queryString: true },
                        },
                    ],
                    customOriginSource: {
                        domainName: `${this.imageApigatewayProxy.apiId}.execute-api.${this.region}.amazonaws.com`,
                    },
                },
                {
                    // Remaining next files (safe-catch) and our assets that are not imported via `next/image`
                    behaviors: [
                        {
                            allowedMethods: awsCloudfront.CloudFrontAllowedMethods.GET_HEAD_OPTIONS,
                            pathPattern: '_next/*',
                        },
                        {
                            allowedMethods: awsCloudfront.CloudFrontAllowedMethods.GET_HEAD_OPTIONS,
                            pathPattern: 'assets/*',
                        },
                    ],
                    s3OriginSource: {
                        s3BucketSource: assetsBucket,
                    },
                },
            ],
        });
        // This can be handled by `aws s3 sync` but we need to ensure invalidation of Cfn after deploy.
        new awsS3Deployment.BucketDeployment(this, 'PublicFilesDeployment', {
            destinationBucket: assetsBucket,
            sources: [awsS3Deployment.Source.asset(props.assetsZipPath)],
            accessControl: awsS3.BucketAccessControl.PUBLIC_READ,
            // Invalidate all paths after deployment.
            distributionPaths: ['/*'],
            distribution: this.cfnDistro,
        });
        new awsCdkLib.CfnOutput(this, 'cfnDistroUrl', { value: this.cfnDistro.distributionDomainName });
        new awsCdkLib.CfnOutput(this, 'cfnDistroId', { value: this.cfnDistro.distributionId });
        new awsCdkLib.CfnOutput(this, 'defaultApiGwUrl', { value: this.serverApigatewayProxy.apiEndpoint });
        new awsCdkLib.CfnOutput(this, 'imagesApiGwUrl', { value: this.imageApigatewayProxy.apiEndpoint });
        new awsCdkLib.CfnOutput(this, 'assetsBucketUrl', { value: assetsBucket.bucketDomainName });
    }
    get cloudfrontDistribution() {
        return this.cfnDistro;
    }
    get serverFn() {
        return this.serverLambda;
    }
    get imageFn() {
        return this.imageLambda;
    }
    get serverApigw() {
        return this.serverApigatewayProxy;
    }
    get imageApigw() {
        return this.imageApigatewayProxy;
    }
}

// ! This is needed for nextjs to correctly resolve.
process.chdir(__dirname);
process.env.NODE_ENV = 'production';
// 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 = {
    hostname: 'localhost',
    port: Number(process.env.PORT) || 3000,
    dir: path__default["default"].join(__dirname),
    dev: false,
    customServer: false,
    conf: nextConf,
};
const getErrMessage = (e) => ({ message: 'Server failed to respond.', details: e });
const nextHandler = new NextServer__default["default"](config).getRequestHandler();
const server = slsHttp__default["default"](async (req, res) => {
    await nextHandler(req, res).catch((e) => {
        // Log into Cloudwatch for easier debugging.
        console.error(`NextJS request failed due to:`);
        console.error(e);
        res.setHeader('Content-Type', 'application/json');
        res.end(JSON.stringify(getErrMessage(e), null, 3));
    });
}, {
    // We have separate function for handling images. Assets are handled by S3.
    binary: false,
});
const handler$1 = server;

// Make header keys lowercase to ensure integrity.
const normalizeHeaders = (headers) => Object.entries(headers).reduce((acc, [key, value]) => ({ ...acc, [key.toLowerCase()]: value }), {});
// Handle fetching of S3 object before optimization happens in nextjs.
const requestHandler = (bucketName) => async (req, res, url) => {
    if (!url) {
        throw new Error('URL is missing from request.');
    }
    // S3 expects keys without leading `/`
    const trimmedKey = url.href.startsWith('/') ? url.href.substring(1) : url.href;
    const client = new clientS3.S3Client({});
    const response = await client.send(new clientS3.GetObjectCommand({ Bucket: bucketName, Key: trimmedKey }));
    if (!response.Body) {
        throw new Error(`Could not fetch image ${trimmedKey} from bucket.`);
    }
    const stream = response.Body;
    const data = await new Promise((resolve, reject) => {
        const chunks = [];
        stream.on('data', (chunk) => chunks.push(chunk));
        stream.once('end', () => resolve(Buffer.concat(chunks)));
        stream.once('error', reject);
    });
    res.statusCode = 200;
    if (response.ContentType) {
        res.setHeader('Content-Type', response.ContentType);
    }
    if (response.CacheControl) {
        res.setHeader('Cache-Control', response.CacheControl);
    }
    res.write(data);
    res.end();
};
var BumpType;
(function (BumpType) {
    BumpType["Patch"] = "patch";
    BumpType["Minor"] = "minor";
    BumpType["Major"] = "major";
})(BumpType || (BumpType = {}));
[
    {
        test: /(.*)(fix:|fix\((.*)\):)/,
        bump: BumpType.Patch,
    },
    {
        test: /(.*)(chore:|chore\((.*)\):)/,
        bump: BumpType.Patch,
    },
    {
        test: /(.*)(feat:|feat\((.*)\):|feature:|feature\((.*)\):)/,
        bump: BumpType.Minor,
    },
    {
        test: /(.*)(perf:|perf\((.*)\):)/,
        bump: BumpType.Minor,
    },
    {
        test: /(.*)(ref:|ref\((.*)\):|refactor:|refactor\((.*)\):|refactoring:|refactoring\((.*)\):)/,
        bump: BumpType.Minor,
    },
    {
        test: /(.*)(revert:|revert\((.*)\):)/,
        bump: BumpType.Patch,
    },
    {
        test: /(.*)(style:|style\((.*)\):)/,
        bump: BumpType.Minor,
    },
    {
        test: /(.*)(test:|test\((.*)\):|tests:|tests\((.*)\):)/,
        bump: BumpType.Minor,
    },
    {
        test: /(.*)(ci:|ci\((.*)\):)/,
        bump: BumpType.Minor,
    },
    {
        test: /(.*)(build:|build\((.*)\):)/,
        bump: BumpType.Minor,
    },
    {
        test: /(.*)(docs:|docs\((.*)\):|doc:|doc\((.*)\):)/,
        bump: BumpType.Minor,
    },
    {
        test: 'BREAKING CHANGE',
        bump: BumpType.Major,
        scanBody: true,
    },
];

// ! Make sure this comes before the fist import
process.env.NEXT_SHARP_PATH = require.resolve('sharp');
process.env.NODE_ENV = 'production';
const sourceBucket = process.env.S3_SOURCE_BUCKET ?? undefined;
// @TODO: Allow passing params as env vars.
const nextConfig = {
    ...configShared.defaultConfig,
    images: {
        ...configShared.defaultConfig.images,
        // ...(domains && { domains }),
        // ...(deviceSizes && { deviceSizes }),
        // ...(formats && { formats }),
        // ...(imageSizes && { imageSizes }),
        // ...(dangerouslyAllowSVG && { dangerouslyAllowSVG }),
        // ...(contentSecurityPolicy && { contentSecurityPolicy }),
    },
};
const optimizer = async (event) => {
    try {
        if (!sourceBucket) {
            throw new Error('Bucket name must be defined!');
        }
        const imageParams = imageOptimizer.ImageOptimizerCache.validateParams({ headers: event.headers }, event.queryStringParameters, nextConfig, false);
        if ('errorMessage' in imageParams) {
            throw new Error(imageParams.errorMessage);
        }
        const optimizedResult = await imageOptimizer.imageOptimizer({ headers: normalizeHeaders(event.headers) }, {}, // res object is not necessary as it's not actually used.
        imageParams, nextConfig, requestHandler(sourceBucket));
        console.log(optimizedResult);
        return {
            statusCode: 200,
            body: optimizedResult.buffer.toString('base64'),
            isBase64Encoded: true,
            headers: { Vary: 'Accept', 'Content-Type': optimizedResult.contentType },
        };
    }
    catch (error) {
        console.error(error);
        return {
            statusCode: 500,
            body: error?.message || error?.toString() || error,
        };
    }
};
const handler = optimizer;

exports.NextStandaloneStack = NextStandaloneStack;
exports.imageHandler = handler;
exports.imageHandlerZipPath = imageHandlerZipPath;
exports.serverHandler = handler$1;
exports.serverHandlerZipPath = serverHandlerZipPath;
exports.sharpLayerZipPath = sharpLayerZipPath;
