<commit_before>// Project
// -------
// Model. A single TileMill map project. Describes an MML JSON map object that
// can be used by `carto` to render a map.
model = Backbone.Model.extend({});

model.prototype.schema = {
    'type': 'object',
    'properties': {
        // Mapnik-specific properties.
        'srs': {
            'type': 'string',
            'required': true
        },
        'Stylesheet': {
            'type': ['object', 'array'],
            'required': true
        },
        'Layer': {
            'type': ['object', 'array'],
            'required': true
        },

        // TileMill-specific properties. @TODO these need a home, see
        // https://github.com/mapbox/tilelive-mapnik/issues/4
        'format': {
            'type': 'string',
            'enum': ['png', 'png24', 'png8', 'jpeg80', 'jpeg85', 'jpeg90', 'jpeg95']
        },
        'interactivity': {
            'type': ['object', 'boolean']
        },

        // TileJSON properties.
        'name':        { 'type': 'string' },
        'description': { 'type': 'string' },
        'version':     {
            'type': 'string',
            'description': 'Semver compatible version string.',
            'pattern': '\\d+\\.\\d+\\.\\d+\\w?[\\w\\d]*'
        },
        'attribution': { 'type': 'string' },
        'legend':      { 'type': 'string' },
        'minzoom': {
            'minimum': 0,
            'maximum': 22,
            'type': 'integer'
        },
        'maxzoom': {
            'minimum': 0,
            'maximum': 22,
            'type': 'integer'
        },
        'bounds': {
            'type': 'array',
            'minItems': 4,
            'maxItems': 4,
            'items': [
                { 'type':'number', 'minimum': -180, 'maximum':180 },
                { 'type':'number', 'minimum': -85.05112877980659, 'maximum': 85.05112877980659 },
                { 'type':'number', 'minimum': -180, 'maximum':180 },
                { 'type':'number', 'minimum': -85.05112877980659, 'maximum': 85.05112877980659 }
            ]
        },
        'center': {
            'type': 'array',
            'minItems': 3,
            'maxItems': 3,
            'items': [
                { 'type':'number', 'minimum':-180, 'maximum':180 },
                { 'type':'number', 'minimum':-85.0511, 'maximum':85.0511 },
                { 'type':'integer', 'minimum':0, 'maximum':22 }
            ]
        },

        // Non-stored properties.
        // @TODO make this writable at some point
        'scheme': {
            'type': 'string',
            'ignore': true
        },
        // @TODO make this writable at some point
        'formatter': {
            'type': 'string',
            'ignore': true
        },
        'tilejson': {
            'type': 'string',
            'ignore': true
        },
        'tiles': {
            'type': 'array',
            'required': true,
            'items': { 'type': 'string' },
            'ignore': true
        },
        'grids': {
            'type': 'array',
            'items': { 'type': 'string' },
            'ignore': true
        },
        '_updated': {
            'type': 'integer',
            'description': 'Last update time of project',
            'ignore': true
        },
        'id': {
            'type': 'string',
            'required': true,
            'pattern': '^[A-Za-z0-9\-_]+$',
            'title': 'Name',
            'description': 'Name may include alphanumeric characters, dashes and underscores.',
            'ignore': true
        }
    }
};

model.prototype.STYLESHEET_DEFAULT = [{
    id: 'style.mss',
    data: 'Map {\n'
        + '  background-color: #fff;\n'
        + '}\n\n'
        + '#world {\n'
        + '  polygon-fill: #eee;\n'
        + '  line-color: #ccc;\n'
        + '  line-width: 0.5;\n'
        + '}'
}];

model.prototype.LAYER_DEFAULT = [{
    id: 'world',
    name: 'world',
    srs: '+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 '
    + '+lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +no_defs +over',
    geometry: 'polygon',
    Datasource: {
        file: 'http://tilemill-data.s3.amazonaws.com/world_borders_merc.zip',
        type: 'shape'
    }
}];

model.prototype.defaults = {
    'bounds': [-180,-85.0511,180,85.0511],
    'center': [0,0,2],
    'format': 'png',
    'interactivity': false,
    'minzoom': 0,
    'maxzoom': 22,
    'srs': '+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 '
        + '+lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +no_defs +over',
    'Stylesheet': [],
    'Layer': []
};

// Set model options to `this.options`.
model.prototype.initialize = function(attributes, options) {
    this.options = options;
};

// Custom setDefaults() method for creating a project with default layers,
// stylesheets, etc. Note that we do not use Backbone native initialize()
// or defaults(), both of which make default values far pervasive than the
// expected use here.
model.prototype.setDefaults = function() {
    var template = {};
    !this.get('Stylesheet').length && (template.Stylesheet = this.STYLESHEET_DEFAULT);
    !this.get('Layer').length && (template.Layer = this.LAYER_DEFAULT);
    this.set(this.parse(template), { silent: true });
};

// Instantiate collections from arrays.
model.prototype.parse = function(resp) {
    resp.Stylesheet && (resp.Stylesheet = new models.Stylesheets(
        resp.Stylesheet,
        {parent: this}
    ));
    resp.Layer && (resp.Layer = new models.Layers(
        resp.Layer,
        {parent: this}
    ));
    return resp;
};

model.prototype.url = function() {
    return '/api/Project/' + this.id;
};

// Adds:
// - id uniqueness checking.
// - interactivity check to ensure the referenced layer exists.
model.prototype.validate = function(attr) {
    var error = this.validateAttributes(attr);
    if (error) return error;

    if (attr.id &&
        this.collection &&
        this.collection.get(attr.id) &&
        this.collection.get(attr.id) !== this)
            return new Error(_('Project "<%=id%>" already exists.').template(attr));

    if (attr.interactivity && attr.interactivity.layer && attr.Layer) {
        var id = attr.interactivity.layer;
        var layers = _(attr.Layer).isArray()
            ? attr.Layer
            : attr.Layer.toJSON();
        if (!_(layers).chain().pluck('id').include(id).value())
            return new Error(_('Interactivity layer "<%=obj%>" does not exist.').template(id));
    }

    if (attr.bounds && attr.bounds[0] >= attr.bounds[2])
        return new Error('Bounds W must be less than E.');
    if (attr.bounds && attr.bounds[1] >= attr.bounds[3])
        return new Error('Bounds S must be less than N.');

    if (attr.center && attr.bounds &&
       (attr.center[0] < attr.bounds[0] ||
        attr.center[0] > attr.bounds[2] ||
        attr.center[1] < attr.bounds[1] ||
        attr.center[1] > attr.bounds[3]))
        return new Error('Center must be within bounds.');
    if (attr.center && attr.minzoom && (attr.center[2] < attr.minzoom))
        return new Error('Center must be within zoom range.');
    if (attr.center && attr.maxzoom && (attr.center[2] > attr.maxzoom))
        return new Error('Center must be within zoom range.');
};

// Single tile thumbnail URL generation. From [OSM wiki][1].
// [1]: http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#lon.2Flat_to_tile_numbers_2
model.prototype.thumb = function() {
    var z = this.get('center')[2];
    var lat_rad = this.get('center')[1] * Math.PI / 180 * -1; // -1 for TMS (flipped from OSM)
    var x = parseInt((this.get('center')[0] + 180.0) / 360.0 * Math.pow(2, z));
    var y = parseInt((1.0 - Math.log(Math.tan(lat_rad) + (1 / Math.cos(lat_rad))) / Math.PI) / 2.0 * Math.pow(2, z));
    return this.get('tiles')[0]
        .replace('{z}', z)
        .replace('{x}', x)
        .replace('{y}', y);
};

// Hit the project poll endpoint.
model.prototype.poll = function(options) {
    if (Bones.server) throw Error('Client-side method only.');
    $.ajax({
        url: this.url() + '/' + this.get('_updated'),
        type: 'GET',
        contentType: 'application/json',
        processData: false,
        success: _(function(resp) {
            if (!_(resp).keys().length) return;
            if (!this.set(this.parse(resp))) return;
            this.trigger('poll', this, resp);
            if (options.success) options.success(this, resp);
        }).bind(this),
        error: _(function(resp) {
            if (options.error) options.error(this, resp);
        }).bind(this)
    });
};

// Hit the project flush endpoint.
model.prototype.flush = function(layer, url, options) {
    if (Bones.server) throw Error('Client-side method only.');
    if (!this.get('Layer').get(layer)) throw Error('No layer ' + layer + ' found.');

    $.ajax({
        url: this.url() + '/' + layer + '?' + $.param({url: url}),
        type: 'DELETE',
        contentType: 'application/json',
        data: JSON.stringify({'bones.token':Backbone.csrf(this.url())}),
        dataType: 'json',
        processData: false,
        success: _(function(resp) {
            this.trigger('change');
            if (options.success) options.success(this, resp);
        }).bind(this),
        error: _(function(resp) {
            if (options.error) options.error(this, resp);
        }).bind(this)
    });
};

<commit_msg>New default project style, with MapBox blue & Nat. Earth data.<commit_after>// Project
// -------
// Model. A single TileMill map project. Describes an MML JSON map object that
// can be used by `carto` to render a map.
model = Backbone.Model.extend({});

