Butochnikov
9/18/2015 - 3:16 PM

Gulp compiler

Gulp compiler

{
    "private": true,
    "dependencies": {
        "gulp": "3.9.*",
        "gulp-util": "3.0.*",
        "gulp-concat": "2.6.*",
        "gulp-debug": "^2.1.0",
        "gulp-sourcemaps": "1.5.*",
        "yargs": "3.*",

        "gulp-uglify": "1.4.*",
        "gulp-minify-css": "^1.2.0",
        "gulp-autoprefixer": "^3.0.1",
        "gulp-gzip": "^1.2.0",

        "commonjs-require": "1.4.*",
        "gulp-wrap-commonjs": "0.1.*",
    }
}
var gulp    = require('gulp');
var console = require('gulp-util');
var argv    = require('yargs').argv;
var debug   = require('gulp-debug');

/*
 |--------------------------------------------------------------------------
 |                              Всякие утилиты
 |--------------------------------------------------------------------------
 */

var utils = {
    concat:     require('gulp-concat'),
    sourcemaps: require('gulp-sourcemaps'),
    commonjs:   require('gulp-wrap-commonjs'),
    prefixes:   require('gulp-autoprefixer')
};

/*
 |--------------------------------------------------------------------------
 |        Минификаторы для стилей, скриптов, картинок и мб ещё чего
 |--------------------------------------------------------------------------
 */

var minify = {
    css:  require('gulp-minify-css'),
    js:   require('gulp-uglify'),
    gzip: require('gulp-gzip')
};

/*
 |--------------------------------------------------------------------------
 |                            Сам компилятор
 |--------------------------------------------------------------------------
 */

var Compiler = (function () {
    // Окружения
    Compiler.ENV_LOCAL      = 'local';
    Compiler.ENV_PRODUCTION = 'production';

    // Типы данных
    Compiler.EXT_STYLE  = 'css';
    Compiler.EXT_SCRIPT = 'js';

    /**
     * @param format Тип данных
     * @param options Окружение
     * @constructor
     */
    function Compiler(format, options) {
        this.options = {
            // Environment
            env: options.env || Compiler.ENV_LOCAL,

            // Storage path
            storage: (options.storage || 'storage') + '/' + this.hash() + '/',

            // Public path
            publish: options.publish || 'out',

            // Default namespace (CommonJS default path)
            base: options.commonJsBase || ['app'],

            // File paths
            paths: options.paths || []
        };

        this.format       = format || Compiler.EXT_SCRIPT;
        this.streams      = [];
        this.files        = [];
        this.prependFiles = [];
    }

    /**
     * Prepend files in result (shims as example)
     *
     * @param file
     * @returns {Compiler}
     */
    Compiler.prototype.prepend = function (file) {
        for (var i = 0; i < this.prependFiles; i++) {
            if (this.prependFiles[i] === file) {
                return this;
            }
        }

        this.prependFiles.push(file);

        return this;
    };

    /**
     * Simple function of random hash
     *
     * @returns {string}
     */
    Compiler.prototype.hash = function () {
        return Math.floor((1 + Math.random()) * 0x10)
            .toString(16);
    };

    /**
     * Добавить файл (с компилем)
     *
     * @param files Файл
     * @param compiler Коллбек, принимающий стрим, должен возвращать стрим
     * @param wrap
     * @returns {Compiler}
     */
    Compiler.prototype.add = function (files, compiler, wrap) {
        var self = this;

        if (typeof wrap === 'undefined' || wrap == null) {
            wrap = true;
        }

        if (typeof files === 'undefined' || files == null) {
            throw new Error('Files not exists');
        }

        if (!(files instanceof Array)) {
            files = [files];
        }


        // Appends base paths
        var newFileList = files.slice();
        for (var i = 0; i < this.options.paths.length; i++) {
            var path = this.options.paths[i];

            for (var j = 0; j < files.length; j++) {
                newFileList.push(path + '/' + files[j]);
            }
        }
        files = newFileList;


        var name = 'id' + (this.streams.length + 1) + '_' +
            this.hash() + this.hash() + '.' + this.format;
        this.files.push(this.options.storage + name);

        var stream = gulp
            .src(files)
            .pipe(debug({title: 'add:'}))
            .on('error', function (error) {
                console.log('Build error:', error.message);
            })
            .pipe(utils.sourcemaps.init());

        // Inject compiler here
        if (compiler != null) {
            stream = compiler(stream);
        }

        // Inject CommonJS for scripts
        if (wrap && this.format === Compiler.EXT_SCRIPT) {
            stream = stream
                .pipe(utils.commonjs({
                    pathModifier: function (path) {
                        var base = self.options.base;
                        path = path.replace(/\\/g, '/');
                        for (var i = 0; i < base.length; i++) {
                            path = path.replace(new RegExp('.*?\/' + (base[i]) + '\/', 'g'), '')
                        }
                        return path.replace(/\.js|\.es6|\.jsx$/, '');
                    }
                }));
        }

        // Inject autoprefixer
        if (this.format === Compiler.EXT_STYLE) {
            stream = stream
                .pipe(utils.prefixes({
                    browsers: ['last 2 versions'],
                    cascade:  false
                }));
        }

        stream = stream
            .pipe(utils.concat(name))
            .pipe(utils.sourcemaps.write());

        this.streams.push(stream);

        return this;
    };

    /**
     * Сборка всего в одно
     *
     * @param outputName
     * @param callback
     * @returns {Compiler}
     */
    Compiler.prototype.build = function (outputName, callback) {
        var self    = this;
        var streams = this.streams.length;
        var current = 0;

        if (typeof callback === 'undefined' || !(callback instanceof Function)) {
            callback = function (output) {
                console.log('File was be published at:', output);
            };
        }

        // On all streams was finish
        var finish = function () {
            var stream = gulp
                .src(self.prependFiles.concat(self.files), {
                    strict:     true,
                    allowEmpty: false
                })
                .pipe(debug({title: 'build:'}))
                .on('error', function (error) {
                    console.log('Error when merge result:', error.message);
                })
                .pipe(utils.sourcemaps.init({loadMaps: true}))
                .pipe(utils.concat(outputName));

            // Minify
            if (self.options.env === Compiler.ENV_PRODUCTION) {
                if (self.format === Compiler.EXT_SCRIPT) {
                    stream = stream.pipe(minify.js());

                } else if (self.format === Compiler.EXT_STYLE) {
                    stream = stream.pipe(minify.css());
                }
            }

            stream = stream.pipe(utils.sourcemaps.write('./'));

            // Gzip
            if (self.options.env === Compiler.ENV_PRODUCTION) {
                stream = stream
                    .pipe(gulp.dest(self.options.publish))
                    .pipe(minify.gzip());
            }

            stream = stream
                .pipe(gulp.dest(self.options.publish))
                .on('finish', function () {
                    if (callback != null) {
                        callback(self.options.publish + '/' + outputName);
                    }
                });

            return stream;
        };

        /**
         * Merge stream builds
         */
        this.streams.forEach(function (stream, index) {
            stream
                .pipe(gulp.dest(self.options.storage))
                .on('finish', function () {
                    current++;
                    if (streams === current) {
                        return finish();
                    }
                });
        });


        return this;
    };


    /*
     |--------------------------------------------------------------------------
     |                                  COMPILERS
     |--------------------------------------------------------------------------
     */


    /**
     * @param files
     * @param commonJsWrap
     * @returns {*}
     */
    Compiler.prototype.js = function (files, commonJsWrap) {
        return this.add(files, function (stream) {
            return stream;
        }, commonJsWrap || false);
    };

    /**
     * @param files
     * @returns {*}
     */
    Compiler.prototype.css = function (files) {
        return this.add(files, function (stream) {
            return stream;
        }, false);
    };

    /**
     * @param files
     * @param commonJsWrap
     * @returns {*}
     */
    Compiler.prototype.coffee = function (files, commonJsWrap) {
        return this.add(files, function (stream) {
            var coffee = require('gulp-coffee');

            return stream.pipe(
                coffee({bare: true}).on('error', function (error) {
                    console.log('Coffee Error:', error.message);
                })
            );
        }, commonJsWrap || false);
    };

    /**
     * @param files
     * @param options
     * @returns {Compiler}
     */
    Compiler.prototype.babel = function (files, options) {
        options = {
            modules:   (options.modules || 'common'),
            optional:  (options.optional || []),
            blacklist: (options.blacklist || []),
            plugins:   (options.plugins || []),
            loose:     (options.loose || [])
        };

        if (this.options.env === Compiler.ENV_PRODUCTION) {
            options.optional.push('minification.removeConsole');
            options.optional.push('minification.removeDebugger');
        }

        return this.add(files, function (stream) {
            var babel = require('gulp-babel');

            return stream.pipe(
                babel(options).on('error', function (error) {
                    console.log('Babel Error:', error.message, '[opt]: ', options);
                })
            );
        }, options.modules === 'common');
    };

    /**
     * @param files
     * @returns {*}
     */
    Compiler.prototype.sass = function (files) {
        return this.add(files, function (stream) {
            var sass = require('gulp-sass');

            return stream.pipe(
                sass().on('error', function (error) {
                    console.log('Sass Error:', error.message);
                })
            );
        }, false);
    };

    /**
     * @param files
     * @returns {*}
     */
    Compiler.prototype.less = function (files) {
        return this.add(files, function (stream) {
            var less = require('gulp-less');

            return stream.pipe(
                less({}).on('error', function (error) {
                    console.log('Less Error:', error.message);
                })
            );
        }, false);
    };

    return Compiler;
})();


