'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 constructs = require('constructs');
var path = require('path');
var clientS3 = require('@aws-sdk/client-s3');
require('archiver');
var fs = require('fs');
require('replace-in-file');
var crypto = require('crypto');
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');

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

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

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');

// 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,
    },
];
const BUFFER_SIZE = 8192;
const md5FileSync = (path) => {
    const fd = fs.openSync(path, 'r');
    const hash = crypto__default["default"].createHash('md5');
    const buffer = Buffer.alloc(BUFFER_SIZE);
    try {
        let bytesRead;
        do {
            bytesRead = fs.readSync(fd, buffer, 0, BUFFER_SIZE, null);
            hash.update(buffer.subarray(0, bytesRead));
        } while (bytesRead === BUFFER_SIZE);
    }
    finally {
        fs.closeSync(fd);
    }
    return hash.digest('hex');
};

class NextStandaloneConstruct extends constructs.Construct {
    cfnDistro;
    serverLambda;
    imageLambda;
    serverApigatewayProxy;
    imageApigatewayProxy;
    region;
    constructor(scope, id, props) {
        super(scope, id);
        const config = {
            sharpLayerZipPath: sharpLayerZipPath,
            nextLayerZipPath: nextLayerZipPath,
            imageHandlerZipPath: imageHandlerZipPath,
            ...props,
        };
        this.region = awsCdkLib.Stack.of(scope).region;
        const depsLayer = new awsLambda.LayerVersion(this, 'DepsLayer', {
            code: awsLambda.Code.fromAsset(props.dependenciesZipPath, {
                assetHash: md5FileSync(props.dependenciesZipPath),
                assetHashType: awsCdkLib.AssetHashType.CUSTOM,
            }),
        });
        const sharpLayer = new awsLambda.LayerVersion(this, 'SharpLayer', {
            code: awsLambda.Code.fromAsset(config.sharpLayerZipPath, {
                assetHash: md5FileSync(config.sharpLayerZipPath),
                assetHashType: awsCdkLib.AssetHashType.CUSTOM,
            }),
        });
        const nextLayer = new awsLambda.LayerVersion(this, 'NextLayer', {
            code: awsLambda.Code.fromAsset(config.nextLayerZipPath, {
                assetHash: md5FileSync(config.nextLayerZipPath),
                assetHashType: awsCdkLib.AssetHashType.CUSTOM,
            }),
        });
        const assetsBucket = new awsS3.Bucket(this, 'NextAssetsBucket', {
            // Those settings are necessary for bucket to be removed on stack removal.
            removalPolicy: awsCdkLib.RemovalPolicy.DESTROY,
            autoDeleteObjects: true,
            // @NOTE: Considering not having public ACL.
            publicReadAccess: true,
        });
        this.serverLambda = new awsLambda.Function(this, 'DefaultNextJs', {
            code: awsLambda.Code.fromAsset(config.codeZipPath, {
                followSymlinks: awsCdkLib.SymlinkFollowMode.NEVER,
                assetHash: md5FileSync(config.codeZipPath),
                assetHashType: awsCdkLib.AssetHashType.CUSTOM,
            }),
            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(config.imageHandlerZipPath, {
                assetHash: md5FileSync(config.imageHandlerZipPath),
                assetHashType: awsCdkLib.AssetHashType.CUSTOM,
            }),
            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,
            accessControl: awsS3.BucketAccessControl.PUBLIC_READ,
            sources: [
                awsS3Deployment.Source.asset(config.assetsZipPath, {
                    assetHashType: awsCdkLib.AssetHashType.CUSTOM,
                    assetHash: md5FileSync(config.assetsZipPath),
                }),
            ],
            // Invalidate all paths after deployment.
            distribution: this.cfnDistro,
            distributionPaths: ['/*'],
        });
        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 });
        new awsCdkLib.CfnOutput(this, 'nextConstructRegion', { value: this.region });
    }
    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 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.NextStandaloneConstruct = NextStandaloneConstruct;
exports.imageHandler = handler;
exports.imageHandlerZipPath = imageHandlerZipPath;
exports.serverHandler = handler$1;
exports.serverHandlerZipPath = serverHandlerZipPath;
exports.sharpLayerZipPath = sharpLayerZipPath;