model.prototype.schema = {
    'type': 'object',
    'properties': {
        // Mapnik-specific properties.
        'srs': {
            'type': 'string',
            'required': true
        },
        'Stylesheet': {
            'type': ['object', 'array'],
            'required': true
        },
        'Layer': {
            'type': ['object', 'array'],
            'required': true
        },

        // TileMill-specific properties. @TODO these need a home, see
        // https://github.com/mapbox/tilelive-mapnik/issues/4
        'format': {
            'type': 'string',
            'enum': ['png', 'png24', 'png8', 'jpeg80', 'jpeg85', 'jpeg90', 'jpeg95']
        },
        'interactivity': {
            'type': ['object', 'boolean']
        },

        // TileJSON properties.
        'name':        { 'type': 'string' },
        'description': { 'type': 'string' },
        'version':     {
            'type': 'string',
            'description': 'Semver compatible version string.',
            'pattern': '\\d+\\.\\d+\\.\\d+\\w?[\\w\\d]*'
        },
        'attribution': { 'type': 'string' },
        'legend':      { 'type': 'string' },
        'minzoom': {
            'minimum': 0,
            'maximum': 22,
            'type': 'integer'
        },
        'maxzoom': {
            'minimum': 0,
            'maximum': 22,
            'type': 'integer'
        },
        'bounds': {
            'type': 'array',
            'minItems': 4,
            'maxItems': 4,
            'items': [
                { 'type':'number', 'minimum': -180, 'maximum':180 },
                { 'type':'number', 'minimum': -85.05112877980659, 'maximum': 85.05112877980659 },
                { 'type':'number', 'minimum': -180, 'maximum':180 },
                { 'type':'number', 'minimum': -85.05112877980659, 'maximum': 85.05112877980659 }
            ]
        },
        'center': {
            'type': 'array',
            'minItems': 3,
            'maxItems': 3,
            'items': [
                { 'type':'number', 'minimum':-180, 'maximum':180 },
                { 'type':'number', 'minimum':-85.0511, 'maximum':85.0511 },
                { 'type':'integer', 'minimum':0, 'maximum':22 }
            ]
        },

        // Non-stored properties.
        // @TODO make this writable at some point
        'scheme': {
            'type': 'string',
            'ignore': true
        },
        // @TODO make this writable at some point
        'formatter': {
            'type': 'string',
            'ignore': true
        },
        'tilejson': {
            'type': 'string',
            'ignore': true
        },
        'tiles': {
            'type': 'array',
            'required': true,
            'items': { 'type': 'string' },
            'ignore': true
        },
        'grids': {
            'type': 'array',
            'items': { 'type': 'string' },
            'ignore': true
        },
        '_updated': {
            'type': 'integer',
            'description': 'Last update time of project',
            'ignore': true
        },
        'id': {
            'type': 'string',
            'required': true,
            'pattern': '^[A-Za-z0-9\-_]+$',
            'title': 'Name',
            'description': 'Name may include alphanumeric characters, dashes and underscores.',
            'ignore': true
        }
    }
};

model.prototype.STYLESHEET_DEFAULT = [{
    id: 'style.mss',
    data: 'Map {\n'
        + '  background-color: #b8dee6;\n'
        + '}\n\n'
        + '#countries {\n'
        + '  ::outline {\n'
        + '    line-color: #85c5d3;\n'
        + '    line-width: 2;\n'
        + '    line-join: round;\n'
        + '  }\n'
        + '  polygon-fill: #fff;\n'
        + '}'
}];

model.prototype.LAYER_DEFAULT = [{
    id: 'countries',
    name: 'countries',
    srs: '+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 '
    + '+lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +no_defs +over',
    geometry: 'polygon',
    Datasource: {
        file: 'http://mapbox-geodata.s3.amazonaws.com/natural-earth-1.3.0/cultural/10m-admin-0-countries.zip',
        type: 'shape'
    }
}];

model.prototype.defaults = {
    'bounds': [-180,-85.0511,180,85.0511],
    'center': [0,0,2],
    'format': 'png',
    'interactivity': false,
    'minzoom': 0,
    'maxzoom': 22,
    'srs': '+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 '
        + '+lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +no_defs +over',
    'Stylesheet': [],
    'Layer': []
};

// Set model options to `this.options`.
model.prototype.initialize = function(attributes, options) {
    this.options = options;
};

// Custom setDefaults() method for creating a project with default layers,
// stylesheets, etc. Note that we do not use Backbone native initialize()
// or defaults(), both of which make default values far pervasive than the
// expected use here.
model.prototype.setDefaults = function() {
    var template = {};
    !this.get('Stylesheet').length && (template.Stylesheet = this.STYLESHEET_DEFAULT);
    !this.get('Layer').length && (template.Layer = this.LAYER_DEFAULT);
    this.set(this.parse(template), { silent: true });
};

// Instantiate collections from arrays.
model.prototype.parse = function(resp) {
    resp.Stylesheet && (resp.Stylesheet = new models.Stylesheets(
        resp.Stylesheet,
        {parent: this}
    ));
    resp.Layer && (resp.Layer = new models.Layers(
        resp.Layer,
        {parent: this}
    ));
    return resp;
};

model.prototype.url = function() {
    return '/api/Project/' + this.id;
};

// Adds:
// - id uniqueness checking.
// - interactivity check to ensure the referenced layer exists.
model.prototype.validate = function(attr) {
    var error = this.validateAttributes(attr);
    if (error) return error;

    if (attr.id &&
        this.collection &&
        this.collection.get(attr.id) &&
        this.collection.get(attr.id) !== this)
            return new Error(_('Project "<%=id%>" already exists.').template(attr));

    if (attr.interactivity && attr.interactivity.layer && attr.Layer) {
        var id = attr.interactivity.layer;
        var layers = _(attr.Layer).isArray()
            ? attr.Layer
            : attr.Layer.toJSON();
        if (!_(layers).chain().pluck('id').include(id).value())
            return new Error(_('Interactivity layer "<%=obj%>" does not exist.').template(id));
    }

    if (attr.bounds && attr.bounds[0] >= attr.bounds[2])
        return new Error('Bounds W must be less than E.');
    if (attr.bounds && attr.bounds[1] >= attr.bounds[3])
        return new Error('Bounds S must be less than N.');

    if (attr.center && attr.bounds &&
       (attr.center[0] < attr.bounds[0] ||
        attr.center[0] > attr.bounds[2] ||
        attr.center[1] < attr.bounds[1] ||
        attr.center[1] > attr.bounds[3]))
        return new Error('Center must be within bounds.');
    if (attr.center && attr.minzoom && (attr.center[2] < attr.minzoom))
        return new Error('Center must be within zoom range.');
    if (attr.center && attr.maxzoom && (attr.center[2] > attr.maxzoom))
        return new Error('Center must be within zoom range.');
};

// Single tile thumbnail URL generation. From [OSM wiki][1].
// [1]: http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#lon.2Flat_to_tile_numbers_2
model.prototype.thumb = function() {
    var z = this.get('center')[2];
    var lat_rad = this.get('center')[1] * Math.PI / 180 * -1; // -1 for TMS (flipped from OSM)
    var x = parseInt((this.get('center')[0] + 180.0) / 360.0 * Math.pow(2, z));
    var y = parseInt((1.0 - Math.log(Math.tan(lat_rad) + (1 / Math.cos(lat_rad))) / Math.PI) / 2.0 * Math.pow(2, z));
    return this.get('tiles')[0]
        .replace('{z}', z)
        .replace('{x}', x)
        .replace('{y}', y);
};

// Hit the project poll endpoint.
model.prototype.poll = function(options) {
    if (Bones.server) throw Error('Client-side method only.');
    $.ajax({
        url: this.url() + '/' + this.get('_updated'),
        type: 'GET',
        contentType: 'application/json',
        processData: false,
        success: _(function(resp) {
            if (!_(resp).keys().length) return;
            if (!this.set(this.parse(resp))) return;
            this.trigger('poll', this, resp);
            if (options.success) options.success(this, resp);
        }).bind(this),
        error: _(function(resp) {
            if (options.error) options.error(this, resp);
        }).bind(this)
    });
};

// Hit the project flush endpoint.
model.prototype.flush = function(layer, url, options) {
    if (Bones.server) throw Error('Client-side method only.');
    if (!this.get('Layer').get(layer)) throw Error('No layer ' + layer + ' found.');

    $.ajax({
        url: this.url() + '/' + layer + '?' + $.param({url: url}),
        type: 'DELETE',
        contentType: 'application/json',
        data: JSON.stringify({'bones.token':Backbone.csrf(this.url())}),
        dataType: 'json',
        processData: false,
        success: _(function(resp) {
            this.trigger('change');
            if (options.success) options.success(this, resp);
        }).bind(this),
        error: _(function(resp) {
            if (options.error) options.error(this, resp);
        }).bind(this)
    });
};

<|endoftext|>