/* ============================= *
 *         TASKS (EXAMPLE)       *
 * ============================= */

// Compiler options
var options = {
    env:          argv.production || Compiler.ENV_LOCAL,
    storage:      'storage/assets',
    publish:      'public/assets',
    commonJsBase: [
        'javascripts/src',
        'javascripts/app'
    ],
    paths:        [
        'resources/stylesheets',
        'resources/javascripts'
    ]
};

gulp.task('scripts', function () {
    // Example babel options
    var babel = {
        optional: [
            'es7.decorators',
            'es7.classProperties',
            'es7.objectRestSpread',
            'es7.functionBind',
            'es7.trailingFunctionCommas'
        ],
        loose:    [
            'es6.classes'
        ]
    };

    // JavaScripts example
    new Compiler(Compiler.EXT_SCRIPT, options)

        .js([
            require.resolve('commonjs-require/commonjs-require'),
            require.resolve('babel-core/browser-polyfill')
            // another...
        ])

        .babel([
            'resources/javascripts/src/**/*.js',
            'resources/javascripts/app/**/*.js'
        ], babel)

        .build('app.js', function (output) {
            console.log('File was be published at:', output);
        });
});


gulp.task('styles', function () {
    // Styles example
    new Compiler(Compiler.EXT_STYLE, options)
        .sass('layout.scss')
        .build('app.css', function (output) {
            console.log('File was be published at:', output);
        });
});


gulp.task('default', ['scripts', 'styles'], function () {

});
{
    "dependencies": {
        // Babel (ES6, ES7, Flow, etc...)
        "babel-core": "5.8.*",
        "gulp-babel": "5.2.*",
        
        // CoffeeScript
        "gulp-coffee": "^2.3.1",
        
        // Sass and Scss
        "gulp-sass": "^2.0.3",
        
        // Less
        "gulp-less": "^3.0.3"
    }
}