Skip to content

The ultralight angular application setup package – Part 2: Build process for large applications

In our last post, we discussed how to create four files that, when chained together with the appropriate commands, would build out the framework for your web app. Now we will show you how to tweak this process to create large application friendly project organization, as well as adding build processes into the mix. But first, lets go over some changes we made since the first blog entry.

Changes since last time

New folder structures

Revised file tree
This image depicts the file tree that is built when running this setup package

As you will see later on, we have made a few changes to the folder layout we’re using in the “fileTree” property which are reflected to the right. These are primarily to encourage a more shallow folder structure with clearly defined, self contained areas of interested. The project structure is, of course, up to you. However, in order to be compatible with this build process, it is necessary to follow the naming conventions used in our example.

New Definitions

You will also notice that all of our angular definitions are now wrapped in self executing anonymous functions. This was an organizational change and does not have anything to do with the transition from smaller app to larger app. We simply adopted this standard between blog posts as a way to prevent global variable declarations as well as forcing errors by using “use strict” in each function.

Using less

Since our last blog post, we have also adopted Less as a more efficient means of managing site css. Less syntax is largely the same as css, but allows for importing of files, variable declarations, and some internal less helper functions. Or new structure will assume you are providing a new less file for every “area” you define in your application. This keeps code modular and helps prevent overlap.

projectStructure.json revised

{
    "fileTree": {
        "build": {
            "app": {
                "core": {
                    "files": [
                        "core.module.js"
                    ]
                },
                "home": {
                    "files": [
                        "home.html",
                        "home.module.js",
                        "home.controller.js"
                    ]
                },
                "layout": {
                    "files": [
                        "layout.module.js",
                        "layout.controller.js"
                    ]
                },
                "files": [
                    "app.module.js",
                    "app.route.js"
                ]
            },
            "less": {
                "files": [
                    "app.less",
                    "home.less"
                ]
            },
            "files":[
                "index.html"
            ]
        },
        "public": null
    },
    "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/app.min.css\" />",
            "<!--Scripts-->",
            "<script src=\"js/vendor.min.js\"></script>",
            "<script src=\"js/app.js\"></script>",
            "</head>",
            "<body ng-controller=\"layoutController as vm\">",
            "<!--Your layout stuff goes here-->",
            "<div>{{vm.test}}</div> ",
            "<div ng-view></div> ",
            "</body> ",
            "</html>"
        ],
        "app.module.js": [
            "(function () {",
            "'use strict';",
            "angular.module('app', [",
            "'app.core',",
            "'app.layout',",
            "'app.home'",
            "]);",
            "})();"
        ],
        "app.route.js": [
            "(function () {",
            "    'use strict';",
            "    angular.module('app').config(function ($routeProvider) {",
            "        $routeProvider.when('/', {",
            "            templateUrl: '../html/home/home.html',",
            "            controller: 'homeController as vm'",
            "        });",
            "    });",
            "})();"
        ],
        "core.module.js": [
            "(function () {",
            "'use strict';",
            "angular.module('app.core', [",
            "'ngRoute'",
            "]);",
            "})();"
        ],
        "layout.module.js": [
            "(function () {",
            "   'use strict';",
            "    angular.module('app.layout', []);",
            "})();"
        ],
        "layout.controller.js": [
            "(function () {",
            "    'use strict';",
            "    angular.module('app.layout').controller('layoutController', layoutController);",
            "    layoutController.$inject = ['$scope']",
            "    function layoutController($scope){",
            "        var vm = {",
            "            test: 'Hello world from layout controller!'",
            "        };",
            "        return vm;",
            "    }",
            "})();"
        ],
        "home.module.js": [
            "(function () {",
            "   'use strict';",
            "    angular.module('app.home', []);",
            "})();"
        ],
        "home.controller.js": [
            "(function () {",
            "    'use strict';",
            "    angular.module('app.home').controller('homeController', homeController);",
            "    homeController.$inject = ['$scope']",
            "    function homeController($scope){",
            "        var vm = {",
            "            test: 'Hello world from home controller!'",
            "        };",
            "        return vm;",
            "    }",
            "})();"
        ],
        "home.html": [
            "<div>{{vm.test}}</div>"
        ],
        "app.less": [
            ".test-class{width:auto;}",
            "@import 'home';"
        ],
        "home.less": [
            ".home{}"
        ]
    }
}