<commit_before>/*global module:false*/
module.exports = function(grunt) {

	var _ = grunt.utils._;
	var _dirs = {
		src: "src",
		dest: "web/sdk",
		tests: "tests"
	};
	var _dontWrap = [
		_dirs.src + "/backplane.js",
		_dirs.src + "/loader.js",
		_dirs.src + "/third-party/jquery.js",
		_dirs.src + "/third-party/echo.jquery.noconflict.js"
	];
	var _dontStripDocs = grunt.file.expandFiles(_dirs.src + "/third-party/*.js");
	var _config = {
		dirs: _dirs,
		pkg: "<json:js-sdk.json>",
		meta: {
			banner:
				"/**\n" +
				" * Copyright (c) 2006-<%= grunt.template.today(\"UTC:yyyy\") %> <%= pkg.author.name %> <<%= pkg.author.email %>>. All rights reserved.\n" +
				" * You may copy and modify this script as long as the above copyright notice,\n" +
				" * this condition and the following disclaimer is left intact.\n" +
				" * This software is provided by the author \"AS IS\" and no warranties are\n" +
				" * implied, including fitness for a particular purpose. In no event shall\n" +
				" * the author be liable for any damages arising in any way out of the use\n" +
				" * of this software, even if advised of the possibility of such damage.\n" +
				" *\n" +
				" * Version: <%= pkg.version %> (<%= grunt.template.today(\"UTC:yyyy-mm-dd HH:MM:ss Z\") %>)\n" +
				" */"
		},
		clean: {
			docs: ["<%= dirs.dest %>/docs"],
			all: ["<%= dirs.dest %>"]
		},
		exec: {
			docs: {
				command: "jsduck src --ignore-global --title=\"Echo JS SDK\" --pretty-json --output web/sdk/docs"
						//	jsduck src --ignore-global --title="Echo JS SDK" --pretty-json --output web/sdk/docs --meta-tags=tools/jsduck.tags.rb
			}
		},
		copy: {
			js: {
				files: {
					"<%= dirs.dest %>": ["<%= dirs.src %>/**/*.js"]
				},
				options: {
					basePath: _dirs.src,
					processContent: function(code) {
						return grunt.helper("wrap_code", grunt.helper("strip_docs", code));
					},
					processContentExclude: _dontWrap
				}
			},
			"images-css": {
				files: {
					"<%= dirs.dest %>": [
						"<%= dirs.src %>/**/*.css",
						"<%= dirs.src %>/**/*.png",
						"<%= dirs.src %>/**/*.jpg",
						"<%= dirs.src %>/**/*.gif"
					]
				},
				options: {
					basePath: _dirs.src
				}
			},
			tests: {
				files: {
					"<%= dirs.dest %>": ["<%= dirs.tests %>/**/*"]
				}
			}
		},
		concat: {
			loader: {
				src: [
					"<banner:meta.banner>",
					"<%= dirs.src %>/third-party/yepnope.1.5.4-min.js",
					"<%= dirs.src %>/loader.js"
				],
				dest: "<%= dirs.dest %>/loader.js"
			}
		},
		packs: {
			"api": [
				"<%= dirs.src %>/api.js",
				"<%= dirs.src %>/stream-server/api.js",
				"<%= dirs.src %>/identity-server/api.js"
			],
			"environment": [
				"<%= dirs.src %>/utils.js",
				"<%= dirs.src %>/events.js",
				"<%= dirs.src %>/labels.js",
				"<%= dirs.src %>/configuration.js",
				"<file_strip_banner:<%= dirs.dest %>/api.pack.js>",
				"<%= dirs.src %>/user-session.js",
				"<%= dirs.src %>/control.js",
				"<%= dirs.src %>/product.js",
				"<%= dirs.src %>/plugin.js",
				"<%= dirs.src %>/button.js",
				"<%= dirs.src %>/tabs.js"
			],
			"third-party/jquery": [
				"<%= dirs.src %>/third-party/jquery.js",
				"<%= dirs.src %>/third-party/echo.jquery.noconflict.js",
				"<%= dirs.src %>/third-party/jquery.ihint.js",
				"<%= dirs.src %>/third-party/jquery.viewport.mini.js",
				"<%= dirs.src %>/third-party/jquery.easing-1.3.min.js",
				"<%= dirs.src %>/third-party/jquery.fancybox-1.3.4.min.js",
				"<%= dirs.src %>/third-party/echo.fancybox.css.js",
				"<%= dirs.src %>/third-party/jquery.ui-1.8.21.min.js",
				"<%= dirs.src %>/third-party/jquery.isotope.min.js",
				"<%= dirs.src %>/third-party/bootstrap/bootstrap-tab.js"
			]
		},
		//min: {
		//	all: ["**/*.pack.js"]
		//},
		//qunit: {
		//	files: ["test/**/*.html"]
		//},
		lint: {
			// TODO: exclude third-party and qunit from linting
			// TODO: lint dest dir cause all files there are properly wrapped
			files: [
				"grunt.js",
				"<%= dirs.dest %>/**/*.js",
				"<%= dirs.tests %>/**/*.js"
			]
		},
		//watch: {
		//	files: "<config:lint.files>",
		//	tasks: "lint qunit"
		//},
		jshint: {
			options: {
				curly: true,
				eqeqeq: true,
				immed: true,
				latedef: true,
				newcap: true,
				noarg: true,
				sub: true,
				undef: true,
				boss: true,
				eqnull: true,
				browser: true
			},
			globals: {
				Echo: true,
				jQuery: true,
				QUnit: true
			}
		},
		uglify: {}
	};

	_.each(["stream-server", "identity-server", "app-server"], function(name) {
		_config.packs[name + "/controls"] = ["<%= dirs.src %>/" + name + "/controls/*.js"];
		_config.packs[name + "/plugins"] = ["<%= dirs.src %>/" + name + "/plugins/*.js"];
		_config.packs[name] = _config.packs[name + "/controls"].concat(_config.packs[name + "/plugins"]);
	});

	grunt.initConfig(_config);

	// ==========================================================================
	// TASKS
	// ==========================================================================

	grunt.loadNpmTasks("grunt-exec");
	grunt.loadNpmTasks("grunt-contrib");
	grunt.loadTasks("tasks");

	grunt.registerMultiTask("packs", "Assemble packages", function() {
		var name = this.target + ".pack.js";
		grunt.log.write("Assembling \"" + name + "\"...");
		var files = grunt.file.expandFiles(this.file.src);
		var code = grunt.helper("wrap_and_concat", files, this.target);
		code = grunt.template.process(grunt.config("meta.banner")) + "\n" + code;
		grunt.file.write(grunt.config("dirs.dest") + "/" + name, code);

		if (this.errorCount) {
			return false;
		}
		grunt.log.ok();
	});

	grunt.registerTask("docs", "clean:docs exec:docs");

	// Default task
	grunt.registerTask("default", "clean copy concat:loader packs");

	// ==========================================================================
	// HELPERS
	// ==========================================================================

	var _stripDocsRE = /[^\r\n]*\/\*[\s\S]*?\*\/\n/g;
	grunt.registerHelper("strip_docs", function(code, filepath) {
		if (_.include(_dontStripDocs, filepath)) {
			return code;
		}
		return code.replace(_stripDocsRE, function(match) {
			if (/\s+\* @\w/.test(match)) {
				return "";
			}
			return match;
		});
	});

	grunt.registerHelper("wrap_and_concat", function(files, name) {
		if (!files) {
			return "";
		}
		return files.map(function(filepath) {
			var code = grunt.task.directive(filepath, grunt.file.read);
			code = grunt.helper("wrap_code", code, filepath);
			code = grunt.helper("strip_docs", code, filepath);
			return code;
		}).join(grunt.utils.normalizelf(grunt.utils.linefeed));
	});

	grunt.registerHelper("wrap_code", function(code, filepath) {
		if (_.include(_dontWrap, filepath)) {
			return code;
		}
		if (!/third-party|\.pack\.js/.test(filepath)) {
			var doc = code.match(/@class ((?:\w+\.?)+)\s/);
			var docClass = doc && doc[1];
			if (docClass) {
				var parts = docClass.split(".Plugins.");
				// if it's plugin file
				if (parts[1]) {
					// we don't wrap plugin files at the moment because they can have 2 plugins in 1 file
					//code = "var plugin = Echo.Plugin.manifest(\"" + parts[1] + "\", \"" + parts[0] + "\");\n\n" +
					//	"if (Echo.Plugin.isDefined(plugin)) return;\n\n" + code;
				} else {
					code = "if (Echo.Utils.isComponentDefined(\"" + docClass + "\")) return;\n\n" + code;
				}
			} else if (filepath) {
				grunt.log.writeln("File " + filepath + " is missing '@class' definition");
			}
		}
		return "(function(jQuery) {\nvar $ = jQuery;\n\n" + code + "\n})(Echo.jQuery);\n";
	});
};
<commit_msg>We should wrap code before stripping docs because we need some info from docs<commit_after>/*global module:false*/
module.exports = function(grunt) {

	var _ = grunt.utils._;
	var _dirs = {
		src: "src",
		dest: "web/sdk",
		tests: "tests"
	};
	var _dontWrap = [
		_dirs.src + "/backplane.js",
		_dirs.src + "/loader.js",
		_dirs.src + "/third-party/jquery.js",
		_dirs.src + "/third-party/echo.jquery.noconflict.js"
	];
	var _dontStripDocs = grunt.file.expandFiles(_dirs.src + "/third-party/*.js");
	var _config = {
		dirs: _dirs,
		pkg: "<json:js-sdk.json>",
		meta: {
			banner:
				"/**\n" +
				" * Copyright (c) 2006-<%= grunt.template.today(\"UTC:yyyy\") %> <%= pkg.author.name %> <<%= pkg.author.email %>>. All rights reserved.\n" +
				" * You may copy and modify this script as long as the above copyright notice,\n" +
				" * this condition and the following disclaimer is left intact.\n" +
				" * This software is provided by the author \"AS IS\" and no warranties are\n" +
				" * implied, including fitness for a particular purpose. In no event shall\n" +
				" * the author be liable for any damages arising in any way out of the use\n" +
				" * of this software, even if advised of the possibility of such damage.\n" +
				" *\n" +
				" * Version: <%= pkg.version %> (<%= grunt.template.today(\"UTC:yyyy-mm-dd HH:MM:ss Z\") %>)\n" +
				" */"
		},
		clean: {
			docs: ["<%= dirs.dest %>/docs"],
			all: ["<%= dirs.dest %>"]
		},
		exec: {
			docs: {
				command: "jsduck src --ignore-global --title=\"Echo JS SDK\" --pretty-json --output web/sdk/docs"
						//	jsduck src --ignore-global --title="Echo JS SDK" --pretty-json --output web/sdk/docs --meta-tags=tools/jsduck.tags.rb
			}
		},
		copy: {
			js: {
				files: {
					"<%= dirs.dest %>": ["<%= dirs.src %>/**/*.js"]
				},
				options: {
					basePath: _dirs.src,
					processContent: function(code) {
						return grunt.helper("strip_docs", grunt.helper("wrap_code", code));
					},
					processContentExclude: _dontWrap
				}
			},
			"images-css": {
				files: {
					"<%= dirs.dest %>": [
						"<%= dirs.src %>/**/*.css",
						"<%= dirs.src %>/**/*.png",
						"<%= dirs.src %>/**/*.jpg",
						"<%= dirs.src %>/**/*.gif"
					]
				},
				options: {
					basePath: _dirs.src
				}
			},
			tests: {
				files: {
					"<%= dirs.dest %>": ["<%= dirs.tests %>/**/*"]
				}
			}
		},
		concat: {
			loader: {
				src: [
					"<banner:meta.banner>",
					"<%= dirs.src %>/third-party/yepnope.1.5.4-min.js",
					"<%= dirs.src %>/loader.js"
				],
				dest: "<%= dirs.dest %>/loader.js"
			}
		},
		packs: {
			"api": [
				"<%= dirs.src %>/api.js",
				"<%= dirs.src %>/stream-server/api.js",
				"<%= dirs.src %>/identity-server/api.js"
			],
			"environment": [
				"<%= dirs.src %>/utils.js",
				"<%= dirs.src %>/events.js",
				"<%= dirs.src %>/labels.js",
				"<%= dirs.src %>/configuration.js",
				"<file_strip_banner:<%= dirs.dest %>/api.pack.js>",
				"<%= dirs.src %>/user-session.js",
				"<%= dirs.src %>/control.js",
				"<%= dirs.src %>/product.js",
				"<%= dirs.src %>/plugin.js",
				"<%= dirs.src %>/button.js",
				"<%= dirs.src %>/tabs.js"
			],
			"third-party/jquery": [
				"<%= dirs.src %>/third-party/jquery.js",
				"<%= dirs.src %>/third-party/echo.jquery.noconflict.js",
				"<%= dirs.src %>/third-party/jquery.ihint.js",
				"<%= dirs.src %>/third-party/jquery.viewport.mini.js",
				"<%= dirs.src %>/third-party/jquery.easing-1.3.min.js",
				"<%= dirs.src %>/third-party/jquery.fancybox-1.3.4.min.js",
				"<%= dirs.src %>/third-party/echo.fancybox.css.js",
				"<%= dirs.src %>/third-party/jquery.ui-1.8.21.min.js",
				"<%= dirs.src %>/third-party/jquery.isotope.min.js",
				"<%= dirs.src %>/third-party/bootstrap/bootstrap-tab.js"
			]
		},
		//min: {
		//	all: ["**/*.pack.js"]
		//},
		//qunit: {
		//	files: ["test/**/*.html"]
		//},
		lint: {
			// TODO: exclude third-party and qunit from linting
			// TODO: lint dest dir cause all files there are properly wrapped
			files: [
				"grunt.js",
				"<%= dirs.dest %>/**/*.js",
				"<%= dirs.tests %>/**/*.js"
			]
		},
		//watch: {
		//	files: "<config:lint.files>",
		//	tasks: "lint qunit"
		//},
		jshint: {
			options: {
				curly: true,
				eqeqeq: true,
				immed: true,
				latedef: true,
				newcap: true,
				noarg: true,
				sub: true,
				undef: true,
				boss: true,
				eqnull: true,
				browser: true
			},
			globals: {
				Echo: true,
				jQuery: true,
				QUnit: true
			}
		},
		uglify: {}
	};

	_.each(["stream-server", "identity-server", "app-server"], function(name) {
		_config.packs[name + "/controls"] = ["<%= dirs.src %>/" + name + "/controls/*.js"];
		_config.packs[name + "/plugins"] = ["<%= dirs.src %>/" + name + "/plugins/*.js"];
		_config.packs[name] = _config.packs[name + "/controls"].concat(_config.packs[name + "/plugins"]);
	});

	grunt.initConfig(_config);

	// ==========================================================================
	// TASKS
	// ==========================================================================

	grunt.loadNpmTasks("grunt-exec");
	grunt.loadNpmTasks("grunt-contrib");
	grunt.loadTasks("tasks");

	grunt.registerMultiTask("packs", "Assemble packages", function() {
		var name = this.target + ".pack.js";
		grunt.log.write("Assembling \"" + name + "\"...");
		var files = grunt.file.expandFiles(this.file.src);
		var code = grunt.helper("wrap_and_concat", files, this.target);
		code = grunt.template.process(grunt.config("meta.banner")) + "\n" + code;
		grunt.file.write(grunt.config("dirs.dest") + "/" + name, code);

		if (this.errorCount) {
			return false;
		}
		grunt.log.ok();
	});

	grunt.registerTask("docs", "clean:docs exec:docs");

	// Default task
	grunt.registerTask("default", "clean copy concat:loader packs");

	// ==========================================================================
	// HELPERS
	// ==========================================================================

	var _stripDocsRE = /[^\r\n]*\/\*[\s\S]*?\*\/\n/g;
	grunt.registerHelper("strip_docs", function(code, filepath) {
		if (_.include(_dontStripDocs, filepath)) {
			return code;
		}
		return code.replace(_stripDocsRE, function(match) {
			if (/\s+\* @\w/.test(match)) {
				return "";
			}
			return match;
		});
	});

	grunt.registerHelper("wrap_and_concat", function(files, name) {
		if (!files) {
			return "";
		}
		return files.map(function(filepath) {
			var code = grunt.task.directive(filepath, grunt.file.read);
			code = grunt.helper("wrap_code", code, filepath);
			code = grunt.helper("strip_docs", code, filepath);
			return code;
		}).join(grunt.utils.normalizelf(grunt.utils.linefeed));
	});

	grunt.registerHelper("wrap_code", function(code, filepath) {
		if (_.include(_dontWrap, filepath)) {
			return code;
		}
		if (!/third-party|\.pack\.js/.test(filepath)) {
			var doc = code.match(/@class ((?:\w+\.?)+)\s/);
			var docClass = doc && doc[1];
			if (docClass) {
				var parts = docClass.split(".Plugins.");
				// if it's plugin file
				if (parts[1]) {
					// we don't wrap plugin files at the moment because they can have 2 plugins in 1 file
					//code = "var plugin = Echo.Plugin.manifest(\"" + parts[1] + "\", \"" + parts[0] + "\");\n\n" +
					//	"if (Echo.Plugin.isDefined(plugin)) return;\n\n" + code;
				} else {
					code = "if (Echo.Utils.isComponentDefined(\"" + docClass + "\")) return;\n\n" + code;
				}
			} else if (filepath) {
				grunt.log.writeln("File " + filepath + " is missing '@class' definition");
			}
		}
		return "(function(jQuery) {\nvar $ = jQuery;\n\n" + code + "\n})(Echo.jQuery);\n";
	});
};
<|endoftext|>


