mibew/src/gulpfile.js

540 lines
20 KiB
JavaScript
Raw Normal View History

var fs = require('fs'),
https = require('https'),
exec = require('child_process').exec,
eventStream = require('event-stream'),
2014-08-07 15:04:45 +04:00
runSequence = require('run-sequence'),
through = require('through2'),
lodash = require('lodash'),
PoFile = require('pofile'),
strftime = require('strftime'),
2015-03-31 13:54:34 +03:00
del = require('del'),
bower = require('bower'),
gulp = require('gulp'),
uglify = require('gulp-uglify'),
concat = require('gulp-concat'),
phpcs = require('gulp-phpcs'),
order = require('gulp-order'),
handlebars = require('gulp-handlebars'),
2014-09-15 18:32:34 +04:00
handlebarsEngine = require('handlebars'),
defineModule = require('gulp-define-module'),
2014-08-07 15:04:45 +04:00
header = require('gulp-header'),
zip = require('gulp-zip'),
tar = require('gulp-tar'),
gzip = require('gulp-gzip'),
2014-08-13 12:40:42 +04:00
chmod = require('gulp-chmod'),
xgettext = require('gulp-xgettext'),
concatPo = require('gulp-concat-po'),
rename = require('gulp-rename'),
eslint = require('gulp-eslint');
// Set global configs.
var config = {
mibewPath: 'mibew',
configsPath: 'mibew/configs',
phpVendorPath: 'mibew/vendor',
jsVendorPath: 'mibew/js/vendor',
pluginsPath: 'mibew/plugins',
avatarsPath: 'mibew/files/avatar',
2014-09-24 17:04:37 +04:00
cachePath: 'mibew/cache',
jsPath: 'mibew/js',
chatStylesPath: 'mibew/styles/chats',
pageStylesPath: 'mibew/styles/pages',
compiledTemplatesHeader: fs.readFileSync('tools/compiled_templates_header.txt'),
2014-08-07 15:04:45 +04:00
getComposerUrl: 'https://getcomposer.org/installer',
2017-11-10 15:52:05 +03:00
phpBin: 'php -d "suhosin.executor.include.whitelist = phar" -d "memory_limit=512M"',
package: require('./composer.json')
}
2015-03-31 13:54:34 +03:00
// Cleans all built files
gulp.task('clean', function(callback) {
del([
'release',
'composer.lock',
config.phpVendorPath,
config.jsVendorPath,
2015-04-14 14:43:56 +03:00
config.jsPath + '/compiled/**/*',
2015-03-31 13:54:34 +03:00
'!' + config.jsPath + '/compiled/.keep',
config.chatStylesPath + '/*/templates_compiled/client_side/*.js',
config.chatStylesPath + '/*/js/compiled/*.js',
config.pageStylesPath + '/*/templates_compiled/client_side/*.js'
2015-03-31 13:54:34 +03:00
], callback);
});
// Checks all PHP files with PHP Code Sniffer.
gulp.task('phpcs', ['composer-install-dev'], function() {
return gulp.src([
config.mibewPath + '/**/*.php',
'!' + config.phpVendorPath + '/**/*.*',
2014-09-24 17:04:37 +04:00
'!' + config.pluginsPath + '/**/*.*',
'!' + config.cachePath + '/**/*.*'
], {
// Content of the cache directory is readable only for webserver. Thus
// we must to set "strict" option to false to prevent "EACCES" errors.
// At the same we need to see all errors that take place.
strict: false,
silent: false
})
.pipe(phpcs({
bin: config.phpVendorPath + '/bin/phpcs',
standard: 'PSR2',
warningSeverity: 0
2014-08-13 15:03:03 +04:00
}))
.pipe(phpcs.reporter('log'))
.pipe(phpcs.reporter('fail'));
});
// Checks all JavaScript Source files with ESLint.
gulp.task('eslint', function() {
return gulp.src(config.jsPath + '/source/**/*.js')
.pipe(eslint())
.pipe(eslint.format())
.pipe(eslint.failAfterError());
});
// Get and install PHP Composer
gulp.task('get-composer', function(callback) {
// Check if Composer already in place
if (fs.existsSync('./composer.phar')) {
callback(null);
return;
}
// Get installer from the internet
https.get(config.getComposerUrl, function(response) {
// Run PHP to install Composer
var php = exec(config.phpBin, function(error, stdout, stderr) {
callback(error ? stderr : null);
});
// Pass installer code to PHP via STDIN
response.pipe(php.stdin);
});
});
// Install Composer dependencies excluding development ones
gulp.task('composer-install', ['get-composer'], function(callback) {
exec(config.phpBin + ' composer.phar install --no-dev', function(error, stdout, stderr) {
callback(error ? stderr : null);
});
});
// Install all Composer dependencies
gulp.task('composer-install-dev', ['get-composer'], function(callback) {
exec(config.phpBin + ' composer.phar install', function(error, stdout, stderr) {
callback(error ? stderr : null);
});
});
// Installs bower dependencies
gulp.task('bower-install', function(callback) {
bower.commands.install([], {}, {})
.on('error', function(error) {
callback(error);
})
.on('end', function() {
// We should manually minify JavaScript files that was not minified
// by bower packages' authors.
// TODO: This is a temproary workaround and should be removed once
// the packages will be fixed.
var stream = eventStream.merge(
gulp.src(config.jsVendorPath + '/backbone/backbone.js', {base: config.jsVendorPath})
.pipe(uglify({preserveComments: 'some'}))
// There are neither "@license" tag nor "!preserve" in the
// header. Add the header manually.
.pipe(header(
"// Backbone.js 1.1.2\n"
+ "// (c) 2010-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors\n"
+ "// Backbone may be freely distributed under the MIT license.\n"
+ "// For all details and documentation:\n"
+ "// http://backbonejs.org\n"
))
.pipe(rename('backbone/backbone-min.js')),
gulp.src(config.jsVendorPath + '/json/json2.js', {base: config.jsVendorPath})
.pipe(uglify({preserveComments: 'some'}))
// There are neither "@license" tag nor "!preserve" in the
// header. Add the header manually.
.pipe(header("// json2.js. Public Domain. See http://www.JSON.org/js.html\n"))
.pipe(rename('json/json2.min.js'))
2015-04-14 14:39:33 +03:00
)
.pipe(gulp.dest(config.jsVendorPath));
stream
.on('error', callback)
.on('end', callback);
});
});
// Compile all JavaScript files of the Mibew Core
gulp.task('js', function() {
return eventStream.merge(
getClientSideApp('default'),
getClientSideApp('chat'),
getClientSideApp('thread_log'),
getClientSideApp('users'),
gulp.src(config.jsPath + '/source/**/*.js')
)
.pipe(uglify({preserveComments: 'some'}))
.pipe(gulp.dest(config.jsPath + '/compiled'));
});
// Performs all job related with chat styles.
gulp.task('chat-styles', ['chat-styles-handlebars', 'chat-styles-js'], function() {
// This task is just a combination of other tasks. That is why there is no
// real code.
});
// Compile and concatenate handlebars files for all chat styles.
gulp.task('chat-styles-handlebars', function() {
2017-03-16 23:46:01 +03:00
return getChildDirs(config.chatStylesPath)
.then(function (dirs) {
return Promise.all(dirs.map(function (dir) {
return new Promise(function(resolve, reject){
gulp.src(config.chatStylesPath + '/' + dir + '/templates_src/client_side/**/*.handlebars')
.pipe(handlebars({
// Use specific version of Handlebars.js
handlebars: handlebarsEngine
}))
.pipe(wrapHandlebarsTemplate())
.pipe(concat('templates.js'))
.pipe(uglify({preserveComments: 'some'}))
.pipe(header(config.compiledTemplatesHeader))
.pipe(gulp.dest(config.chatStylesPath + '/' + dir + '/templates_compiled/client_side'))
.on('end', resolve)
.on('error', reject);
});
}));
2017-03-10 01:44:53 +03:00
});
});
// Compile and concatenate js files for all chat styles.
gulp.task('chat-styles-js', function() {
2017-03-16 23:46:01 +03:00
return getChildDirs(config.chatStylesPath)
.then(function (dirs) {
return Promise.all(dirs.map(function (dir) {
return new Promise(function(resolve, reject){
gulp.src(config.chatStylesPath + '/' + dir + '/js/source/**/*.js')
.pipe(concat('scripts.js'))
.pipe(uglify({preserveComments: 'some'}))
.pipe(gulp.dest(config.chatStylesPath + '/' + dir + '/js/compiled'))
.on('end', resolve)
.on('error', reject);
});
}));
2017-03-10 01:44:53 +03:00
});
});
// Performs all job related with pages styles.
gulp.task('page-styles', function() {
2017-03-16 23:46:01 +03:00
return getChildDirs(config.pageStylesPath)
.then(function (dirs) {
return Promise.all(dirs.map(function (dir) {
return new Promise(function(resolve, reject){
gulp.src(config.pageStylesPath + '/' + dir + '/templates_src/client_side/**/*.handlebars')
.pipe(handlebars({
// Use specific version of Handlebars.js
handlebars: handlebarsEngine
}))
.pipe(wrapHandlebarsTemplate())
.pipe(concat('templates.js'))
.pipe(uglify({preserveComments: 'some'}))
.pipe(header(config.compiledTemplatesHeader))
.pipe(gulp.dest(config.pageStylesPath + '/' + dir + '/templates_compiled/client_side'))
.on('end', resolve)
.on('error', reject);
});
}));
2017-03-10 01:44:53 +03:00
});
});
2017-03-10 01:45:23 +03:00
// Watch styles
gulp.task('watch', [], function(){
gulp.watch(config.pageStylesPath + '/**/*.handlebars', ['page-styles']);
gulp.watch(config.chatStylesPath + '/**/js/source/**/*.js', ['chat-styles-js']);
gulp.watch(config.chatStylesPath + '/**/*.handlebars', ['chat-styles-handlebars']);
});
2014-08-13 12:40:42 +04:00
// Generate .pot files based on the sources
gulp.task('generate-pot', function() {
return eventStream.merge(
gulp.src([
config.mibewPath + '/**/*.php',
'!' + config.phpVendorPath + '/**/*.*',
2014-09-24 17:04:37 +04:00
'!' + config.pluginsPath + '/**/*.*',
'!' + config.cachePath + '/**/*.*'
], {
// Content of the cache directory is readable only for webserver.
// Thus we must to set "strict" option to false to prevent "EACCES"
// errors. At the same we need to see all errors that take place.
strict: false,
silent: false
})
.pipe(xgettext({
language: 'PHP',
keywords: [
2014-08-18 13:42:08 +04:00
{name: 'getlocal'},
{name: 'no_field'},
{name: 'wrong_field'},
{name: 'failed_uploading_file', singular: 2}
]
})),
gulp.src(config.jsPath + '/source/**/*.js', {base: config.mibewPath})
.pipe(xgettext({
language: 'JavaScript',
keywords: [
{name: 'trans'}
]
})),
gulp.src([
config.chatStylesPath + '/default/templates_src/**/*.handlebars',
config.pageStylesPath + '/default/templates_src/**/*.handlebars'
], {base: config.mibewPath})
.pipe(xgettextHandlebars())
)
2014-08-13 12:40:42 +04:00
.pipe(concatPo('translation.pot', {
headers: {
2014-08-19 16:49:54 +04:00
'Project-Id-Version': 'Mibew Messenger ' + config.package.version,
'Report-Msgid-Bugs-To': config.package.support.email,
'POT-Creation-Date': strftime('%Y-%m-%d %H:%M%z'),
2014-08-13 12:40:42 +04:00
'PO-Revision-Date': '',
'Last-Translator': '',
'Language-Team': '',
'Content-Type': 'text/plain; charset=UTF-8'
}
}))
.pipe(gulp.dest('release'));
});
2014-08-07 15:04:45 +04:00
// Pack sources to .zip and .tar.gz archives.
gulp.task('pack-sources', ['composer-install', 'bower-install'], function() {
var sources = [
config.mibewPath + '/**/*',
// Exclude user's config
'!' + config.configsPath + '/config.yml',
// Exclude cache files but not the ".keep" file.
2014-09-24 17:04:37 +04:00
'!' + config.cachePath + '/**/!(.keep)',
// Exclude avatars but not the ".keep" file.
'!' + config.avatarsPath + '/!(.keep)',
// Exclude plugins but not the ".keep" file.
'!' + config.pluginsPath + '/!(.keep)',
'!' + config.pluginsPath + '/*/**/*',
// Exclude Git repositories that can be shipped with third-party libs
'!' + config.phpVendorPath + '/**/.git',
'!' + config.phpVendorPath + '/**/.git/**/*',
// Exclude vendors binaries
'!' + config.phpVendorPath + '/bin/**/*',
// Exclude JavaScript sources
'!' + config.jsPath + '/source/**/*',
// Actually we does not need backbone.babysitter and backbone.wreqr
// dependencies because they embed into marionette.js. So we exclude
// "backbone.babysitter" and "backbone.wreqr" directories and all their
// contents.
'!' + config.jsVendorPath + '/backbone.babysitter{,/**}',
'!' + config.jsVendorPath + '/backbone.wreqr{,/**}',
// Exclude dot files within third-party JS libraries.
'!' + config.jsVendorPath + '/**/.*',
// Exclude config files of various package systems
'!' + config.jsVendorPath + '/**/{bower,component,package,composer}.json',
// Exclude config files of various build systems
'!' + config.jsVendorPath + '/**/Gruntfile.*',
'!' + config.jsVendorPath + '/**/gulpfile.*',
'!' + config.jsVendorPath + '/**/Makefile',
// Exclude HTML files from third-party JS libraries. Such files can be
// used for docs or for tests, we need none of them.
'!' + config.jsVendorPath + '/**/*.html',
// There are too many useless files in Vex.js library. Exclude them.
'!' + config.jsVendorPath + '/vex/sass{,/**}',
'!' + config.jsVendorPath + '/vex/docs{,/**}',
'!' + config.jsVendorPath + '/vex/docs{,/**}',
'!' + config.jsVendorPath + '/vex/coffee{,/**}'
];
2014-09-24 17:04:37 +04:00
var srcOptions = {
// Dot files (.htaccess, .keep, etc.) must be included in the package.
dot: true,
// Content of the cache directory is readable only for webserver. Thus
// we must to set "strict" option to false to prevent "EACCES" errors.
// At the same we need to see all errors that take place.
strict: false,
silent: false
}
var version = config.package.version;
2014-08-07 15:04:45 +04:00
return eventStream.merge(
2014-09-24 17:04:37 +04:00
gulp.src(sources, srcOptions)
2014-08-07 15:04:45 +04:00
.pipe(zip('mibew-' + version + '.zip')),
2014-09-24 17:04:37 +04:00
gulp.src(sources, srcOptions)
2014-08-07 15:04:45 +04:00
.pipe(tar('mibew-' + version + '.tar'))
.pipe(gzip())
)
2017-02-08 10:41:18 +03:00
.pipe(chmod(644))
2014-08-07 15:04:45 +04:00
.pipe(gulp.dest('release'));
});
// Performs all tasks in the correct order.
gulp.task('prepare-release', function(callback) {
2014-08-07 15:04:45 +04:00
runSequence(
2015-03-31 13:54:34 +03:00
'clean',
2014-08-13 12:40:42 +04:00
['phpcs', 'js', 'chat-styles', 'page-styles', 'generate-pot'],
2014-08-07 15:04:45 +04:00
'pack-sources',
callback
);
});
2015-04-17 17:18:59 +03:00
// Prepares ready to use development version of Mibew without packing or
// validating it.
gulp.task('rebuild', function(callback) {
runSequence(
'clean',
['js', 'chat-styles', 'page-styles', 'composer-install', 'bower-install'],
callback
);
});
// Builds the sources
gulp.task('default', function(callback) {
runSequence(
2015-04-17 16:07:25 +03:00
'clean',
['js', 'chat-styles', 'page-styles'],
'pack-sources',
callback
);
});
/**
* Loads and prepare js file for a client side application with the specified
* name.
*
* @param {String} name Application name
* @returns A files stream that can be piped to any gulp plugin.
*/
var getClientSideApp = function(name) {
var appSource = config.jsPath + '/source/' + name;
return gulp.src(appSource + '/**/*.js')
.pipe(order(
[
appSource + '/init.js',
// The following line is equivalent to
// gulp.src([appSource + '/*.js', '!' + appSource + '/app.js']);
appSource + '/!(app).js',
appSource + '/models/**/base*.js',
appSource + '/models/**/*.js',
appSource + '/collections/**/base*.js',
appSource + '/collections/**/*.js',
appSource + '/model_views/**/base*.js',
appSource + '/model_views/**/*.js',
appSource + '/collection_views/**/base*.js',
appSource + '/collection_views/**/*.js',
appSource + '/regions/**/base*.js',
appSource + '/regions/**/*.js',
appSource + '/layouts/**/base*.js',
appSource + '/layouts/**/*.js',
appSource + '/**/base*.js',
// The following line is equivalent to
// gulp.src([appSource + '/**/*.js', '!' + appSource + '/app.js']);
'!' + appSource + '/app.js',
appSource + '/app.js'
],
{base: process.cwd()}
))
.pipe(concat(name + '_app.js'));
}
/**
* Wraps a handlebars template with a function and attach it to the
* Handlebars.templates object.
*
* @returns {Function} A function that can be used in pipe() method of a file
* stream right after gulp-handlebars plugin.
*/
var wrapHandlebarsTemplate = function() {
return defineModule('plain', {
wrapper: '(function() {\n'
+ 'var templates = Handlebars.templates = Handlebars.templates || {};\n'
+ 'Handlebars.templates["<%= relative %>"] = <%= handlebars %>;\n'
+ '})()',
context: function(context) {
2015-01-23 14:34:24 +03:00
return {relative: context.file.relative.replace(/\.js$/, '').replace(/\\/g, '/')};
}
});
}
/**
* Extracts gettext messages from handlebars templates.
*
* @returns {Function} A function that can be used in pipe() method of a file
* stream.
*/
var xgettextHandlebars = function() {
var helperRegExp = /\{{2}l10n\s*('|")(.*?[^\\])\1.*?\}{2}/g;
return through.obj(function (file, enc, callback) {
var po = new PoFile();
match = false,
contents = file.contents.toString();
while (match = helperRegExp.exec(contents)) {
// Try to find item in the .po file by its name.
var item = lodash.find(po.items, function(item) {
return match[2] === item.msgid;
});
var line = contents.substr(0, match.index).split(/\r?\n|\r/g).length;
if (!item) {
// There is no such item. Create new one.
item = new PoFile.Item();
item.msgid = match[2].replace(/\\'/g, "'").replace(/\\"/g, '"');
po.items.push(item);
}
// Add new reference
item.references.push(file.relative + ':' + line);
}
// Update file contents
file.contents = new Buffer(po.toString());
this.push(file);
callback();
});
}
2017-03-13 14:29:16 +03:00
/**
* Retrieves list of all dirs which are placed in the specified one.
*
* @param {String} srcDir A dir where to search.
2017-03-16 23:46:01 +03:00
* @returns Promise A promise which will be resolved with list of child dirs or
* rejected with the occured error.
2017-03-13 14:29:16 +03:00
*/
var getChildDirs = function(srcDir) {
2017-03-16 23:46:01 +03:00
return (new Promise(function(resolve, reject) {
fs.readdir(srcDir, function (err, files) {
if (err) {
reject(err);
} else {
resolve(files);
}
});
})).then(function (files) {
// Replace all the files that are not a directory with nulls.
return Promise.all(files.map(function (file) {
return new Promise(function (resolve, reject) {
2017-11-10 18:15:59 +03:00
fs.lstat(srcDir + '/' + file, function (err, stat) {
2017-03-16 23:46:01 +03:00
if (err) {
reject(err);
} else {
resolve(stat.isDirectory() ? file : null);
}
});
});
}));
}).then(function(dirs) {
// Remove all the nulls from the array.
return dirs.filter(function (dir) {
return null !== dir;
});
2017-03-13 14:29:16 +03:00
});
}