// NPM Install Command: "npm install -D @aws-sdk/client-s3 @aws-sdk/client-cloudfront dotenv mime-types"// NOTE: This command does NOT work correctly when bucket versioning is enabled.// Use in GitHub Actions: https://github.com/Mohitjain49/mohit-website/blob/main/.github/workflows/main.yml#L30// Run with "node scripts/deploy.mjs". You can make a reference to this in your package.json file.import 'dotenv/config'; // Used To import .env variables independent from Vite.import * as fs from "fs"; // The File System Module. Used to find and get data from files in the build output.import * as path from 'path'; // The Path Module. Used to create path names and file names.import { lookup } from 'mime-types'; // The lookup function here can generate the MIME type for every file we fetch.import { S3Client, PutObjectCommand, DeleteObjectsCommand, paginateListObjectsV2 } from "@aws-sdk/client-s3"; // This module allows us to remove and send files to an Amazon S3 bucket.import { CloudFrontClient, CreateInvalidationCommand } from '@aws-sdk/client-cloudfront'; // This module allows us invalidate a CloudFront Cache.const BUILD_OUTPUT = (process.env.BUILD_OUTPUT ?? 'dist'); // This is the name of your folder where your application was built for deployment. Defaults to 'dist'.const AWS_REGION = process.env.AWS_REGION; // This is the region where both the S3 bucket and CloudFront Distribution should be located (e.g. "us-east-1").const AWS_BUCKET = process.env.AWS_BUCKET; // This is the name of the AWS bucket to send messages to. (e.g. "mohit-website").const AWS_CLOUDFRONT_DIST_ID = process.env.AWS_CLOUDFRONT_DIST_ID; // This is the ID of your CloudFront Distribution (e.g. "EDFDVBD632BHDS5").const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID; // This serves as the access key for your IAM account (e.g. AKIAIOSFODNN7EXAMPLE).const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY; // This serves as the access key for your IAM account (e.g. wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY).// console.log(BUILD_OUTPUT)// console.log(AWS_REGION);// console.log(AWS_BUCKET);// console.log(AWS_CLOUDFRONT_DIST_ID);// console.log(AWS_ACCESS_KEY_ID);// console.log(AWS_SECRET_ACCESS_KEY);const outDir = path.resolve(BUILD_OUTPUT); // The path that leads to the build output.const args = process.argv.slice(2); // These are the arguments for altering how the script works.const NO_DELETION = (args.findIndex(item => item === "--no-delete") != -1);const NO_INVALIDATION = (args.findIndex(item => item === "--no-invalidation") != -1);// If any of the 4 required environment variables are not defined, this will throw an error before the script starts.if(!AWS_REGION || !AWS_BUCKET || !AWS_ACCESS_KEY_ID || !AWS_SECRET_ACCESS_KEY) { throw new Error("The mandatory environment variables are not properly configured.");}if(!AWS_CLOUDFRONT_DIST_ID && !NO_INVALIDATION) { throw new Error("No CloudFront Distribution ID Provided.");}/** * ---------------------------------------------------------- * This section specifically handles commands with Amazon S3. * ---------------------------------------------------------- *//** The Client for Amazon S3. */const s3Client = new S3Client({ region: AWS_REGION, credentials: { accessKeyId: AWS_ACCESS_KEY_ID, secretAccessKey: AWS_SECRET_ACCESS_KEY }});/** * This function sends a command via the S3 client. * @param {PutObjectCommand | DeleteObjectsCommand} command the command for the S3 client to send. */async function sendS3Command(command) { return await s3Client.send(command);}/** * This function returns all the put object commands that will be sent by the S3 client. */function getPutObjectCommands() { /** @type {Array<String>} All the filenames from the build output. */ const allFiles = fs.readdirSync(outDir, { recursive: true }); const assetFolders = ["_nuxt/", "_fonts/"]; const commands = []; for(let i = 0; i < allFiles.length; i++) { const filename = allFiles[i].replaceAll("\\", "/"); const mimeType = lookup(filename); const filePath = path.join(outDir, filename); if(!fs.statSync(filePath).isFile()) { continue; } if(!mimeType || mimeType === "application/x-install-instructions") { continue; } commands.push(new PutObjectCommand({ Body: fs.createReadStream(filePath), Bucket: AWS_BUCKET, Key: filename, ContentType: ((mimeType === "text/javascript") ? "application/javascript" : mimeType), CacheControl: ((filename.startsWith(assetFolders[0]) || filename.startsWith(assetFolders[1])) ? "public, max-age=2592000, no-cache" : "no-cache, no-store, must-revalidate" ) })); } // Returns all the "Put Object" commands for use. return commands;}/** * This function returns commands to delete ALL the objects currently in an array. */async function getDeleteObjectCommands() { // This gets all the current objects in the bucket. const paginator = paginateListObjectsV2( { client: s3Client }, { Bucket: AWS_BUCKET } ); // This gets all the keys. const objectKeys = []; for await (const { Contents } of paginator) { objectKeys.push(...Contents.map((obj) => ({ Key: obj.Key }))); } // This creates and returns the commands. const deleteCommands = []; while(objectKeys.length != 0) { deleteCommands.push(new DeleteObjectsCommand({ Bucket: AWS_BUCKET, Delete: { Objects: objectKeys.splice(0, Math.min(objectKeys.length, 1000)) } })); } return deleteCommands;};/** * --------------------------------------------------------------------------------------------- * This section specifically invalidates the Amazon CloudFront Cache that delivers the S3 files. * --------------------------------------------------------------------------------------------- *//** The Client for Amazon CloudFront. */const cloudfrontClient = new CloudFrontClient({ region: AWS_REGION, credentials: { accessKeyId: AWS_ACCESS_KEY_ID, secretAccessKey: AWS_SECRET_ACCESS_KEY }});/** * This function sends an Invalidation command to the website's cloudfront distribution. */async function sendCloudfrontInvalidation() { return cloudfrontClient.send(new CreateInvalidationCommand({ DistributionId: AWS_CLOUDFRONT_DIST_ID, InvalidationBatch: { CallerReference: Date.now(), Paths: { Quantity: 1, Items: ["/*"] } } }));}/** * ----------------------------------------------------------------------------- * This part below fully starts the script and runs all the necessary functions. * ----------------------------------------------------------------------------- *//** This is the main function to run at the end once all objects are initialized. */async function main() { try { // Unless an argument is passed in, this deletes all current files in the bucket. if(!NO_DELETION) { const deleteCommands = await getDeleteObjectCommands(); for(let j = 0; j < deleteCommands.length; j++) { await sendS3Command(deleteCommands[j]); } console.log(`✅ Deleted All Files From Bucket!\n`); } const commands = getPutObjectCommands(); const length = commands.length; // This sends all the files in the build output to the bucket, replacing any files with the same key. for(let i = 0; i < length; i++) { await sendS3Command(commands[i]); console.log(`✅ Uploaded file ${i + 1} of ${length}: ${commands[i].input.Key}`); } console.log(`✅ Uploaded All Files To Your Bucket!\n`); // Unless an argument is passed in, this invalidates a cloudfront cache. if(!NO_INVALIDATION) { await sendCloudfrontInvalidation(); console.log("✅ Completed CloudFront Cache Invalidation!"); } // This marks the deployment as complete. console.log("🏁 Script Complete!"); process.exit(0); } catch(err) { console.error(err); process.exit(1); }}// Runs The Main Function.await main();