<commit_before>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

'use strict';

const async = require('async');
const _ = require('lodash');
const request = require('request');

const debug = require('debug')('http');
const debugRequests = require('debug')('http:request');
const debugResponse = require('debug')('http:response');
const debugFullBody = require('debug')('http:full_body');
const USER_AGENT = 'Artillery (https://artillery.io)';
const engineUtil = require('./engine_util');
const template = engineUtil.template;
const http = require('http');
const https = require('https');
const fs = require('fs');
const filtrex = require('filtrex');

module.exports = HttpEngine;

function HttpEngine(script) {
  this.config = script.config;

  if (script.config.http && script.config.http.pool) {
    this.pool = {
      maxSockets: Number(script.config.http.pool)
    };
  }
}

HttpEngine.prototype.createScenario = function(scenarioSpec, ee) {
  var self = this;

  // Helper function to wrap an object's property in a list if it's
  // defined, or set it to an empty list if not.
  function ensurePropertyIsAList(obj, prop) {
    obj[prop] = [].concat(
      typeof obj[prop] === 'undefined' ?
        [] : obj[prop]);
    return obj;
  }

  ensurePropertyIsAList(scenarioSpec, 'beforeRequest');
  ensurePropertyIsAList(scenarioSpec, 'afterResponse');
  ensurePropertyIsAList(scenarioSpec, 'beforeScenario');
  ensurePropertyIsAList(scenarioSpec, 'afterScenario');
  ensurePropertyIsAList(scenarioSpec, 'onError');

  // Add scenario-level hooks if needed:
  // For now, just turn them into function steps and insert them
  // directly into the flow array.
  // TODO: Scenario-level hooks will probably want access to the
  // entire scenario spec rather than just the userContext.
  const beforeScenarioFns = _.map(
    scenarioSpec.beforeScenario,
    function(hookFunctionName) {
      return {'function': hookFunctionName};
    });
  const afterScenarioFns = _.map(
    scenarioSpec.afterScenario,
    function(hookFunctionName) {
      return {'function': hookFunctionName};
    });

  const newFlow = beforeScenarioFns.concat(
    scenarioSpec.flow.concat(afterScenarioFns));

  scenarioSpec.flow = newFlow;

  let tasks = _.map(scenarioSpec.flow, function(rs) {
    return self.step(rs, ee, {
      beforeRequest: scenarioSpec.beforeRequest,
      afterResponse: scenarioSpec.afterResponse,
      onError: scenarioSpec.onError
    });
  });

  return self.compile(tasks, scenarioSpec.flow, ee);
};

