The ultralight angular application setup package – Part 1: Four files to rule them all

In this tutorial I will be walking you through a means of rapid angular application setup. By the end you should be able to run a single command and have a bare bones angular application built and ready for experimenting. By eliminating the overhead in setting up a project, I hope to encourage rapid development of bad ideas.

We will be using Node.js, gulp.js, and bower.js to setup the initial structure for a project. The final product of this tutorial will a foundation for a project but will not contain the build processes needed compile project script. Because of this, you will not be able to official “start” the project until the end of Part 2.s

Step one: The package.json file

As a quick disclaimer, this tutorial will be using Node.js extensively. If you do not have any experience with Node, I would recommend downloading and installing it from NodeJS.org and familiarizing yourself with its functions before attempting this.

Navigate to the project folder you would like to use for your Angular application and create a file called package.json. Enter the following code into this file and save.

{
  "name": "PackageName",
  "private": true,
  "version": "0.0.0",
  "description": "",
  "repository": "",
  "license": "",
  "devDependencies": {
    "fs-path": "^0.0.22",
    "gulp": "^3.9.1",
    "gulp-load-plugins": "^1.2.4",
     "http-server": "^0.9.0",
    "mkdirp": "^0.5.1"
  },
  "scripts": {
    "preinstall": "npm install -g bower && npm install -g gulp",
    "postinstall": "bower install && gulp setup",
    "start": "http-server -a localhost -p 8000 -c-1",
    "pretest": "npm install"
  },
  "dependencies": {}
}

While the package.json file offers a large number of useful tools, the area we will be focusing on in this case is the devDependencies and scripts properties. Note: the name field is required by Node and will throw errors if empty.

  • devDependencies: This is what node will use to define what node development packages to install when you run the initial “npm install” command on the project folder. Keep in mind, packages placed here should only be regarding the development process, not libraries used by the application (Those would go in the dependencies object, but in this instance we’ll be managing those with Bower)
  • scripts: These are command line scripts that will be executed during various node processes. As you can see, before the installation we are running a command to install Bower and Gulp globally. After those development tools are installed, we’re running two tasks (both of which we will define later), one to install the website dependencies defined in the bower.json file and one to run the gulp process to build the site structure.

Step 2: The bower.json file

Next, we will create a file called bower.json in the folder and define it as follows.

{
  "name": "Setup package",
  "description": "",
  "main": "",
  "authors": ["" ],
  "license": "",
  "keywords": [""],
  "homepage": "",
  "private": true,
  "ignore": [""], 
  "dependencies": {
    "angular": "^1.5.7",
    "bootstrap": "^3.3.6",
    "angular-route": "^1.5.7"
  }
}

Once again, we will be skipping over some parts of the object that are not relevant to what we’re doing. It is worth reading about how bower manages packages, but for the time being we will focus on the dependencies object.

  • dependencies: These are actual packages used by your website that are managed by bower. These can be automatically updated with bower commands entered manually or from a gulp process. For now, we’ll be using this file to simply install the packages in our project folder.

Step 3: The custom file

Next comes our definition of how the Angular application project should be organized. In the same folder, create a file called projectSetup.json with the following code. This object will be consumed by our custom processes defined in step 4. With this, we will define the folder layout of the project, the .gitignore file, and any template files we would like to create.

{
    "fileTree": {
        "working": {
            "app": {
                "components": {
                    "main": {
                        "files":[
                        "main.html",
                        "main.js" ]
                    } 
                },
                "files":["app.module.js","app.route.js","app.controller.js"],
                "shared": null
            },
            "assets": {
                "css": null,
                "img": null,
                "js": null,
                "less": null
            },
            "files": ["index.html"]
        },
        "public": {
            "css": null,
            "img": null,
            "js": null,
            "html": {
                "files": ["main.html"]
            },
            "files": ["index.html"]
        }
    },
    "gitignore": [ "node_modules", "bower_components" ],
    "templates": {
        "index.html": [
            "<!DOCTYPE html>",
            "<html lang=\"en\" ng-app=\"app\">",
            "<head>",
            "<meta charset=\"utf-8\">",
            "<title></title>   ",
            "<link rel=\"stylesheet\" href=\"css/index.css\" />",
            "<!--Scripts-->",
            "<script src=\"js/vendor-min.js\"></script>",
            "<script src=\"js/app.js\"></script>",
            "</head>",
            "<body ng-controller=\"mainController\">",
            "<div ng-view></div> ",
            "</body> ",
            "</html>"
        ],
        "app.module.js": [ "var app = angular.module(\"app\", [\"ngRoute\"]);" ],
        "app.controller.js": [
            "app.controller(\"mainController\", function ($scope) {",
            "$scope.msg = \"Hello World\";",
            "});"
        ],
        "app.route.js": [
            "app.config(function($routeProvider) {",
            "$routeProvider.when(\"/\", {",
            "templateUrl : \"html/main.html\"",
            "});",
            "});"
        ],
        "main.html": ["<div>{{msg}}</div>"]
    }
}
  • fileTree: This is how we will lay out the folder structure for the project, as well as defining any placeholder files we may want. Folder names will be decided based on any property that contains an object or a null value, while file names will be defined by any property with an array of file names. While we chose to use “files” for all of our file arrays, this isn’t strictly necessary.
  • gitignore: This is a simple array of items you would like to be added to a .gitignore file that will be generated.
  • templates: The properties of this object should exactly match the names of placeholder files that you’ve defined in your file tree. The array of strings will be formatted and injected into the file that matches the property name, allowing you to define some basic default functionality in the javascript/html files. Note: this was written with the intention of keeping everything within a single file. While I will not be covering it in this tutorial, it would be fairly easy to convert the functions in step 4 to accept file paths that point to template files, rather than arrays of strings. You do you.

