UNPKG

cdk-nextjs

Version:

Deploy Next.js apps on AWS with CDK

247 lines (238 loc) 8.15 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; 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 __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/lambdas/post-deploy/post-deploy.lambda.ts var post_deploy_lambda_exports = {}; __export(post_deploy_lambda_exports, { handler: () => handler }); module.exports = __toCommonJS(post_deploy_lambda_exports); // src/lambdas/post-deploy/create-invalidation.ts var import_client_cloudfront = require("@aws-sdk/client-cloudfront"); var cfClient = new import_client_cloudfront.CloudFrontClient(); async function createInvalidation(input) { return cfClient.send( new import_client_cloudfront.CreateInvalidationCommand({ DistributionId: input.distributionId, InvalidationBatch: { CallerReference: input.invalidationBatch.callerReference, Paths: { Quantity: input.invalidationBatch.paths.quantity, Items: input.invalidationBatch.paths.items } } }) ); } // src/lambdas/post-deploy/prune-fs.ts var import_node_fs = require("node:fs"); var import_node_path = require("node:path"); // src/lambdas/utils.ts function cfnResponse(props) { const body = JSON.stringify({ Status: props.responseStatus, Reason: "See the details in CloudWatch Log Stream: " + props.context.logStreamName, PhysicalResourceId: props.physicalResourceId || props.context.logStreamName, StackId: props.event.StackId, RequestId: props.event.RequestId, LogicalResourceId: props.event.LogicalResourceId, Data: props.responseData }); return fetch(props.event.ResponseURL, { method: "PUT", body, headers: { "content-type": "", "content-length": body.length.toString() } }); } function debug(value) { if (process.env.DEBUG) console.log(JSON.stringify(value, null, 2)); } // src/lambdas/post-deploy/prune-fs.ts function pruneFs(props) { const { mountPath, currentBuildId } = props; if (!(0, import_node_fs.existsSync)(mountPath)) { debug(`Mount path ${mountPath} does not exist, nothing to prune`); return; } const entries = (0, import_node_fs.readdirSync)(mountPath); const directoriesToDelete = entries.filter((entry) => { const entryPath = (0, import_node_path.join)(mountPath, entry); return (0, import_node_fs.statSync)(entryPath).isDirectory() && entry !== currentBuildId; }); for (const dir of directoriesToDelete) { const dirPath = (0, import_node_path.join)(mountPath, dir); debug(`Pruning directory: ${dirPath}`); (0, import_node_fs.rmSync)(dirPath, { recursive: true, force: true }); } debug( `Pruned ${directoriesToDelete.length} directories, kept ${currentBuildId}` ); } // src/lambdas/post-deploy/prune-s3.ts var import_client_s3 = require("@aws-sdk/client-s3"); var s3Client = new import_client_s3.S3Client(); var MAX_CONCURRENT_OPERATIONS = 50; async function pruneS3(props) { const { bucketName, currentBuildId, msTtl } = props; const cutoffDate = new Date(Date.now() - msTtl); const objectsToDelete = []; let continuationToken = void 0; let listObjectsCount = 0; do { const listObjectsV2Input = { Bucket: bucketName, ContinuationToken: continuationToken }; const listResponse = await s3Client.send( new import_client_s3.ListObjectsV2Command(listObjectsV2Input) ); if (!listResponse.Contents || listResponse.Contents.length === 0) { break; } const oldObjects = listResponse.Contents.filter((obj) => { const lastModified = obj.LastModified || /* @__PURE__ */ new Date(); return obj.Key && lastModified < cutoffDate; }); debug( `Checking old objects metadata to determine pruning: ${oldObjects.map((o) => o.Key)}` ); const checkResults = await processBatch( oldObjects, MAX_CONCURRENT_OPERATIONS, async (object) => { if (!object.Key) return null; try { const headResponse = await s3Client.send( new import_client_s3.HeadObjectCommand({ Bucket: bucketName, Key: object.Key }) ); const objectBuildId = headResponse.Metadata?.["next-build-id"]; if (objectBuildId !== currentBuildId) { return { Key: object.Key }; } } catch (error) { console.error(`Error checking object ${object.Key}:`, error); } return null; } ); objectsToDelete.push( ...checkResults.filter(Boolean) ); if (listResponse.NextContinuationToken) { continuationToken = listResponse.NextContinuationToken; } listObjectsCount++; } while (continuationToken && listObjectsCount <= 100); if (objectsToDelete.length > 0) { const deleteBatches = []; for (let i = 0; i < objectsToDelete.length; i += 1e3) { const batch = objectsToDelete.slice(i, i + 1e3); deleteBatches.push(batch); } await processBatch( deleteBatches, 5, // Process up to 5 delete batches in parallel async (batch) => { try { debug( `Deleting objects: ${batch.map((b) => b.Key)} from ${bucketName}` ); await s3Client.send( new import_client_s3.DeleteObjectsCommand({ Bucket: bucketName, Delete: { Objects: batch } }) ); debug(`Deleted ${batch.length} objects from ${bucketName}`); } catch (error) { console.error("Error deleting objects:", error); } } ); } debug( `Pruning complete. Deleted ${objectsToDelete.length} objects from ${bucketName}` ); } async function processBatch(items, batchSize, processFn) { const results = []; for (let i = 0; i < items.length; i += batchSize) { const batch = items.slice(i, i + batchSize); const batchResults = await Promise.all(batch.map(processFn)); results.push(...batchResults); } return results; } // src/constants.ts var MOUNT_PATH = "/mnt/cdk-nextjs"; var CACHE_PATH = ".next/cache"; var DATA_CACHE_PATH = `${CACHE_PATH}/fetch-cache`; var IMAGE_CACHE_PATH = `${CACHE_PATH}/images`; var SERVER_DIST_PATH = ".next/server"; var FULL_ROUTE_CACHE_PATH = `${SERVER_DIST_PATH}/app`; // src/lambdas/post-deploy/post-deploy.lambda.ts var handler = async (event, context) => { debug({ event }); let responseStatus = "FAILED" /* Failed */; try { const props = event.ResourceProperties; if (event.RequestType === "Create" || event.RequestType === "Update") { const { buildId, createInvalidationCommandInput, msTtl, staticAssetsBucketName } = props; const promises = []; if (createInvalidationCommandInput) { promises.push(createInvalidation(createInvalidationCommandInput)); } pruneFs({ currentBuildId: buildId, mountPath: MOUNT_PATH }); if (staticAssetsBucketName) { promises.push( pruneS3({ bucketName: staticAssetsBucketName, currentBuildId: buildId, msTtl: parseInt(msTtl) }) ); } await Promise.all(promises); responseStatus = "SUCCESS" /* Success */; } else { responseStatus = "SUCCESS" /* Success */; } } catch (err) { console.error(err); } finally { await cfnResponse({ event, context, responseStatus, responseData: {} }); } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { handler });