HttpEngine.prototype.step = function step(requestSpec, ee, opts) {

  opts = opts || {};
  let self = this;
  let config = this.config;

  if (requestSpec.loop) {
    let steps = _.map(requestSpec.loop, function(rs) {
      return self.step(rs, ee, opts);
    });

    return engineUtil.createLoopWithCount(
      requestSpec.count || -1,
      steps,
      {
        loopValue: requestSpec.loopValue || '$loopCount',
        overValues: requestSpec.over,
        whileTrue: self.config.processor ?
          self.config.processor[requestSpec.whileTrue] : undefined
      });
  }

  if (requestSpec.parallel) {
    let steps = _.map(requestSpec.parallel, function(rs) {
        return self.step(rs, ee, opts);
    });

    return engineUtil.createParallel(
        steps,
        {
          limitValue: requestSpec.limit
        }
      );
  }

  if (requestSpec.think) {
    return engineUtil.createThink(requestSpec, _.get(self.config, 'defaults.think', {}));
  }

  if (requestSpec.log) {
    return function(context, callback) {
      console.log(template(requestSpec.log, context));
      return process.nextTick(function() { callback(null, context); });
    };
  }

  if (requestSpec.function) {
    return function(context, callback) {
      let processFunc = self.config.processor[requestSpec.function];
      if (processFunc) {
        return processFunc(context, ee, function() {
          return callback(null, context);
        });
      } else {
        return process.nextTick(function () { callback(null, context); });
      }
    };
  }

  let f = function(context, callback) {
    let method = _.keys(requestSpec)[0].toUpperCase();
    let params = requestSpec[method.toLowerCase()];

    const onErrorHandlers = opts.onError; // only scenario-lever onError handlers are supported

    // A special case for when "url" attribute is missing. We need to check for
    // it manually as request.js won't emit an 'error' event when the argument
    // is missing.
    // This will be obsoleted by better script validation.
    if (!params.url && !params.uri) {
      let err = new Error('an URL must be specified');
      ee.emit('error', err.message);
      return callback(err, context);
    }

    let tls = config.tls || {};
    let timeout = config.timeout || _.get(config, 'http.timeout') || 120;

    if (!engineUtil.isProbableEnough(params)) {
      return process.nextTick(function() {
        callback(null, context);
      });
    }

    if (!_.isUndefined(params.ifTrue)) {
      let cond;
      let result;
      try {
        cond = filtrex(params.ifTrue);
        result = cond(context.vars);
      } catch (e) {
        result = 1; // if the expression is incorrect, just proceed // TODO: debug message
      }
      if (typeof result === 'undefined' || result === 0) {
        return process.nextTick(function () {
          callback(null, context);
        });
      }
    }

    // Run beforeRequest processors (scenario-level ones too)
    let requestParams = _.cloneDeep(params);
    requestParams = _.extend(requestParams, {
      url: maybePrependBase(params.url || params.uri, config), // *NOT* templating here
      method: method,
      headers: {
      },
      timeout: timeout * 1000,
      jar: context._jar
    });
    requestParams = _.extend(requestParams, tls);

    let functionNames = _.concat(opts.beforeRequest || [], params.beforeRequest || []);

    async.eachSeries(
      functionNames,
      function iteratee(functionName, next) {

        let processFunc = config.processor[functionName];
        processFunc(requestParams, context, ee, function(err) {
          if (err) {
            return next(err);
          }
          return next(null);
        });
      },
      function done(err) {
        if (err) {
          debug(err);
          let errCode = err.code || err.message;
          // FIXME: Should not need to have to emit manually here
          ee.emit('error', errCode);
          return callback(err, context);
        }

        // Assemble the new request params
        if (params.json) {
          requestParams.json = template(params.json, context);
        }

        if (params.body) {
          requestParams.body = template(params.body, context);
        }

        if (params.form) {
          requestParams.form = _.reduce(
            requestParams.form,
            function (acc, v, k) {
              acc[k] = template(v, context);
              return acc;
            },
            {});
        }

        // Assign default headers then overwrite as needed
        let defaultHeaders = lowcaseKeys(
          (config.defaults && config.defaults.headers) ?
            config.defaults.headers : {'user-agent': USER_AGENT});
        const combinedHeaders = _.extend(defaultHeaders, lowcaseKeys(params.headers), lowcaseKeys(requestParams.headers));
        const templatedHeaders = _.mapValues(combinedHeaders, function(v, k, obj) {
          return template(v, context);
        });
        requestParams.headers = templatedHeaders;

        let defaultCookie = config.defaults ? config.defaults.cookie || {} : {};

        let cookie = _.reduce(
          params.cookie,
          function(acc, v, k) {
            acc[k] = v;
            return acc;
          },
          defaultCookie);

        if (cookie) {
          _.each(cookie, function(v, k) {
            context._jar.setCookie(k + '=' + template(v, context), requestParams.url);
          });
        }

        if (typeof requestParams.auth === 'object') {
          requestParams.auth.user = template(requestParams.auth.user, context);
          requestParams.auth.pass = template(requestParams.auth.pass, context);
        }

        let url = maybePrependBase(template(requestParams.uri || requestParams.url, context), config);

        if (requestParams.uri) {
          // If a hook function sets requestParams.uri to something, request.js
          // will pick that over .url, so we need to delete it.
          delete requestParams.uri;
        }

        requestParams.url = url;

        if (!self.pool) {
          if ((/^https/i).test(requestParams.url)) {
            requestParams.agent = context._httpsAgent;
          } else {
            requestParams.agent = context._httpAgent;
          }
        } else {
          requestParams.pool = self.pool;
        }

        function requestCallback(err, res, body) {
          if (err) {
            return;
          }

          if (process.env.DEBUG) {
            let requestInfo = {
              url: requestParams.url,
              method: requestParams.method,
              headers: requestParams.headers
            };
            if (requestParams.json && typeof requestParams.json !== 'boolean') {
              requestInfo.json = requestParams.json;
            }

            // If "json" is set to an object, it will be serialised and sent as body and the value of the "body" attribute will be ignored.
            if (requestParams.body && typeof requestParams.json !== 'object') {
              if (process.env.DEBUG.indexOf('http:full_body') > -1) {
                // Show the entire body
                requestInfo.body = requestParams.body;
              } else {
                // Only show the beginning of long bodies
                requestInfo.body = requestParams.body.substring(0, 512);
                if (requestParams.body.length > 512) {
                  requestInfo.body += ' ...';
                }
              }
            }
            debug('request: %s', JSON.stringify(requestInfo, null, 2));
          }

          debugResponse(JSON.stringify(res.headers, null, 2));
          debugResponse(JSON.stringify(body, null, 2));

          engineUtil.captureOrMatch(
            params,
            res,
            context,
            function captured(err, result) {
              if (err) {
                // Run onError hooks and end the scenario:
                runOnErrorHooks(onErrorHandlers, config.processor, err, requestParams, context, ee, function(asyncErr) {
                  ee.emit('error', err.message);
                  return callback(err, context);
                });
              }

              debug('captures and matches:');
              debug(result.matches);
              debug(result.captures);

              // match and capture are strict by default:
              let haveFailedMatches = _.some(result.matches, function(v, k) {
                return !v.success && v.strict !== false;
              });

              let haveFailedCaptures = _.some(result.captures, function(v, k) {
                return v === '';
              });

              if (haveFailedMatches || haveFailedCaptures) {
                // TODO: Emit the details of each failed capture/match
              } else {
                _.each(result.matches, function(v, k) {
                  ee.emit('match', v.success, {
                    expected: v.expected,
                    got: v.got,
                    expression: v.expression,
                    strict: v.strict
                  });
                });

                _.each(result.captures, function(v, k) {
                  context.vars[k] = v;
                });
              }

              // Now run afterResponse processors
              let functionNames = _.concat(opts.afterResponse || [], params.afterResponse || []);
              async.eachSeries(
                functionNames,
                function iteratee(functionName, next) {
                  let processFunc = config.processor[functionName];
                  processFunc(requestParams, res, context, ee, function(err) {
                    if (err) {
                      return next(err);
                    }
                    return next(null);
                  });
                }, function(err) {
                  if (err) {
                    debug(err);
                    ee.emit('error', err.code || err.message);
                    return callback(err, context);
                  }

                  if (haveFailedMatches || haveFailedCaptures) {
                    // FIXME: This means only one error in the report even if multiple captures failed for the same request.
                    return callback(new Error('Failed capture or match'), context);
                  }

                  return callback(null, context);
                });
            });
        }

        // If we aren't processing the full response, we don't need the
        // callback:
        let maybeCallback;
        if (typeof requestParams.capture === 'object' ||
            typeof requestParams.match === 'object' ||
            requestParams.afterResponse ||
            opts.afterResponse ||
            process.env.DEBUG) {
          maybeCallback = requestCallback;
        }

        if(!requestParams.url) {
          let err = new Error('an URL must be specified');

          // Run onError hooks and end the scenario
          runOnErrorHooks(onErrorHandlers, config.processor, err, requestParams, context, ee, function(asyncErr) {
            ee.emit('error', err.message);
            return callback(err, context);
          });
        }

        request(requestParams, maybeCallback)
          .on('request', function(req) {
            debugRequests("request start: %s", req.path);
            ee.emit('request');

            const startedAt = process.hrtime();

            req.on('response', function updateLatency(res) {
              let code = res.statusCode;
              const endedAt = process.hrtime(startedAt);
              let delta = (endedAt[0] * 1e9) + endedAt[1];
              debugRequests("request end: %s", req.path);
              ee.emit('response', delta, code, context._uid);
            });
          }).on('end', function() {
            context._successCount++;

            if (!maybeCallback) {
              callback(null, context);
            } // otherwise called from requestCallback
          }).on('error', function(err) {
            debug(err);

            // Run onError hooks and end the scenario
            runOnErrorHooks(onErrorHandlers, config.processor, err, requestParams, context, ee, function(asyncErr) {
              let errCode = err.code || err.message;
              ee.emit('error', errCode);
              return callback(err, context);
            });
          });
      }); // eachSeries
  };

  return f;
};

HttpEngine.prototype.compile = function compile(tasks, scenarioSpec, ee) {
  let self = this;
  let config = this.config;
  let tls = config.tls || {};

  return function scenario(initialContext, callback) {
    initialContext._successCount = 0;

    initialContext._jar = request.jar();
    let keepAliveMsec = 1000;
    let maxSockets = 1;
    if (self.config.http && self.config.http.maxSockets) {
      maxSockets = self.config.http.maxSockets;
    }
    if (!self.pool) {
      let agentOpts = {
        keepAlive: true,
        keepAliveMsecs: keepAliveMsec,
        maxSockets: maxSockets,
        maxFreeSockets: maxSockets
      };

      initialContext._httpAgent = new http.Agent(agentOpts);
      initialContext._httpsAgent = new https.Agent(agentOpts);
    }

    let steps = _.flatten([
      function zero(cb) {
        ee.emit('started');
        return cb(null, initialContext);
      },
      tasks
    ]);

    async.waterfall(
      steps,
      function scenarioWaterfallCb(err, context) {
        // If the connection was refused we might not have a context
        if (context && context._httpAgent) {
          context._httpAgent.destroy();
        }
        if (context && context._httpsAgent) {
          context._httpsAgent.destroy();
        }

        if (err) {
          //ee.emit('error', err.message);
          return callback(err, context);
        } else {
          return callback(null, context);
        }
      });
  };
};

function maybePrependBase(uri, config) {
  if (_.startsWith(uri, '/')) {
    return config.target + uri;
  } else {
    return uri;
  }
}

/*
 * Given a dictionary, return a dictionary with all keys lowercased.
 */
function lowcaseKeys(h) {
  return _.transform(h, function(result, v, k) {
    result[k.toLowerCase()] = v;
  });
}

function runOnErrorHooks(functionNames, functions, err, requestParams, context, ee, callback) {
  async.eachSeries(functionNames, function iteratee(functionName, next) {
    let processFunc = functions[functionName];
    processFunc(err, requestParams, context, ee, function(asyncErr) {
      if (asyncErr) {
        return next(asyncErr);
      }
      return next(null);
    });
  }, function done(asyncErr) {
    return callback(asyncErr);
  });
}
<commit_msg>fix(engine_http): Set request body correctly<commit_after>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

'use strict';

const async = require('async');
const _ = require('lodash');
const request = require('request');

const debug = require('debug')('http');
const debugRequests = require('debug')('http:request');
const debugResponse = require('debug')('http:response');
const debugFullBody = require('debug')('http:full_body');
const USER_AGENT = 'Artillery (https://artillery.io)';
const engineUtil = require('./engine_util');
const template = engineUtil.template;
const http = require('http');
const https = require('https');
const fs = require('fs');
const filtrex = require('filtrex');

module.exports = HttpEngine;

function HttpEngine(script) {
  this.config = script.config;

  if (script.config.http && script.config.http.pool) {
    this.pool = {
      maxSockets: Number(script.config.http.pool)
    };
  }
}

HttpEngine.prototype.createScenario = function(scenarioSpec, ee) {
  var self = this;

  // Helper function to wrap an object's property in a list if it's
  // defined, or set it to an empty list if not.
  function ensurePropertyIsAList(obj, prop) {
    obj[prop] = [].concat(
      typeof obj[prop] === 'undefined' ?
        [] : obj[prop]);
    return obj;
  }

  ensurePropertyIsAList(scenarioSpec, 'beforeRequest');
  ensurePropertyIsAList(scenarioSpec, 'afterResponse');
  ensurePropertyIsAList(scenarioSpec, 'beforeScenario');
  ensurePropertyIsAList(scenarioSpec, 'afterScenario');
  ensurePropertyIsAList(scenarioSpec, 'onError');

  // Add scenario-level hooks if needed:
  // For now, just turn them into function steps and insert them
  // directly into the flow array.
  // TODO: Scenario-level hooks will probably want access to the
  // entire scenario spec rather than just the userContext.
  const beforeScenarioFns = _.map(
    scenarioSpec.beforeScenario,
    function(hookFunctionName) {
      return {'function': hookFunctionName};
    });
  const afterScenarioFns = _.map(
    scenarioSpec.afterScenario,
    function(hookFunctionName) {
      return {'function': hookFunctionName};
    });

  const newFlow = beforeScenarioFns.concat(
    scenarioSpec.flow.concat(afterScenarioFns));

  scenarioSpec.flow = newFlow;

  let tasks = _.map(scenarioSpec.flow, function(rs) {
    return self.step(rs, ee, {
      beforeRequest: scenarioSpec.beforeRequest,
      afterResponse: scenarioSpec.afterResponse,
      onError: scenarioSpec.onError
    });
  });

  return self.compile(tasks, scenarioSpec.flow, ee);
};