The new fileTree property

As you can see, this is the json representation of the file structure explained earlier.

The new templates

Along with a different folder structure, we’ve added a few files that need to be populated by default. The templates we’ve provided will get you set up with a basic index page, layout area, and home page/controller. If you wish to add more, simply place the “files” array inside which ever folder object you’d like, and than create the definition in the templates array with the same file name.

package.json revised

{
  "name": "WebsiteName",
  "private": true,
  "version": "0.0.0",
  "description": "Description of your site",
  "repository": "",
  "license": "",
  "devDependencies": {
    "fs-path": "^0.0.22",
    "gulp": "^3.9.1",
    "gulp-autoprefixer": "latest",
    "gulp-concat": "^2.6.0",
    "gulp-filter": "^4.0.0",
    "gulp-ftp": "^1.1.0",
    "gulp-htmlmin": "3.0.0",
    "gulp-less": "^3.3.0",
    "gulp-load-plugins": "^1.2.4",
    "gulp-main-bower-files": "^1.5.3",
    "gulp-minify": "0.0.12",
    "gulp-order": "^1.1.1",
    "gulp-rename": "^1.2.2",
    "gulp-uglify": "^1.5.4",
    "gulp-uglifycss": "^1.0.6",
    "http-server": "^0.9.0",
    "mkdirp": "^0.5.1"
  },
  "scripts": {
    "preinstall": "npm install -g bower && npm install -g gulp && npm install -g gitignore-gen",
    "postinstall": "bower install && gulp setup",
    "start": "http-server -a localhost -p 8000 -c-1",
    "pretest": "npm install"
  },
  "dependencies": {}
}

Some of the build processes that we added to the gulp file require new plugs to work. Covering the possible usages of these plugins would require a blog post of its own, for now we’ll just recommend you copy the new devDependencies properties into your current file.

The final piece: running the build

Now that we have an organized app structure, the last thing to do is combine the scripts and less inside of build into bundles, and rearange them from their current organization into something a little less complex before placing them into public. This allows us to minimize the number of files we have to pull into our index page, as well as simplify file paths used in the application. Rather than rember the full path to your html file, you simply have to remember “html/yourFileName.html” which cuts down on confusion.

Below is the revised gulpFile.js

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

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

// Define default destination folder
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);
    }

    //give hard drive time to write files
    setTimeout(function () { gulp.start('default') }, 5000);
});

//Top level task definitions
gulp.task('default',['js', 'css', 'html']);
gulp.task('js', ['js-app', 'js-vend']);
gulp.task('css', ['css-app', 'css-vend']);

//Compile JS bower files
gulp.task('js-vend', function () {
    return gulp.src('bower.json')
        .pipe(plugins.mainBowerFiles('**/*.js'))
        .pipe(plugins.concat('vendor.js'))
        .pipe(plugins.minify({
            ext: {
                src: '.js',
                min: '.min.js'
            }
        }))
        .pipe(gulp.dest('public/js/'));
});

gulp.task('js-app', function () {

    //outputs app.js for all custom angular code
    gulp.src("build/app/" + '**/*.js')
        .pipe(plugins.order([
         '**/*.module.js',
         'core/**/*.js',
         'layout/**/*.js'
        ]))
        .pipe(plugins.concat("app.js"))
        .pipe(plugins.minify())
        .pipe(gulp.dest("public/js/"));
});

//HTML Tasks
gulp.task('html', function () {
    gulp.src('build/app/**/*.html')
        .pipe(plugins.htmlmin({ collapseWhitespace: true }))
        .pipe(gulp.dest('public/html/'));

        gulp.src('build/*.html')
        .pipe(plugins.htmlmin({ collapseWhitespace: true }))
        .pipe(gulp.dest('public/'));
});

