UNPKG

@sladg/nextjs-lambda

Version:

Plug-and-play lambda for replacing default NextJS image optimization handler.

464 lines (449 loc) 20.7 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); // lib/cdk/app.ts var import_imaginex_lambda = require("@sladg/imaginex-lambda"); var import_aws_cdk_lib10 = require("aws-cdk-lib"); var import_path2 = __toESM(require("path")); // lib/cdk/config.ts var import_aws_lambda3 = require("aws-cdk-lib/aws-lambda"); var import_envalid = require("envalid"); // lib/cdk/utils/imageLambda.ts var import_aws_cdk_lib = require("aws-cdk-lib"); var import_aws_lambda = require("aws-cdk-lib/aws-lambda"); var DEFAULT_MEMORY = 512; var DEFAULT_TIMEOUT = 10; var setupImageLambda = (scope, { assetsBucket, codePath, handler: handler2, layerPath, lambdaHash, memory = DEFAULT_MEMORY, timeout = DEFAULT_TIMEOUT }) => { const depsLayer = new import_aws_lambda.LayerVersion(scope, "ImageOptimizationLayer", { code: import_aws_lambda.Code.fromAsset(layerPath, { assetHash: lambdaHash + "_layer", assetHashType: import_aws_cdk_lib.AssetHashType.CUSTOM }) }); const imageLambda = new import_aws_lambda.Function(scope, "ImageOptimizationNextJs", { code: import_aws_lambda.Code.fromAsset(codePath, { assetHash: lambdaHash + "_code", assetHashType: import_aws_cdk_lib.AssetHashType.CUSTOM }), // @NOTE: Make sure to keep python3.8 as binaries seems to be messed for other versions. runtime: import_aws_lambda.Runtime.PYTHON_3_8, handler: handler2, memorySize: memory, timeout: import_aws_cdk_lib.Duration.seconds(timeout), layers: [depsLayer], environment: { S3_BUCKET_NAME: assetsBucket.bucketName } }); assetsBucket.grantRead(imageLambda); new import_aws_cdk_lib.CfnOutput(scope, "imageLambdaArn", { value: imageLambda.functionArn }); return imageLambda; }; // lib/cdk/utils/serverLambda.ts var import_aws_cdk_lib2 = require("aws-cdk-lib"); var import_aws_lambda2 = require("aws-cdk-lib/aws-lambda"); var DEFAULT_MEMORY2 = 1024; var DEFAULT_TIMEOUT2 = 20; var DEFAULT_RUNTIME = import_aws_lambda2.Runtime.NODEJS_16_X; var setupServerLambda = (scope, { basePath, codePath, dependenciesPath, handler: handler2, memory = DEFAULT_MEMORY2, timeout = DEFAULT_TIMEOUT2, runtime = DEFAULT_RUNTIME }) => { const depsLayer = new import_aws_lambda2.LayerVersion(scope, "DepsLayer", { // This folder does not use Custom hash as depenendencies are most likely changing every time we deploy. code: import_aws_lambda2.Code.fromAsset(dependenciesPath) }); const serverLambda = new import_aws_lambda2.Function(scope, "DefaultNextJs", { code: import_aws_lambda2.Code.fromAsset(codePath), runtime, handler: handler2, layers: [depsLayer], // No need for big memory as image handling is done elsewhere. memorySize: memory, timeout: import_aws_cdk_lib2.Duration.seconds(timeout), environment: { // Set env vars based on what's available in environment. ...Object.entries(process.env).filter(([key]) => key.startsWith("NEXT_")).reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}), NEXTJS_LAMBDA_BASE_PATH: basePath } }); new import_aws_cdk_lib2.CfnOutput(scope, "serverLambdaArn", { value: serverLambda.functionArn }); return serverLambda; }; // lib/cdk/config.ts var RuntimeEnum = /* @__PURE__ */ ((RuntimeEnum2) => { RuntimeEnum2["NODEJS_14_X"] = "node14"; RuntimeEnum2["NODEJS_16_X"] = "node16"; RuntimeEnum2["NODEJS_18_X"] = "node18"; return RuntimeEnum2; })(RuntimeEnum || {}); var runtimeMap = { ["node14" /* NODEJS_14_X */]: import_aws_lambda3.Runtime.NODEJS_14_X, ["node16" /* NODEJS_16_X */]: import_aws_lambda3.Runtime.NODEJS_16_X, ["node18" /* NODEJS_18_X */]: import_aws_lambda3.Runtime.NODEJS_18_X }; var RawEnvConfig = (0, import_envalid.cleanEnv)(process.env, { STACK_NAME: (0, import_envalid.str)(), LAMBDA_TIMEOUT: (0, import_envalid.num)({ default: DEFAULT_TIMEOUT2 }), LAMBDA_MEMORY: (0, import_envalid.num)({ default: DEFAULT_MEMORY2 }), LAMBDA_RUNTIME: (0, import_envalid.str)({ default: "node16" /* NODEJS_16_X */, choices: Object.values(RuntimeEnum) }), IMAGE_LAMBDA_TIMEOUT: (0, import_envalid.num)({ default: DEFAULT_TIMEOUT }), IMAGE_LAMBDA_MEMORY: (0, import_envalid.num)({ default: DEFAULT_MEMORY }), CUSTOM_API_DOMAIN: (0, import_envalid.str)({ default: void 0 }), REDIRECT_FROM_APEX: (0, import_envalid.bool)({ default: false }), DOMAIN_NAMES: (0, import_envalid.str)({ default: void 0 }), PROFILE: (0, import_envalid.str)({ default: void 0 }) }); var envConfig = { profile: RawEnvConfig.PROFILE, stackName: RawEnvConfig.STACK_NAME, lambdaMemory: RawEnvConfig.LAMBDA_MEMORY, lambdaTimeout: RawEnvConfig.LAMBDA_TIMEOUT, lambdaRuntime: runtimeMap[RawEnvConfig.LAMBDA_RUNTIME], imageLambdaMemory: RawEnvConfig.IMAGE_LAMBDA_MEMORY, imageLambdaTimeout: RawEnvConfig.IMAGE_LAMBDA_TIMEOUT, customApiDomain: RawEnvConfig.CUSTOM_API_DOMAIN, redirectFromApex: RawEnvConfig.REDIRECT_FROM_APEX, domainNames: RawEnvConfig.DOMAIN_NAMES ? RawEnvConfig.DOMAIN_NAMES.split(",").map((a) => a.trim()) : [] }; // lib/cdk/stack.ts var import_aws_cdk_lib9 = require("aws-cdk-lib"); var import_aws_cloudfront_origins2 = require("aws-cdk-lib/aws-cloudfront-origins"); // lib/cdk/utils/apiGw.ts var import_aws_apigatewayv2_alpha = require("@aws-cdk/aws-apigatewayv2-alpha"); var import_aws_apigatewayv2_integrations_alpha = require("@aws-cdk/aws-apigatewayv2-integrations-alpha"); var import_aws_cdk_lib3 = require("aws-cdk-lib"); var setupApiGateway = (scope, { imageLambda, imageBasePath, serverLambda, serverBasePath }) => { const apiGateway = new import_aws_apigatewayv2_alpha.HttpApi(scope, "ServerProxy"); apiGateway.addRoutes({ path: `${serverBasePath}/{proxy+}`, integration: new import_aws_apigatewayv2_integrations_alpha.HttpLambdaIntegration("LambdaApigwIntegration", serverLambda) }); apiGateway.addRoutes({ path: `${imageBasePath}/{proxy+}`, integration: new import_aws_apigatewayv2_integrations_alpha.HttpLambdaIntegration("ImagesApigwIntegration", imageLambda) }); new import_aws_cdk_lib3.CfnOutput(scope, "apiGwUrlServerUrl", { value: `${apiGateway.apiEndpoint}${serverBasePath}` }); new import_aws_cdk_lib3.CfnOutput(scope, "apiGwUrlImageUrl", { value: `${apiGateway.apiEndpoint}${imageBasePath}` }); return apiGateway; }; // lib/cdk/utils/cfnCertificate.ts var import_aws_cdk_lib4 = require("aws-cdk-lib"); var import_aws_certificatemanager = require("aws-cdk-lib/aws-certificatemanager"); var setupCfnCertificate = (scope, { domains }) => { const [firstDomain, ...otherDomains] = domains; const multiZoneMap = otherDomains.reduce((acc, curr) => ({ ...acc, [curr.domain]: curr.zone }), {}); const certificate = new import_aws_certificatemanager.DnsValidatedCertificate(scope, "Certificate", { domainName: firstDomain.domain, hostedZone: firstDomain.zone, subjectAlternativeNames: otherDomains.map((a) => a.domain), validation: import_aws_certificatemanager.CertificateValidation.fromDnsMultiZone(multiZoneMap), region: "us-east-1" }); new import_aws_cdk_lib4.CfnOutput(scope, "certificateArn", { value: certificate.certificateArn }); return certificate; }; // lib/cdk/utils/cfnDistro.ts var import_aws_cdk_lib5 = require("aws-cdk-lib"); var import_aws_cloudfront = require("aws-cdk-lib/aws-cloudfront"); var import_aws_cloudfront_origins = require("aws-cdk-lib/aws-cloudfront-origins"); var setupCfnDistro = (scope, { apiGateway, imageBasePath, serverBasePath, assetsBucket, domains, certificate, customApiOrigin }) => { const apiGwDomainName = `${apiGateway.apiId}.execute-api.${scope.region}.amazonaws.com`; const serverOrigin = new import_aws_cloudfront_origins.HttpOrigin(apiGwDomainName, { originPath: serverBasePath }); const imageOrigin = new import_aws_cloudfront_origins.HttpOrigin(apiGwDomainName, { originPath: imageBasePath }); const assetsOrigin = new import_aws_cloudfront_origins.S3Origin(assetsBucket); const defaultOptions = { viewerProtocolPolicy: import_aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, allowedMethods: import_aws_cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS }; const defaultCacheOptions = { headerBehavior: import_aws_cloudfront.CacheHeaderBehavior.allowList("accept", "accept-language", "content-language", "content-type", "user-agent", "authorization"), queryStringBehavior: import_aws_cloudfront.CacheQueryStringBehavior.all(), cookieBehavior: import_aws_cloudfront.CacheCookieBehavior.all() }; const imagesCachePolicy = new import_aws_cloudfront.CachePolicy(scope, "NextImageCachePolicy", { queryStringBehavior: import_aws_cloudfront.CacheQueryStringBehavior.all(), enableAcceptEncodingGzip: true, defaultTtl: import_aws_cdk_lib5.Duration.days(30) }); const serverCachePolicy = new import_aws_cloudfront.CachePolicy(scope, "NextServerCachePolicy", { ...defaultCacheOptions }); const assetsCachePolicy = new import_aws_cloudfront.CachePolicy(scope, "NextPublicCachePolicy", { queryStringBehavior: import_aws_cloudfront.CacheQueryStringBehavior.all(), enableAcceptEncodingGzip: true, defaultTtl: import_aws_cdk_lib5.Duration.hours(12) }); const cfnDistro = new import_aws_cloudfront.Distribution(scope, "CfnDistro", { defaultRootObject: "", comment: `CloudFront distribution for ${scope.stackName}`, enableIpv6: true, priceClass: import_aws_cloudfront.PriceClass.PRICE_CLASS_100, domainNames: domains.length > 0 ? domains.map((a) => a.domain) : void 0, certificate, defaultBehavior: { origin: serverOrigin, allowedMethods: import_aws_cloudfront.AllowedMethods.ALLOW_ALL, cachePolicy: serverCachePolicy, viewerProtocolPolicy: import_aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS }, additionalBehaviors: { "/api*": { ...defaultOptions, origin: customApiOrigin != null ? customApiOrigin : serverOrigin, allowedMethods: import_aws_cloudfront.AllowedMethods.ALLOW_ALL, cachePolicy: import_aws_cloudfront.CachePolicy.CACHING_DISABLED }, "_next/data/*": { ...defaultOptions, origin: serverOrigin }, "_next/image*": { ...defaultOptions, origin: imageOrigin, cachePolicy: imagesCachePolicy, compress: true }, "_next/*": { ...defaultOptions, origin: assetsOrigin }, "assets/*": { ...defaultOptions, origin: assetsOrigin, cachePolicy: assetsCachePolicy } } }); new import_aws_cdk_lib5.CfnOutput(scope, "cfnDistroUrl", { value: cfnDistro.distributionDomainName }); new import_aws_cdk_lib5.CfnOutput(scope, "cfnDistroId", { value: cfnDistro.distributionId }); return cfnDistro; }; // lib/cdk/utils/dnsRecords.ts var import_aws_cdk_lib6 = require("aws-cdk-lib"); var import_aws_route53 = require("aws-cdk-lib/aws-route53"); var import_aws_route53_targets = require("aws-cdk-lib/aws-route53-targets"); var import_child_process = require("child_process"); var import_fs = require("fs"); var import_os = require("os"); var import_path = __toESM(require("path")); var getAvailableHostedZones = (profile) => { const tmpDir = import_path.default.join((0, import_os.tmpdir)(), "hosted-zones.json"); const profileFlag = profile ? `--profile ${profile}` : ""; (0, import_child_process.execSync)(`aws route53 list-hosted-zones --output json ${profileFlag} > ${tmpDir}`); const output = JSON.parse((0, import_fs.readFileSync)(tmpDir, "utf8")); return output.HostedZones.map((zone) => zone.Name); }; var matchDomainToHostedZone = (domainToMatch, zones) => { const matchedZone = zones.reduce((acc, curr) => { var _a2; const matchRegex = new RegExp(`(.*)${curr}$`); const isMatching = !!`${domainToMatch}.`.match(matchRegex); const isMoreSpecific = curr.split(".").length > ((_a2 = acc == null ? void 0 : acc.split(".").length) != null ? _a2 : 0); if (isMatching && isMoreSpecific) { return curr; } else { return acc; } }, null); if (!matchedZone) { throw new Error(`No hosted zone found for domain: ${domainToMatch}`); } return matchedZone.endsWith(".") ? matchedZone.slice(0, -1) : matchedZone; }; var prepareDomains = (scope, { domains, profile }) => { const zones = getAvailableHostedZones(profile); return domains.map((domain, index) => { const hostedZone = matchDomainToHostedZone(domain, zones); const subdomain = domain.replace(hostedZone, ""); const recordName = subdomain.endsWith(".") ? subdomain.slice(0, -1) : subdomain; const zone = import_aws_route53.HostedZone.fromLookup(scope, `Zone_${index}`, { domainName: hostedZone }); return { zone, recordName, domain, subdomain, hostedZone }; }); }; var setupDnsRecords = (scope, { domains, cfnDistro }) => { const target = import_aws_route53.RecordTarget.fromAlias(new import_aws_route53_targets.CloudFrontTarget(cfnDistro)); domains.forEach(({ recordName, zone }, index) => { const dnsARecord = new import_aws_route53.ARecord(scope, `AAliasRecord_${index}`, { recordName, target, zone }); const dnsAaaaRecord = new import_aws_route53.AaaaRecord(scope, `AaaaAliasRecord_${index}`, { recordName, target, zone }); new import_aws_cdk_lib6.CfnOutput(scope, `dns_A_Record_${index}`, { value: dnsARecord.domainName }); new import_aws_cdk_lib6.CfnOutput(scope, `dns_AAAA_Record_${index}`, { value: dnsAaaaRecord.domainName }); }); }; // lib/cdk/utils/redirect.ts var import_aws_cdk_lib7 = require("aws-cdk-lib"); var import_aws_route53_patterns = require("aws-cdk-lib/aws-route53-patterns"); var setupApexRedirect = (scope, { domain }) => { new import_aws_route53_patterns.HttpsRedirect(scope, `ApexRedirect`, { // Currently supports only apex (root) domain. zone: domain.zone, targetDomain: domain.domain }); new import_aws_cdk_lib7.CfnOutput(scope, "RedirectFrom", { value: domain.zone.zoneName }); }; // lib/cdk/utils/s3.ts var import_aws_cdk_lib8 = require("aws-cdk-lib"); var import_aws_s3 = require("aws-cdk-lib/aws-s3"); var import_aws_s3_deployment = require("aws-cdk-lib/aws-s3-deployment"); var setupAssetsBucket = (scope) => { const assetsBucket = new import_aws_s3.Bucket(scope, "NextAssetsBucket", { // Those settings are necessary for bucket to be removed on stack removal. removalPolicy: import_aws_cdk_lib8.RemovalPolicy.DESTROY, autoDeleteObjects: true, publicReadAccess: false }); new import_aws_cdk_lib8.CfnOutput(scope, "assetsBucketUrl", { value: assetsBucket.bucketDomainName }); new import_aws_cdk_lib8.CfnOutput(scope, "assetsBucketName", { value: assetsBucket.bucketName }); return assetsBucket; }; var uploadStaticAssets = (scope, { assetsBucket, assetsPath, cfnDistribution }) => { new import_aws_s3_deployment.BucketDeployment(scope, "PublicFilesDeployment", { destinationBucket: assetsBucket, sources: [import_aws_s3_deployment.Source.asset(assetsPath)], // Invalidate all paths after deployment. distribution: cfnDistribution, distributionPaths: ["/*"] }); }; // lib/cdk/stack.ts var NextStandaloneStack = class extends import_aws_cdk_lib9.Stack { constructor(scope, id, config) { super(scope, id, config); this.domains = []; console.log("CDK's config:", config); if (!!config.customApiDomain && config.domainNames.length > 1) { throw new Error("Cannot use Apex redirect with multiple domains"); } this.assetsBucket = this.setupAssetsBucket(); this.imageLambda = this.setupImageLambda({ codePath: config.imageHandlerZipPath, handler: config.customImageHandler, assetsBucket: this.assetsBucket, lambdaHash: config.imageLambdaHash, layerPath: config.imageLayerZipPath, timeout: config.imageLambdaTimeout, memory: config.imageLambdaMemory }); this.serverLambda = this.setupServerLambda({ basePath: config.apigwServerPath, codePath: config.codeZipPath, handler: config.customServerHandler, dependenciesPath: config.dependenciesZipPath, timeout: config.lambdaTimeout, memory: config.lambdaMemory, runtime: config.lambdaRuntime }); this.apiGateway = this.setupApiGateway({ imageLambda: this.imageLambda, serverLambda: this.serverLambda, imageBasePath: config.apigwImagePath, serverBasePath: config.apigwServerPath }); if (config.domainNames.length > 0) { this.domains = this.prepareDomains({ domains: config.domainNames, profile: config.awsProfile }); console.log("Domains's config:", this.domains); } if (this.domains.length > 0) { this.cfnCertificate = this.setupCfnCertificate({ domains: this.domains }); } this.cfnDistro = this.setupCfnDistro({ assetsBucket: this.assetsBucket, apiGateway: this.apiGateway, imageBasePath: config.apigwImagePath, serverBasePath: config.apigwServerPath, domains: this.domains, certificate: this.cfnCertificate, customApiOrigin: config.customApiDomain ? new import_aws_cloudfront_origins2.HttpOrigin(config.customApiDomain) : void 0 }); this.uploadStaticAssets({ assetsBucket: this.assetsBucket, assetsPath: config.assetsZipPath, cfnDistribution: this.cfnDistro }); if (this.domains.length > 0) { this.setupDnsRecords({ cfnDistro: this.cfnDistro, domains: this.domains }); if (config.redirectFromApex) { this.setupApexRedirect({ domain: this.domains[0] }); } } } prepareDomains(props) { return prepareDomains(this, props); } setupAssetsBucket() { return setupAssetsBucket(this); } setupApiGateway(props) { return setupApiGateway(this, props); } setupServerLambda(props) { return setupServerLambda(this, props); } setupImageLambda(props) { return setupImageLambda(this, props); } setupCfnDistro(props) { return setupCfnDistro(this, props); } // Creates a certificate for Cloudfront to use in case parameters are passed. setupCfnCertificate(props) { return setupCfnCertificate(this, props); } setupDnsRecords(props) { return setupDnsRecords(this, props); } // Creates a redirect from apex/root domain to subdomain (typically wwww). setupApexRedirect(props) { return setupApexRedirect(this, props); } // Upload static assets, public folder, etc. uploadStaticAssets(props) { return uploadStaticAssets(this, props); } }; // lib/cdk/app.ts var app = new import_aws_cdk_lib10.App(); var commandCwd = process.cwd(); var _a; new NextStandaloneStack(app, envConfig.stackName, { // NextJS lambda specific config. assetsZipPath: import_path2.default.resolve(commandCwd, "./next.out/assetsLayer.zip"), codeZipPath: import_path2.default.resolve(commandCwd, "./next.out/code.zip"), dependenciesZipPath: import_path2.default.resolve(commandCwd, "./next.out/dependenciesLayer.zip"), customServerHandler: "index.handler", // Image lambda specific config. imageHandlerZipPath: import_imaginex_lambda.optimizerCodePath, imageLayerZipPath: import_imaginex_lambda.optimizerLayerPath, imageLambdaHash: `${import_imaginex_lambda.name}_${import_imaginex_lambda.version}`, customImageHandler: import_imaginex_lambda.handler, // Lambda & AWS config. apigwServerPath: "/_server", apigwImagePath: "/_image", ...envConfig, env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: (_a = process.env.AWS_REGION) != null ? _a : process.env.CDK_DEFAULT_REGION } }); app.synth();