HttpEngine.prototype.step = function step(requestSpec, ee, opts) {

  opts = opts || {};
  let self = this;
  let config = this.config;

  if (requestSpec.loop) {
    let steps = _.map(requestSpec.loop, function(rs) {
      return self.step(rs, ee, opts);
    });

    return engineUtil.createLoopWithCount(
      requestSpec.count || -1,
      steps,
      {
        loopValue: requestSpec.loopValue || '$loopCount',
        overValues: requestSpec.over,
        whileTrue: self.config.processor ?
          self.config.processor[requestSpec.whileTrue] : undefined
      });
  }

  if (requestSpec.parallel) {
    let steps = _.map(requestSpec.parallel, function(rs) {
        return self.step(rs, ee, opts);
    });

    return engineUtil.createParallel(
        steps,
        {
          limitValue: requestSpec.limit
        }
      );
  }

  if (requestSpec.think) {
    return engineUtil.createThink(requestSpec, _.get(self.config, 'defaults.think', {}));
  }

  if (requestSpec.log) {
    return function(context, callback) {
      console.log(template(requestSpec.log, context));
      return process.nextTick(function() { callback(null, context); });
    };
  }

  if (requestSpec.function) {
    return function(context, callback) {
      let processFunc = self.config.processor[requestSpec.function];
      if (processFunc) {
        return processFunc(context, ee, function() {
          return callback(null, context);
        });
      } else {
        return process.nextTick(function () { callback(null, context); });
      }
    };
  }

  let f = function(context, callback) {
    let method = _.keys(requestSpec)[0].toUpperCase();
    let params = requestSpec[method.toLowerCase()];

    const onErrorHandlers = opts.onError; // only scenario-lever onError handlers are supported

    // A special case for when "url" attribute is missing. We need to check for
    // it manually as request.js won't emit an 'error' event when the argument
    // is missing.
    // This will be obsoleted by better script validation.
    if (!params.url && !params.uri) {
      let err = new Error('an URL must be specified');
      ee.emit('error', err.message);
      return callback(err, context);
    }

    let tls = config.tls || {};
    let timeout = config.timeout || _.get(config, 'http.timeout') || 120;

    if (!engineUtil.isProbableEnough(params)) {
      return process.nextTick(function() {
        callback(null, context);
      });
    }

    if (!_.isUndefined(params.ifTrue)) {
      let cond;
      let result;
      try {
        cond = filtrex(params.ifTrue);
        result = cond(context.vars);
      } catch (e) {
        result = 1; // if the expression is incorrect, just proceed // TODO: debug message
      }
      if (typeof result === 'undefined' || result === 0) {
        return process.nextTick(function () {
          callback(null, context);
        });
      }
    }

    // Run beforeRequest processors (scenario-level ones too)
    let requestParams = _.cloneDeep(params);
    requestParams = _.extend(requestParams, {
      url: maybePrependBase(params.url || params.uri, config), // *NOT* templating here
      method: method,
      headers: {
      },
      timeout: timeout * 1000,
      jar: context._jar
    });
    requestParams = _.extend(requestParams, tls);

    let functionNames = _.concat(opts.beforeRequest || [], params.beforeRequest || []);

    async.eachSeries(
      functionNames,
      function iteratee(functionName, next) {

        let processFunc = config.processor[functionName];
        processFunc(requestParams, context, ee, function(err) {
          if (err) {
            return next(err);
          }
          return next(null);
        });
      },
      function done(err) {
        if (err) {
          debug(err);
          let errCode = err.code || err.message;
          // FIXME: Should not need to have to emit manually here
          ee.emit('error', errCode);
          return callback(err, context);
        }

        // Order of precedence: json set in a function, json set in the script, body set in a function, body set in the script.
        if (requestParams.json) {
          requestParams.json = template(requestParams.json, context);
          delete requestParams.body;
        } else if (requestParams.body) {
          requestParams.body = template(requestParams.body, context);
          // TODO: Warn if body is not a string or a buffer
        }

        if (params.form) {
          requestParams.form = _.reduce(
            requestParams.form,
            function (acc, v, k) {
              acc[k] = template(v, context);
              return acc;
            },
            {});
        }

        // Assign default headers then overwrite as needed
        let defaultHeaders = lowcaseKeys(
          (config.defaults && config.defaults.headers) ?
            config.defaults.headers : {'user-agent': USER_AGENT});
        const combinedHeaders = _.extend(defaultHeaders, lowcaseKeys(params.headers), lowcaseKeys(requestParams.headers));
        const templatedHeaders = _.mapValues(combinedHeaders, function(v, k, obj) {
          return template(v, context);
        });
        requestParams.headers = templatedHeaders;

        let defaultCookie = config.defaults ? config.defaults.cookie || {} : {};

        let cookie = _.reduce(
          params.cookie,
          function(acc, v, k) {
            acc[k] = v;
            return acc;
          },
          defaultCookie);

        if (cookie) {
          _.each(cookie, function(v, k) {
            context._jar.setCookie(k + '=' + template(v, context), requestParams.url);
          });
        }

        if (typeof requestParams.auth === 'object') {
          requestParams.auth.user = template(requestParams.auth.user, context);
          requestParams.auth.pass = template(requestParams.auth.pass, context);
        }

        let url = maybePrependBase(template(requestParams.uri || requestParams.url, context), config);

        if (requestParams.uri) {
          // If a hook function sets requestParams.uri to something, request.js
          // will pick that over .url, so we need to delete it.
          delete requestParams.uri;
        }

        requestParams.url = url;

        if (!self.pool) {
          if ((/^https/i).test(requestParams.url)) {
            requestParams.agent = context._httpsAgent;
          } else {
            requestParams.agent = context._httpAgent;
          }
        } else {
          requestParams.pool = self.pool;
        }

        function requestCallback(err, res, body) {
          if (err) {
            return;
          }

          if (process.env.DEBUG) {
            let requestInfo = {
              url: requestParams.url,
              method: requestParams.method,
              headers: requestParams.headers
            };
            if (requestParams.json && typeof requestParams.json !== 'boolean') {
              requestInfo.json = requestParams.json;
            }

            // If "json" is set to an object, it will be serialised and sent as body and the value of the "body" attribute will be ignored.
            if (requestParams.body && typeof requestParams.json !== 'object') {
              if (process.env.DEBUG.indexOf('http:full_body') > -1) {
                // Show the entire body
                requestInfo.body = requestParams.body;
              } else {
                // Only show the beginning of long bodies
                requestInfo.body = requestParams.body.substring(0, 512);
                if (requestParams.body.length > 512) {
                  requestInfo.body += ' ...';
                }
              }
            }
            debug('request: %s', JSON.stringify(requestInfo, null, 2));
          }

          debugResponse(JSON.stringify(res.headers, null, 2));
          debugResponse(JSON.stringify(body, null, 2));

          engineUtil.captureOrMatch(
            params,
            res,
            context,
            function captured(err, result) {
              if (err) {
                // Run onError hooks and end the scenario:
                runOnErrorHooks(onErrorHandlers, config.processor, err, requestParams, context, ee, function(asyncErr) {
                  ee.emit('error', err.message);
                  return callback(err, context);
                });
              }

              debug('captures and matches:');
              debug(result.matches);
              debug(result.captures);

              // match and capture are strict by default:
              let haveFailedMatches = _.some(result.matches, function(v, k) {
                return !v.success && v.strict !== false;
              });

              let haveFailedCaptures = _.some(result.captures, function(v, k) {
                return v === '';
              });

              if (haveFailedMatches || haveFailedCaptures) {
                // TODO: Emit the details of each failed capture/match
              } else {
                _.each(result.matches, function(v, k) {
                  ee.emit('match', v.success, {
                    expected: v.expected,
                    got: v.got,
                    expression: v.expression,
                    strict: v.strict
                  });
                });

                _.each(result.captures, function(v, k) {
                  context.vars[k] = v;
                });
              }

              // Now run afterResponse processors
              let functionNames = _.concat(opts.afterResponse || [], params.afterResponse || []);
              async.eachSeries(
                functionNames,
                function iteratee(functionName, next) {
                  let processFunc = config.processor[functionName];
                  processFunc(requestParams, res, context, ee, function(err) {
                    if (err) {
                      return next(err);
                    }
                    return next(null);
                  });
                }, function(err) {
                  if (err) {
                    debug(err);
                    ee.emit('error', err.code || err.message);
                    return callback(err, context);
                  }

                  if (haveFailedMatches || haveFailedCaptures) {
                    // FIXME: This means only one error in the report even if multiple captures failed for the same request.
                    return callback(new Error('Failed capture or match'), context);
                  }

                  return callback(null, context);
                });
            });
        }

        // If we aren't processing the full response, we don't need the
        // callback:
        let maybeCallback;
        if (typeof requestParams.capture === 'object' ||
            typeof requestParams.match === 'object' ||
            requestParams.afterResponse ||
            opts.afterResponse ||
            process.env.DEBUG) {
          maybeCallback = requestCallback;
        }

        if(!requestParams.url) {
          let err = new Error('an URL must be specified');

          // Run onError hooks and end the scenario
          runOnErrorHooks(onErrorHandlers, config.processor, err, requestParams, context, ee, function(asyncErr) {
            ee.emit('error', err.message);
            return callback(err, context);
          });
        }

        request(requestParams, maybeCallback)
          .on('request', function(req) {
            debugRequests("request start: %s", req.path);
            ee.emit('request');

            const startedAt = process.hrtime();

            req.on('response', function updateLatency(res) {
              let code = res.statusCode;
              const endedAt = process.hrtime(startedAt);
              let delta = (endedAt[0] * 1e9) + endedAt[1];
              debugRequests("request end: %s", req.path);
              ee.emit('response', delta, code, context._uid);
            });
          }).on('end', function() {
            context._successCount++;

            if (!maybeCallback) {
              callback(null, context);
            } // otherwise called from requestCallback
          }).on('error', function(err) {
            debug(err);

            // Run onError hooks and end the scenario
            runOnErrorHooks(onErrorHandlers, config.processor, err, requestParams, context, ee, function(asyncErr) {
              let errCode = err.code || err.message;
              ee.emit('error', errCode);
              return callback(err, context);
            });
          });
      }); // eachSeries
  };

  return f;
};