//CSS Tasks
gulp.task('css-app', function () {
    gulp.src('build/less/app.less')
        .pipe(plugins.less())
        .pipe(plugins.autoprefixer({
            browsers: ['last 2 versions'],
            cascade: false
        }))
        .pipe(plugins.concat('app.css'))
        .pipe(gulp.dest('public/css/'))
        .pipe(plugins.uglifycss({
            maxLineLen: 500,
            expandVars: true
        }))
        .pipe(plugins.rename({ extname: ".min.css" }))
        .pipe(gulp.dest('public/css/'));

});
gulp.task('css-vend', function () {
    return gulp.src('bower.json')
     .pipe(plugins.mainBowerFiles('**/*.css'))
     .pipe(plugins.autoprefixer({
         browsers: ['last 2 versions'],
         cascade: false
     }))
     .pipe(plugins.uglifycss({
         maxLineLen: 500,
         expandVars: true
     }))
     .pipe(plugins.concat('vendor.css'))
     .pipe(gulp.dest('public/css/'));
});

//Methods
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]);
            }
        });
    }
}

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!");
    });
}

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!");
    });
}

function buildFileStructure(paths) {
    console.log(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 (isFile(curPath)) {
            fsPath.writeFile(__dirname + '/' + curPath, "", function (err) {
                if (err) {
                    return console.log(err);
                }

                console.log("The file was saved!");
            })
        } else {
            mkdirp(curPath, function (err) {
                if (err) { console.log(err) }
            });
        }
    }
}

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;
}

function isFile(path) {
    return /\.[0-9a-z]+$/i.test(path);
}

Combination tasks

These are the tasks you will want to run after editing files in the build folder. Its generally better to only run the task you need to build the app areas you need as some of the tasks (such as compiling all vendor files) may take a little time.

  • Default: Runs tasks “js”, “css” and “html”
  • js: Runs compilation/minification tasks for the custom app code, and also the javascipt included in the bower files
  • css: Runs compilation and minification task for your custom less files, as well as any css included in your bower packages

Updated Tasks

  • setup task: This task is largely the same, with the addition of a final call to the build processes.

New tasks

  • js-vend: This task selects any javascript defined in the “main” property of the bower.json files of your bower packages. Once selected, it then concatenates them, minifies them, and places them in the following address “public/js/”. Note: If you notice a package not getting included in your vendor.js file, there may be an issue with the packages bower.json. Always check to make sure “main” property of a packages bower.json file is pointing towards the correct file before assuming there is an issue with your gulp task.
  • Js-app: Similar to js-vent, this function selects, compiles, and minifies all of your files inside of your “app” folder. This function will place all files that end with “.module.js” at the top of the compiled file. It is mandatory that you name module definition files this way as they must be defined before the following code attempts to use them.
  • Html: This is the most simple of the build processes. It is responsible for moving html files out of the “app” file structure, minifying them, and then placing them in “public/html/”.
  • Css-app: This task selects a single main less file which we have called “app.less”. It then search for any import statements inside of that file which it will use to select other less files. Then it converts the files into the proper css, uglifies it, concatenates it, and places it inside of the “public/css” folder.
  • Css-vend: Exactly the same as js-vend. Main bower files are used to select vendor css files, compile them into one file, then place it into the “public/css” folder.

TL;DR

With the changes we’ve provided, simply run npm install to execute the following steps automatically:

  1. Make sure gulp and bower are installed on your machine
  2. Install bower packages (defined in bower.json)
  3. Run the gulp setup process (defined in gulpfile.js)
  4. Import the custom filetree object
  5. Create the file structure
  6. Create the .gitignore file
  7. Overwrite blank files with templates
  8. Run build compilation process
    1. Compile, minify and move JS
    2. Compile, minify and move CSS
    3. Compile, minify and move HTML

Once you’re done, all you have to do is enter npm start to spin up a local server and see your app run on http://localhost:8000.