Uploading a Angular App to S3 with npm

Lately I have been working in a few Angular projects. I would like to share some the discoveries I have made, but honestly I'm lacking the inspiration to write. I would rather just share my code and move on, but some information in not that easy to convey through code. Depending your interest, I'll might do both: force myself in to writing more often; and have a small shared Angular project with small, gradual, self-explanatory commits. For the upcoming blog posts expect more Angular. When I mean Angular It's Angular 2 and above, not to be confused with the first version (now named AngularJS). If you are not familiarized with the differences please check this brilliant Quora answer.

As previously talked in previous a blog post, I described how to host an Angular app in AWS S3. Then how use it in combination with AWS CloudFront to tackle some of the features S3 lacks. Now that we know how to serve our SPA, what about deployment? There are multiple ways to 'skin this cat', especially if there's a team involved, but for this example let's make it as easy as possible. Our goal is to run the following command npm run deploy to deploy a compiled Angular app to an S3 bucket.

Let me work your through the necessary tools: starting the most obvious node.js, make sure both node and npm are installed by opening your preferred command prompt and calling node -v and npm -v. For this example, I'm going to use Angular-CLI, keep in mind this optional, I'm just going to use it to create a new Angular project. To install Angular-CLI run npm install -g @angular/cli then check if it was properly installed by running ng -v.

check Angular-CLI version

The next step is to create a new Angular project ng new MyAngularApp. I'm not going to change/all any to the actual project, the objective is to compile it as it is and deploy it to S3.

create new project using Angular-CLI

Next, we need to install AWS SDK npm install aws-sdk --save-dev. This allows us to spawn a client that will interact with S3. Additionally, for every file uploaded to S3, the proper MIME type needs to be set. If not specified S3 will default to 'application/octet-stream' resulting in files being downloaded instead of interpreted and rendered by a browser. To avoid manually mapping files to their respective MIME type let's install a helper library npm install mime-types --save-dev

install AWS-SDK using npm

You will also need to configure AWS SDK to get access to your AWS account. For me details please check the 'configure' section in AWS SDK for Node.js instructions

Now let's create our deployment script. It will be a simple JavaScript file that will be interpreted and run by node, invoked by npm run-script. For now, just create a new folder named 'scripts' at the root level of your project. Inside this folder create a file name 'deploy.js'. The path from your project's root level should look like this './scripts/deploy.js'. Grab the following code and paste in your newly created file. The code itself it's simple, the inline comments should help understanding it.

const AWS = require("aws-sdk"); // imports AWS SDK
const mime = require('mime-types') // mime type resolver
const fs = require("fs"); // utility from node.js to interact with the file system
const path = require("path"); // utility from node.js to manage file/folder paths

// configuration necessary for this script to run
const config = {
    s3BucketName: 'your.s3.bucket.name',
    folderPath: '../dist' // path relative script's location
  };

// initialise S3 client
const s3 = new AWS.S3({
    signatureVersion: 'v4'
});

// resolve full folder path
const distFolderPath = path.join(__dirname, config.folderPath);

// Normalize \\ paths to / paths.
function unixifyPath(filepath) {
    return process.platform === 'win32' ? filepath.replace(/\\/g, '/') : filepath;
};

// Recurse into a directory, executing callback for each file.
function walk(rootdir, callback, subdir) {
    // is sub-directory
    const isSubdir = subdir ? true : false;
    // absolute path
    const abspath = subdir ? path.join(rootdir, subdir) : rootdir;

    // read all files in the current directory
    fs.readdirSync(abspath).forEach((filename) => {
        // full file path
        const filepath = path.join(abspath, filename);
        // check if current path is a directory
        if (fs.statSync(filepath).isDirectory()) {
            walk(rootdir, callback, unixifyPath(path.join(subdir || '', filename || '')))
        } else {
            fs.readFile(filepath, (error, fileContent) => {
                // if unable to read file contents, throw exception
                if (error) {
                    throw error;
                }

                // map the current file with the respective MIME type
                const mimeType = mime.lookup(filepath)
                
                // build S3 PUT object request
                const s3Obj = {
                    // set appropriate S3 Bucket path
                    Bucket: isSubdir ? `${config.s3BucketName}/${subdir}` : config.s3BucketName,
                    Key: filename,
                    Body: fileContent,
                    ContentType: mimeType
                }

                // upload file to S3
                s3.putObject(s3Obj, (res) => {
                    console.log(`Successfully uploaded '${filepath}' with MIME type '${mimeType}'`)
                })
            })
        }
    })
}

// start upload process
walk(distFolderPath, (filepath, rootdir, subdir, filename) => {
    console.log('Filepath', filepath);
});

To handle trigger the deployment we will need a task runner. Feel free to pick whatever you prefer, personally I tend to use npm run-script. Define a task name and the command in the project's 'package.json' and use npm run <task name> to execute it. Let's do it for our example project. Open your project's 'package.json' . In your 'scripts' section add the following "deploy": "node ./scripts/deploy.js" instructing node.js to execute './scripts/deploy.js', which by itself with handle the deployment of the compiled Angular application.

Additionally, you can tell npm to compile the project before deploying it. If you create a new entry with same task name prepended with 'pre', npm will run this task first before executing the task you requested. You can create a multiple step chain using this technique. Add a new entry with the following "predeploy": "ng build -prod -aot" instructing Angular-CLI to compile in production mode with ahead-of-time compilation enabled. Check out a excerpt of 'package.json' containing only our modifications.

{
  "name": "my-angular-app",
  "scripts": {
    (...) // existing scripts
    
    "predeploy": "ng build -prod -aot", // will run before 'deploy' (notice 'pre')
    "deploy": "node ./scripts/deploy.js" // tell node.js to execute './scripts/deploy.js'
    
  },
  "dependencies": {
	(...) // existing dependencies    
  },
  "devDependencies": {
	(...) // existing devDependencies  
	
    "aws-sdk": "^2.48.0", // installed with npm
    "mime-types": "^2.1.15" // installed with npm
  }
}

Now you can try 'the whole shebang' by executing npm run deploy. Boom, your code is now deployed in S3.

(update) My thanks to Diego Arevalo for providing a script update to traverse subfolders.

executing npm run deploy