HttpEngine.prototype.compile = function compile(tasks, scenarioSpec, ee) {
  let self = this;
  let config = this.config;
  let tls = config.tls || {};

  return function scenario(initialContext, callback) {
    initialContext._successCount = 0;

    initialContext._jar = request.jar();
    let keepAliveMsec = 1000;
    let maxSockets = 1;
    if (self.config.http && self.config.http.maxSockets) {
      maxSockets = self.config.http.maxSockets;
    }
    if (!self.pool) {
      let agentOpts = {
        keepAlive: true,
        keepAliveMsecs: keepAliveMsec,
        maxSockets: maxSockets,
        maxFreeSockets: maxSockets
      };

      initialContext._httpAgent = new http.Agent(agentOpts);
      initialContext._httpsAgent = new https.Agent(agentOpts);
    }

    let steps = _.flatten([
      function zero(cb) {
        ee.emit('started');
        return cb(null, initialContext);
      },
      tasks
    ]);

    async.waterfall(
      steps,
      function scenarioWaterfallCb(err, context) {
        // If the connection was refused we might not have a context
        if (context && context._httpAgent) {
          context._httpAgent.destroy();
        }
        if (context && context._httpsAgent) {
          context._httpsAgent.destroy();
        }

        if (err) {
          //ee.emit('error', err.message);
          return callback(err, context);
        } else {
          return callback(null, context);
        }
      });
  };
};

function maybePrependBase(uri, config) {
  if (_.startsWith(uri, '/')) {
    return config.target + uri;
  } else {
    return uri;
  }
}

/*
 * Given a dictionary, return a dictionary with all keys lowercased.
 */
function lowcaseKeys(h) {
  return _.transform(h, function(result, v, k) {
    result[k.toLowerCase()] = v;
  });
}

function runOnErrorHooks(functionNames, functions, err, requestParams, context, ee, callback) {
  async.eachSeries(functionNames, function iteratee(functionName, next) {
    let processFunc = functions[functionName];
    processFunc(err, requestParams, context, ee, function(asyncErr) {
      if (asyncErr) {
        return next(asyncErr);
      }
      return next(null);
    });
  }, function done(asyncErr) {
    return callback(asyncErr);
  });
}
<|endoftext|>


<commit_before>'use strict';

let spawn = require( 'child_process' ).spawnSync

let python_usr_bin = spawn( 'ls', [ '-lh', '/usr/bin/python' ] )
let python_usr_bin_version = spawn( '/usr/bin/python', [ '--version'] )
let python_usr_local_bin = spawn( 'ls', [ '-lh', '/usr/local/bin/python' ] )
let pip_usr_local_bin = spawn( 'ls', [ '-lh', '/usr/local/bin/pip' ] )
let pip_usr_local_bin_version = spawn( '/usr/local/bin/pip', [ '--version' ] )
let python_usr_bin_site = spawn( '/usr/bin/python', [ '-m', 'site' ] )

let info

function prt(cmd) {
  if (cmd.stderr.length != 0)
    console.log(`stderr: ${cmd.stderr.toString()}`)
  console.log(`${cmd.stdout.toString()}`)
}

console.log('--------')

// console.log( `stderr: ${ls.stderr.toString()}` );
// console.log( `stdout: ${ls.stdout.toString()}` );

info = 'System Mac python'
if (python_usr_bin.stderr.length == 0) {
  console.log(`${info} exists OK: ${python_usr_bin.stdout.toString()}`)
  console.log(`${info} version: ${python_usr_bin_version.stderr.toString()}`)  // bug in python 2.7 puts this info in stderr instead of stdout
}
else
  console.log(`${info} missing?: ${python_usr_bin.stderr.toString()}`)

info = 'Python Org or Brew python'
if (python_usr_local_bin.stderr.length == 0)
  console.log(`${info} exists OK: ${python_usr_local_bin.stdout.toString()}`)
else
  console.log(`${info} not installed in /usr/local/bin\n`)

info = 'pip'
let pip_ver
let pip_site
let pip_version_str

// Need to take into account different pip version reporting strings.  The egg bit at the end is not
// necessarily going to be there.
// pip 9.0.1 from /Users/Andy/miniconda/lib/python2.7/site-packages (python 2.7)
// pip 7.1.0 from /Library/Python/2.7/site-packages/pip-7.1.0-py2.7.egg (python 2.7)
// const rexp1 = /^pip (.*) from (.*)\/pip-.*egg/
const rexp1 = /pip (.*) from (.*)\/site-packages/

if (pip_usr_local_bin.stderr.length == 0) {
  console.log(`${info} installed OK: ${pip_usr_local_bin.stdout.toString()}`)
  pip_version_str = pip_usr_local_bin_version.stdout.toString()
  let match = rexp1.exec(pip_version_str)
  if (match != null) {
    pip_ver = match[1]
    pip_site = match[2] + '/site-packages'
    console.log(`${info} is version ${pip_ver} associated with site-packages dir ${pip_site}`)
  }
  else
    console.log(`${info} is installed but cannot figure out anything about it - the pip command is probably empty or broken: "${pip_version_str}" - should retry with the python -m pip technique`)
}
else
  console.log(`${info} not installed in /usr/local/bin`)

// parse the site info
// prt(python_usr_bin_site)
let line = ''
let scan = false
let sys_path_str = 'var sys_path = '
let sys_path
let chunk = ''
let stdout = python_usr_bin_site.stdout.toString()
let lines = stdout.split("\n")

// alternatively, pure ECMA6, use the new 'of' syntax
for (let line of lines) {
  if (line == 'sys.path = [') {
    scan = true
    continue
  }
  if (scan)
    chunk = chunk + line
  if (line == ']')
    scan = false
}
chunk = 'sys_path = [' + chunk
eval(chunk)

// prt(ls_python_usr_bin)
// prt(ls_python_usr_local_bin)
// prt(pip_usr_local_bin)

let pip_in_site = sys_path.indexOf(pip_site) >= 0
// console.log('sys_path', sys_path)
console.log('Is /usr/local/bin/pip associated with system python?', pip_in_site)

console.log('DONE ')
<commit_msg>pip class created - encapsulates pip analysis info<commit_after>'use strict';

let spawn = require( 'child_process' ).spawnSync

let python_usr_bin = spawn( 'ls', [ '-lh', '/usr/bin/python' ] )
let python_usr_bin_version = spawn( '/usr/bin/python', [ '--version'] )
let python_usr_local_bin = spawn( 'ls', [ '-lh', '/usr/local/bin/python' ] )
// let pip_usr_local_bin = spawn( 'ls', [ '-lh', '/usr/local/bin/pip' ] )
// let pip_usr_local_bin_version = spawn( '/usr/local/bin/pip', [ '--version' ] )
let python_usr_bin_site = spawn( '/usr/bin/python', [ '-m', 'site' ] )

let info

function prt(cmd) {
  if (cmd.stderr.length != 0)
    console.log(`stderr: ${cmd.stderr.toString()}`)
  console.log(`${cmd.stdout.toString()}`)
}

console.log('--------')

// console.log( `stderr: ${ls.stderr.toString()}` );
// console.log( `stdout: ${ls.stdout.toString()}` );

info = 'System Mac python'
if (python_usr_bin.stderr.length == 0) {
  console.log(`${info} exists OK: ${python_usr_bin.stdout.toString()}`)
  console.log(`${info} version: ${python_usr_bin_version.stderr.toString()}`)  // bug in python 2.7 puts this info in stderr instead of stdout
}
else
  console.log(`${info} missing?: ${python_usr_bin.stderr.toString()}`)

info = 'Python Org or Brew python'
if (python_usr_local_bin.stderr.length == 0)
  console.log(`${info} exists OK: ${python_usr_local_bin.stdout.toString()}`)
else
  console.log(`${info} not installed in /usr/local/bin\n`)

class Pip {

  constructor(path) {
    this.path = path
    this.result_shell_ls
    this.result_shell_pip_version
    this.version
    this.site_package_path
    this.warnings = []

    this.analyse()
  }

  get exists() {
    return this.result_shell_ls.stderr.length == 0
  }
  get runs_ok() {
    return this.result_shell_pip_version.stderr.length == 0
  }

  analyse() {
    this.result_shell_ls = spawn( 'ls', [ '-lh', this.path ] )
    this.result_shell_pip_version = spawn( this.path, [ '--version' ] )

    if (this.exists && this.runs_ok) {
      this.analyse_version()
      if (this.version == undefined) this.warnings.push(`${this.path} exists but version could not be determined`)
    }

    if (! this.exists) this.warnings.push("executable doesn't exist")
    if (! this.runs_ok) this.warnings.push("doesn't run properly")
  }

  analyse_version() {
    const regex = /pip (.*) from (.*)\/site-packages/

    let match = regex.exec(this.result_shell_pip_version.stdout.toString())
    if (match != null) {
      this.version = match[1]
      this.site_package_path = match[2] + '/site-packages'
    }
  }

  report() {
    console.log(`${this.path} exists: ${this.exists} runs ok: ${this.runs_ok} version: ${this.version} site_package_path: ${this.site_package_path}`)
    if (this.warnings.length > 0) {
      console.log(`Warnings: ${this.warnings}`)
      // if verbose
      prt(this.result_shell_ls)
      prt(this.result_shell_pip_version)
    }
  }
}

let pip_usr_local_bin = new Pip('/usr/local/bin/pip')
pip_usr_local_bin.report()

// parse the site info
// prt(python_usr_bin_site)
let line = ''
let scan = false
let sys_path_str = 'var sys_path = '
let sys_path
let chunk = ''
let stdout = python_usr_bin_site.stdout.toString()
let lines = stdout.split("\n")

// alternatively, pure ECMA6, use the new 'of' syntax
for (let line of lines) {
  if (line == 'sys.path = [') {
    scan = true
    continue
  }
  if (scan)
    chunk = chunk + line
  if (line == ']')
    scan = false
}
chunk = 'sys_path = [' + chunk
eval(chunk)

