Source: tasks/javascript/bundle.js

'use strict';

var Builder = require('systemjs-builder');

var fs = require('../fs');
var path = require('path');
var vm = require('vm');

var glob = require('glob');

/**
 * @module tasks/javascript/bundle
 * @description use systemJS to create bundles of the specified glob passed
 */

/**
 * taskFactory
 * @param options {Object}
 * @returns {Function}
 */
module.exports = function taskFactory(options) {

    var defaults = {
        destination     : '',
        product         : '',
        channel         : '',
        configLocation  : '',
        base            : '',
        arithmetic      : '',
        application     : {},
        bundles         : null,
        builder : {
            minify      : false,
            sourceMaps  : false,
            mangle      : false
        }
    };

    options = Object.assign({}, defaults, options);

    const DEFAULT_MODULE_FORMAT = 'umd';

    var fileConfig;
    var configSet = false;

    var sandbox = {
        System: {
            config: function (config) {
                fileConfig = config;
            }
        }
    };

    var configPath = path.resolve(options.configLocation);
    var builder = new Builder(options.base);

    var configOverride = {
        baseURL: options.base,
        paths: {
            'node_modules/*': './node_modules/*'
        }
    };

    /**
     * @param source {String}
     * @param format {String}
     * @returns {Object}
     */
    function getBuilderOptions(source, format) {

        var builderOptions = options.builder;

        if (bundleIsSelfExecuting(source)) {
            builderOptions.globalName = options.bundles[source].name || '';
        }

        if (format) {
            builderOptions.format = format;
        }

        return builderOptions;
    }

    /**
     * @description return the path for the module depending on, if any format is specified.
     * @param destination {String}
     * @param format {String}
     * @returns {String}
     */
    function getDestinationPath (destination, format) {

        if (format === DEFAULT_MODULE_FORMAT) {
            return destination;
        }

        var parts = path.parse(destination);

        return path.join(parts.dir, format, parts.base);
    }

    /**
     * @param source {String}
     * @returns {Array}
     */
    function getBundleFormats(source) {

        if (options.bundles && options.bundles[source] && options.bundles[source].format) {
            return options.bundles[source].format;
        }

        return [DEFAULT_MODULE_FORMAT];
    }

    /**
     * @param source {String}
     * @returns {boolean}
     */
    function bundleIsSelfExecuting(source) {
        return !!(options.bundles && options.bundles[source] && options.bundles[source].sfx);
    }

    /**
     * @param source {String}
     * @returns {boolean}
     */
    function bundleHasName(source) {
        return !!(options.bundles && options.bundles[source] && options.bundles[source].name);
    }

    /**
     * @description get message about created bundle
     * @param source {String}
     * @param destination {String}
     * @param format {String}
     * @returns {Boolean}
     */
    function logCreatedMessage(source, destination, format) {

        var message = 'bundle created: ' + destination + (bundleIsSelfExecuting(source) ? ' (' + format + ' self executing)' : '') + '\n';

        if (bundleIsSelfExecuting(source) && !bundleHasName(source)) {
            message += source + ' has not been assigned a name to expose to global scope\n';
        }

        return process.stdout.write(message);
    }

    /**
     * @description triggers promised builds of module for each of the specified formats in the bundles in
     * the bundle config or just for default module type if no config exists.
     * @param source {String} file path
     * @returns {Promise}
     */
    function createBundles(source) {

        var destination = path.join(options.destination, source);

        var bundleMethod = bundleIsSelfExecuting(source) ? 'buildStatic' : 'bundle';

        return Promise.all(getBundleFormats(source).map(function(format) {

            var builderOptions = getBuilderOptions(source, format);

            return createBundleForFormatType(source + options.arithmetic, destination, bundleMethod, builderOptions);
        }));
    }

    /**
     * @description executes the system JS builder for a module of the specified format
     * umd, amd, cjs or es6 for consumption in different environments.
     * @param expression {String}
     * @param destination {String}
     * @param bundleMethod {String}
     * @param builderOptions {Object}
     * @returns {Promise}
     */
    function createBundleForFormatType(expression, destination, bundleMethod, builderOptions) {
        return (function () {

            var bundleExpression = arguments[0];
            var bundleDestination = arguments[1];
            var bundleFormat = arguments[2].format;

            bundleDestination = arguments[1] = getDestinationPath(bundleDestination, bundleFormat);

            return builder[bundleMethod].apply(builder, arguments)
                .then(function () {
                    return logCreatedMessage(bundleExpression, bundleDestination, bundleFormat);
                })
                .catch(function (error) {
                    throw new Error(error);
                });
        })(expression, destination, builderOptions);
    }

    /**
     * @param sources {String} glob
     * @returns {Promise}
     */
    function bundle(sources) {
        return new Promise(function (resolve, reject) {
            glob(sources, {'cwd': options.base}, function (error, files) {

                if (error) {
                    reject(error);
                }

                return resolve(Promise.all(files.map(createBundles)));
            });
        });
    }

    /**
     * @param config
     */
    function setBuilderConfig(config) {
        if (!configSet) {
            builder.config(config);
            configSet = true;
        }
    }

    /**
     * @param config {Object}
     * @returns {Object}
     */
    function setApplicationConfig(config) {

        config.map.config = options.application;

        return config;
    }

    return function task(sources) {

        return fs.readFile(configPath)
            .then(function (data) {

                var code = data.toString();
                var script = new vm.Script(code, {filename: configPath});

                script.runInNewContext(sandbox);

                fileConfig = setApplicationConfig(fileConfig);

                return Object.assign(fileConfig, configOverride);
            })
            .then(function (config) {

                setBuilderConfig(config);

                return bundle(sources);
            })
            .catch(function (error) {
                throw new Error(error);
            });
    };
};