cdk-nextjs
Version:
Deploy Next.js apps on AWS with CDK
247 lines (238 loc) • 8.15 kB
JavaScript
;
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
});