// prt(ls_python_usr_bin)
// prt(ls_python_usr_local_bin)
// prt(pip_usr_local_bin)

let pip_in_site = sys_path.indexOf(pip_usr_local_bin.site_package_path) >= 0
// console.log('sys_path', sys_path)
console.log(`${pip_usr_local_bin.path} associated with system python? ${pip_in_site}`)

console.log('DONE ')
<|endoftext|>


<commit_before>/*
 * Copyright 2012 Amadeus s.a.s.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * Aria Templates opensource production build file.

 * This build depends on the bootstrap build to execute properly.

 * This build does the following things:
 *

 *
creates packages from multiple input files
,
 *
compiles templates to JavaScript classes
,
 *
minifies JavaScript
,
 *
adds a header in each package with license and current release version
,
 *
appends MD5 checksum to each file name as a way to make files cacheable for longer periods
.
 *

 */

module.exports = function (grunt) {
    var atExtensions = ['**/*.js', '**/*.tpl', '**/*.tpl.css', '**/*.tpl.txt', '**/*.tml', '**/*.cml'];
    var notAtExtensions = atExtensions.map(function (value) {
        return '!' + value;
    });
    var mainATFile = 'aria/<%= pkg.name %>-<%= pkg.version %>.js';

    grunt.config.set('atpackager.prod', {
        options : {
            ATBootstrapFile : mainATFile,
            sourceDirectories : ['<%= packaging.bootstrap.outputdir %>'],
            sourceFiles : ['**/*'],
            defaultBuilder : {
                type : 'ATMultipart',
                cfg : {
                    header : '<%= packaging.license %>'
                }
            },
            outputDirectory : '<%= packaging.prod.outputdir %>',
            visitors : [{
                        type : 'ATDependencies',
                        cfg : {
                            externalDependencies : '<%= packaging.bootstrap.files %>',
                            files : atExtensions.concat(['!' + mainATFile])
                        }
                    }, {
                        type : 'CheckDependencies',
                        cfg : {
                            checkPackagesOrder : false
                        }
                    }, 'ATCompileTemplates', 'ATRemoveDoc', {
                        type : 'JSMinify',
                        cfg : {
                            files : atExtensions
                        }
                    }, {
                        type : 'Hash',
                        cfg : {
                            files : atExtensions.concat(['aria/core/transport/iframeSource*',
                                    'aria/utils/FrameATLoaderHTML*', '**/*.jnlp', '!aria/css/**', '!' + mainATFile,
                                    '<%= packaging.prod.hash_include_files %>'])
                        }
                    }, {
                        type : 'ATUrlMap',
                        cfg : {
                            mapFile : mainATFile,
                            sourceFiles : ['**/*', '!aria/css/**/*'],
                            starStarCompress : ['**/*', '!aria/resources']
                        }
                    }, {
                        type : 'CopyUnpackaged',
                        cfg : {
                            files : [mainATFile, 'aria/css/*.js', '<%= packaging.prod.allow_unpackaged_files %>'],
                            builder : {
                                type : 'ATMultipart',
                                cfg : {
                                    header : '<%= packaging.license %>'
                                }
                            }
                        }
                    }, {
                        type : 'CopyUnpackaged',
                        cfg : {
                            files : ['**/*.jnlp', "aria/core/transport/iframeSource.txt",
                                    "aria/utils/FrameATLoaderHTML.html"],
                            builder : 'ATMultipart'
                        }
                    }, {
                        type : 'CopyUnpackaged',
                        cfg : {
                            files : ['**/*'].concat(notAtExtensions)
                        }
                    }, 'CheckPackaged'],
            packages : ['<%= packaging.prod.files %>', '<%= packaging.prod.expanded_localization_files %>']
        }
    });

    grunt.config.set('gzipStats.prod', {
        src : '<%= packaging.prod.outputdir %>'
    });

    grunt.config.set('removedirs.prod', {
        folders : ['<%= packaging.prod.outputdir %>']
    });

    grunt.registerTask('expandLocalizationFiles', function () {
        var res = [];
        var savedLocale = grunt.config.getRaw('locale');
        var locales = grunt.config.get('packaging.locales');
        locales.forEach(function (curLocale) {
            grunt.config.set('locale', "_" + curLocale);
            res.push(grunt.config.get('packaging.prod.localization_files'));
        });
        grunt.config.set('locale', "");
        res.push(grunt.config.get('packaging.prod.localization_files'));
        grunt.config.set('locale', savedLocale);
        grunt.config.set('packaging.prod.expanded_localization_files', res);
    });

    grunt.registerTask('prod', ['expandLocalizationFiles', 'atpackager:prod']);
};
<commit_msg>fix #447 avoiding * in the root map for the aria package itself.<commit_after>/*
 * Copyright 2012 Amadeus s.a.s.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * Aria Templates opensource production build file.

 * This build depends on the bootstrap build to execute properly.

 * This build does the following things:
 *

 *
creates packages from multiple input files
,
 *
compiles templates to JavaScript classes
,
 *
minifies JavaScript
,
 *
adds a header in each package with license and current release version
,
 *
appends MD5 checksum to each file name as a way to make files cacheable for longer periods
.
 *

 */

module.exports = function (grunt) {
    var atExtensions = ['**/*.js', '**/*.tpl', '**/*.tpl.css', '**/*.tpl.txt', '**/*.tml', '**/*.cml'];
    var notAtExtensions = atExtensions.map(function (value) {
        return '!' + value;
    });
    var mainATFile = 'aria/<%= pkg.name %>-<%= pkg.version %>.js';

    grunt.config.set('atpackager.prod', {
        options : {
            ATBootstrapFile : mainATFile,
            sourceDirectories : ['<%= packaging.bootstrap.outputdir %>'],
            sourceFiles : ['**/*'],
            defaultBuilder : {
                type : 'ATMultipart',
                cfg : {
                    header : '<%= packaging.license %>'
                }
            },
            outputDirectory : '<%= packaging.prod.outputdir %>',
            visitors : [{
                        type : 'ATDependencies',
                        cfg : {
                            externalDependencies : '<%= packaging.bootstrap.files %>',
                            files : atExtensions.concat(['!' + mainATFile])
                        }
                    }, {
                        type : 'CheckDependencies',
                        cfg : {
                            checkPackagesOrder : false
                        }
                    }, 'ATCompileTemplates', 'ATRemoveDoc', {
                        type : 'JSMinify',
                        cfg : {
                            files : atExtensions
                        }
                    }, {
                        type : 'Hash',
                        cfg : {
                            files : atExtensions.concat(['aria/core/transport/iframeSource*',
                                    'aria/utils/FrameATLoaderHTML*', '**/*.jnlp', '!aria/css/**', '!' + mainATFile,
                                    '<%= packaging.prod.hash_include_files %>'])
                        }
                    }, {
                        type : 'ATUrlMap',
                        cfg : {
                            mapFile : mainATFile,
                            sourceFiles : ['**/*', '!aria/css/**/*'],
                            starCompress : ['**/*', '!aria'],
                            starStarCompress : ['**/*', '!aria/resources']
                        }
                    }, {
                        type : 'CopyUnpackaged',
                        cfg : {
                            files : [mainATFile, 'aria/css/*.js', '<%= packaging.prod.allow_unpackaged_files %>'],
                            builder : {
                                type : 'ATMultipart',
                                cfg : {
                                    header : '<%= packaging.license %>'
                                }
                            }
                        }
                    }, {
                        type : 'CopyUnpackaged',
                        cfg : {
                            files : ['**/*.jnlp', "aria/core/transport/iframeSource.txt",
                                    "aria/utils/FrameATLoaderHTML.html"],
                            builder : 'ATMultipart'
                        }
                    }, {
                        type : 'CopyUnpackaged',
                        cfg : {
                            files : ['**/*'].concat(notAtExtensions)
                        }
                    }, 'CheckPackaged'],
            packages : ['<%= packaging.prod.files %>', '<%= packaging.prod.expanded_localization_files %>']
        }
    });

    grunt.config.set('gzipStats.prod', {
        src : '<%= packaging.prod.outputdir %>'
    });

    grunt.config.set('removedirs.prod', {
        folders : ['<%= packaging.prod.outputdir %>']
    });

    grunt.registerTask('expandLocalizationFiles', function () {
        var res = [];
        var savedLocale = grunt.config.getRaw('locale');
        var locales = grunt.config.get('packaging.locales');
        locales.forEach(function (curLocale) {
            grunt.config.set('locale', "_" + curLocale);
            res.push(grunt.config.get('packaging.prod.localization_files'));
        });
        grunt.config.set('locale', "");
        res.push(grunt.config.get('packaging.prod.localization_files'));
        grunt.config.set('locale', savedLocale);
        grunt.config.set('packaging.prod.expanded_localization_files', res);
    });

    grunt.registerTask('prod', ['expandLocalizationFiles', 'atpackager:prod']);
};
<|endoftext|>


<commit_before>javascript: (function() {

    var url = prompt('URL');
    if (!url) {
        mw.notify('已取消');
        return;
    }

    new mw.Api().postWithEditToken({
        'action': 'wbcreateclaim',
        'format': 'json',
        'entity': mw.config.get('wgTitle'),
        'snaktype': 'value',
        'property': 'P1',
        'value': '"' + url + '"',
    }).then(function() {
        mw.notify('成功新增');
        location.reload();
    }, function(e) {
        mw.notify('未知錯誤：' + e);
    });

}
)();
<commit_msg>Check claims exists<commit_after>javascript: (function() {

    var api = new mw.Api();
    api.get({
        'action': 'wbgetclaims',
        'format': 'json',
        'entity': mw.config.get('wgTitle'),
        'property': 'P1'
    }).then(function(data) {
        if (data['claims']['P1'] !== undefined) {
            mw.notify('已存在資料');
            return;
        }

        var url = prompt('URL');
        if (!url) {
            mw.notify('已取消');
            return;
        }

        api.postWithEditToken({
            'action': 'wbcreateclaim',
            'format': 'json',
            'entity': mw.config.get('wgTitle'),
            'snaktype': 'value',
            'property': 'P1',
            'value': '"' + url + '"',
        }).then(function() {
            mw.notify('成功新增');
            location.reload();
        }, function(e) {
            mw.notify('未知錯誤：' + e);
        });
    }, function(e) {
        mw.notify('未知錯誤：' + e);
    });

}
)();
<|endoftext|>