Step 4: The gulpfile.js file

This file will be the backbone of our setup process for our Angular application. For now, we will only use it for generating the folder/files structure, generating the .gitignore file, and injecting default code into our auto generated files. However, in the next tutorial we will also be defining build processes and ftp capabilities from this file.

//Includes
var gulp = require('gulp');
var fs = require('fs');
var fsPath = require('fs-path');
var mkdirp = require('mkdirp');


//Gulp plugin manager
 var plugins = require("gulp-load-plugins")({
     pattern: ['gulp-*', 'gulp.*', 'main-bower-files'],
     replaceString: /\bgulp[\-.]/
 });

//Define setup task
gulp.task('setup', function () {
    //get project layout object
    var json = JSON.parse(fs.readFileSync('projectSetup.json'));
    var paths = getPaths(json.fileTree || null);
    var templates = json.templates || null;
    var ignores = json.gitignore || null;

    //build file structure
    if (paths) {
        buildFileStructure(paths);
    }

    //build .gitignore
    if (ignores) {
        buildGitIgnore(ignores);
    }
   
    //populate base files
    if (templates) {
        buildBaseFiles(templates, paths);
    }
});


//Supporting Methods

/**
 * Loops through all template objects, finds the associated file path, and over writes the target file with the template
 * @param {object} templates 
 * @param {list} paths
 */
function buildBaseFiles(templates, paths) {
    for (var template in templates) {
        // skip loop if the property is from prototype
        if (!templates.hasOwnProperty(template)) continue;
      
        paths.forEach(function (path) {
            if (path.indexOf(template) > -1) {
                writeFileFromTemplate(path, templates[template]);
            }
        });
    }
}

/**
 * Creates files or folders according the paths passed in
 * @param {array} paths 
 */
function buildFileStructure(paths) {
    //Sort paths based on if they are files or folders
    paths.sort(function (a, b) {
        return (isFile(b) === isFile(a)) ? 0 : isFile(b) ? -1 : 1;
    });

    for (var path in paths) {
        var curPath = paths[path];

        //If the current path leads to a file, write the file path
        if (isFile(curPath)) {
            fsPath.writeFile(__dirname + '/' + curPath, "", function (err) {
                if (err) {
                    return console.log(err);
                }

                console.log("The file was saved!");
            })
        //if not, just make a folder
        } else {
            mkdirp(curPath, function (err) {
                if (err) { console.log(err) }
            });
        }
    }
}

/**
 * Finds the file defined in the path and overwrites it with strings in the content array
 * @param {string} path 
 * @param {array} contentArray 
 */
function writeFileFromTemplate(path, contentArray) {
    var content = "";
    for (var line in contentArray) {
        content += contentArray[line] + "\r\n";
    }
    fsPath.writeFile(__dirname + '/' + path, content, function (err) {
        if (err) {
            return console.log(err);
        }

        console.log("The file was saved!");
    });
}

/**
 * Builds the .gitignore file on the same file system level as this file
 * @param {array} ignores 
 */
function buildGitIgnore(ignores) {
    var content = "";
    ignores.forEach(function (ignore) {
        content += ignore + "\r\n";
    });
    fsPath.writeFile(__dirname + '/' + ".gitignore", content, function(err) {
        if (err) {
            return console.log(err);
        }

        console.log("The file was saved!");
    });
}

/**
 * Returns list of paths to be built in th file system
 * @param {Object} json 
 * @return {Array} pathArray
 */
function getPaths(json) {

    if (json == null) return null;
    var pathArray = [];
    for (var key in json) {
        // skip loop if the property is from prototype
        if (!json.hasOwnProperty(key)) continue;

        var obj = json[key];
        if (Array.isArray(obj)) {
            for (var x in obj) {
                pathArray.push(obj[x]);
            }

        } else if (obj == null) {
            console.log(key);
            pathArray.push(key);

        } else if (typeof obj == "object") {
            //console.log(key);
            var tempArray = getPaths(obj);
            for (var x = 0; x < tempArray.length; x++) {
                pathArray.push(key + "\\" + tempArray[x]);
            }
        }
    }
    return pathArray;
}

/**
 * Returns true if string contians a dot extension
 * @param {string} path 
 * @return {Bool} 
 */
function isFile(path) {
    return /\.[0-9a-z]+$/i.test(path);
}
  • Includes: Here you can see we are referencing the development packages we defined in step one. Require.js will automatically find them in the node_modules folder.
  • Gulp plugin manager: This is a handy tool that will be used in part 2 of this tutorial. In this step, we search for any node package that is prefixed with “gulp-”, strip the prefix off, and then load it into the plugins object. This keeps all our gulp plugins in one object.
  • Gulp setup: This is the main task that sets up the project structure. As you can see, we first read in the json object using the fs package and then send the various parts to our helper functions. In the first file when we defined gulp setup inside of the postInstall property, we were telling Node to run the setup command through Gulp.
  • Supporting Methods: We will not go into details about these as the names and purposes of them should be fairly intuitive.

Step 5: where do we stand now?

At this point, the final step is to watch it work. With your Angular application project folder select in your command window, enter the command npm install. At this point, the process will execute the following:

  1. Download the dev dependencies (defined in packages.json),
  2. Make sure gulp and bower are installed on your machine,
  3. Install bower packages (defined in bower.json),
  4. Run the gulp setup process (defined in gulpfile.js)
  5. Import the custom object
  6. Create the file structure
  7. Create the .gitignore file
  8. Overwrite blank files with templates

Once the process ends your project folder contents should look like this:

Angular_setup_file_structure

This is good start, but we’re not quite done getting everything ready to go. In part 2 we’ll cover compiling the bower package files into a usable script file for use by the index.html file, as well as how to automate that process during the installation.