1197 lines
39 KiB
JavaScript
1197 lines
39 KiB
JavaScript
/* GULP itself */
|
||
'use strict';
|
||
|
||
let util = require('util');
|
||
let fs = require('fs');
|
||
let ini = require('ini');
|
||
|
||
let gulp = require('gulp');
|
||
let requirejsOptimize = require('gulp-requirejs-optimize');
|
||
let filter = require('gulp-filter');
|
||
let gulpif = require('gulp-if');
|
||
let jshint = require('gulp-jshint');
|
||
let sourcemaps = require('gulp-sourcemaps');
|
||
let zlib = require('zlib');
|
||
let gzip = require('gulp-gzip');
|
||
let brotli = require('gulp-brotli');
|
||
let uglifyjs = require('uglify-es');
|
||
let composer = require('gulp-uglify/composer');
|
||
let sass = require('gulp-sass');
|
||
let autoprefixer = require('gulp-autoprefixer');
|
||
let cleanCSS = require('gulp-clean-css');
|
||
let imageResize = require('gulp-image-resize');
|
||
let imagemin = require('gulp-imagemin');
|
||
let imageminWebp = require('imagemin-webp');
|
||
let rename = require('gulp-rename');
|
||
let bytediff = require('gulp-bytediff');
|
||
let debug = require('gulp-debug');
|
||
let notifier = require('node-notifier');
|
||
|
||
// -- Helper & NPM modules ----------------------------------------------------
|
||
let flatten = require('flat');
|
||
let padEnd = require('lodash.padend');
|
||
let merge = require('lodash.merge');
|
||
let minimist = require('minimist');
|
||
let slash = require('slash');
|
||
let fileExtension = require('file-extension');
|
||
let log = require('fancy-log');
|
||
let colors = require('ansi-colors');
|
||
let stylish = require('jshint-stylish');
|
||
let Table = require('terminal-table');
|
||
let prettyBytes = require('pretty-bytes');
|
||
let del = require('promised-del');
|
||
|
||
let minify = composer(uglifyjs, console);
|
||
|
||
sass.compiler = require('node-sass');
|
||
|
||
// == Settings ========================================================================================================
|
||
|
||
// build/src directories
|
||
let PATH = {
|
||
JS_HINT: {
|
||
CONF: '/.jshintrc'
|
||
},
|
||
ASSETS: {
|
||
DEST: './public'
|
||
},
|
||
JS: {
|
||
SRC: 'js/**/*.js',
|
||
SRC_LIBS: './js/lib/**/*',
|
||
DEST: 'public/js',
|
||
DEST_BUILD: './public/js/vX.X.X'
|
||
},
|
||
CSS: {
|
||
SRC: 'sass/**/*.scss',
|
||
},
|
||
IMG: {
|
||
SRC: 'img/**/*.{jpg,png,svg}',
|
||
SRC_HEADER: 'img/header/*.{jpg,png}',
|
||
SRC_GALLERY: 'img/gallery/**/*.{jpg,png}',
|
||
SRC_SVG: 'img/svg/*.svg'
|
||
}
|
||
};
|
||
|
||
// Pathfinder config files
|
||
let pathfinderConfigFileApp = './app/pathfinder.ini';
|
||
let pathfinderConfigFileConf = './conf/pathfinder.ini';
|
||
|
||
// CLI box size in characters
|
||
let cliBoxLength = 80;
|
||
|
||
// cache for already combined JS files
|
||
let combinedJsFiles = [];
|
||
|
||
// cache for tracked JS files
|
||
let trackedFiles = {};
|
||
|
||
// columns for tracked JS files (used for mapping)
|
||
let trackTable = {
|
||
cols: [
|
||
'file',
|
||
'src',
|
||
'src_percent',
|
||
'uglify',
|
||
'gzipFile',
|
||
'gzip_percent',
|
||
'gzip',
|
||
'brotliFile',
|
||
'brotli_percent',
|
||
'brotli',
|
||
'all_percent',
|
||
'mapFile',
|
||
'map'
|
||
]
|
||
};
|
||
|
||
// UglifyJS options
|
||
// https://www.npmjs.com/package/uglify-es
|
||
let uglifyJsOptions = {
|
||
warnings: true,
|
||
toplevel: false,
|
||
ecma: 8,
|
||
nameCache: {}, // cache mangled variable and property names across multiple invocations of minify()
|
||
keep_classnames: true // pass true to prevent discarding or mangling of class names.
|
||
};
|
||
|
||
// Sourcemaps options
|
||
// https://www.npmjs.com/package/gulp-sourcemaps
|
||
|
||
// -- Error output ------------------------------------------------------------
|
||
|
||
/**
|
||
* print error box output
|
||
* @param title
|
||
* @param example
|
||
*/
|
||
let printError = (title, example) => {
|
||
log('');
|
||
log(colors.danger.divider('error'));
|
||
log(colors.iconDanger(title));
|
||
if(example){
|
||
log(` ${colors.gray(example)}`);
|
||
}
|
||
log(colors.danger.divider(' '));
|
||
log('');
|
||
};
|
||
|
||
// == Settings ========================================================================================================
|
||
|
||
// parse pathfinder.ini config file for relevant data
|
||
let tagVersion;
|
||
try{
|
||
let pathfinderAppIni = ini.parse(fs.readFileSync(pathfinderConfigFileApp, 'utf-8'));
|
||
let pathfinderConfIni = fs.existsSync(pathfinderConfigFileConf) ?
|
||
ini.parse(fs.readFileSync(pathfinderConfigFileConf, 'utf-8')) : {};
|
||
let pathfinderIni = merge(pathfinderAppIni, pathfinderConfIni);
|
||
|
||
try{
|
||
tagVersion = pathfinderIni.PATHFINDER.VERSION;
|
||
}catch(err){
|
||
printError(
|
||
err.message,
|
||
'Missing "PATHFINDER.VERSION" in "' + pathfinderConfigFileApp + '"');
|
||
process.exit(1);
|
||
}
|
||
}catch(err){
|
||
printError(
|
||
err.message,
|
||
'Check read permissions for "' + pathfinderConfigFileApp + '"');
|
||
process.exit(1);
|
||
}
|
||
|
||
// parse CLI parameters
|
||
let options = minimist(process.argv.slice(2));
|
||
|
||
// custom task configuration (user CLI options if provided) (overwrites default)
|
||
let CONF = {
|
||
TASK: options.hasOwnProperty('_') ? options._[0] : undefined,
|
||
TAG: options.tag ? options.tag : tagVersion ? tagVersion : undefined,
|
||
JS: {
|
||
UGLIFY: options.hasOwnProperty('jsUglify') ? options.jsUglify === 'true': undefined,
|
||
SOURCEMAPS: options.hasOwnProperty('jsSourcemaps') ? options.jsSourcemaps === 'true': undefined,
|
||
GZIP: options.hasOwnProperty('jsGzip') ? options.jsGzip === 'true': undefined,
|
||
BROTLI: options.hasOwnProperty('jsBrotli') ? options.jsBrotli === 'true': undefined
|
||
},
|
||
CSS: {
|
||
SOURCEMAPS: options.hasOwnProperty('cssSourcemaps') ? options.cssSourcemaps === 'true': undefined,
|
||
GZIP: options.hasOwnProperty('cssGzip') ? options.cssGzip === 'true': undefined,
|
||
BROTLI: options.hasOwnProperty('cssBrotli') ? options.cssBrotli === 'true': undefined
|
||
},
|
||
IMG: {
|
||
ACTIVE: options.hasOwnProperty('imgActive') ? options.imgActive === 'true': undefined,
|
||
},
|
||
DEBUG: false
|
||
};
|
||
|
||
// -- Plugin options ----------------------------------------------------------
|
||
let sassOptions = {
|
||
errorLogToConsole: true,
|
||
outputStyle: 'compressed' // nested, expanded, compact, compressed
|
||
};
|
||
|
||
let autoprefixerOptions = {
|
||
// browsers: ['last 2 versions'], read from package.json key: "browserslist"
|
||
cascade: false
|
||
};
|
||
|
||
let cleanCssOptions = {
|
||
compatibility: '*',
|
||
level: 2
|
||
};
|
||
|
||
let gZipOptions = {
|
||
append: false, // disables default append ext .gz
|
||
extension: 'gz', // use "custom" ext: .gz
|
||
threshold: '1kb', // min size required to compress a file
|
||
deleteMode: PATH.JS.DEST_BUILD, // replace *.gz files if size < 'threshold'
|
||
gzipOptions: {
|
||
level: 9 // zlib.Gzip compression level [0-9]
|
||
},
|
||
skipGrowingFiles: true // use orig. files in case of *.gz size > orig. size
|
||
};
|
||
|
||
let brotliOptions = {
|
||
extension: 'br', // use "custom" ext: .br
|
||
skipLarger: true, // use orig. files in case of *.br size > orig. size
|
||
params: {
|
||
[zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT, // compression mode for UTF-8 formatted text
|
||
[zlib.constants.BROTLI_PARAM_QUALITY]: zlib.constants.BROTLI_MAX_QUALITY // quality [1 worst - 11 best]
|
||
}
|
||
};
|
||
|
||
let compressionExt = [gZipOptions.extension, brotliOptions.extension];
|
||
|
||
let imageminWebpOptions = {quality: 80};
|
||
|
||
let imgResizeOptions = {
|
||
crop : true,
|
||
upscale : false,
|
||
noProfile: true
|
||
};
|
||
|
||
colors.theme({
|
||
primary: colors.cyan,
|
||
success: colors.green,
|
||
info: colors.blue,
|
||
warning: colors.yellow,
|
||
danger: colors.red,
|
||
|
||
disabled: (...args) => colors.dim.gray(util.format(...args)),
|
||
comment: (...args) => colors.disabled.italic(util.format(...args)),
|
||
|
||
iconSuccess: text => ` ${colors.success(`✔`)} ${colors.success(text)}`,
|
||
iconDanger: text => ` ${colors.danger(`✘`)} ${colors.danger(text)}`,
|
||
|
||
divider: text => colors.disabled(`${(text || '').trim().length ? `═ ${(text || '').trim().toUpperCase()} ═` : ''}`.padEnd(cliBoxLength, '═'))
|
||
});
|
||
|
||
/**
|
||
* example output formatting
|
||
*/
|
||
/*
|
||
log(colors.primary('primary'));
|
||
log(colors.success('success'));
|
||
log(colors.info('info'));
|
||
log(colors.warning('warning'));
|
||
log(colors.danger('danger'));
|
||
|
||
log(colors.disabled('disabled'));
|
||
log(colors.comment('comment'));
|
||
|
||
log(colors.iconSuccess('success'));
|
||
log(colors.iconDanger('danger'));
|
||
|
||
log(colors.bold('bold'));
|
||
log(colors.italic('italic'));
|
||
log(colors.underline('underline'));
|
||
log('');
|
||
log(colors.magenta('magenta'));
|
||
log(colors.magentaBright('magentaBright'));
|
||
log(colors.cyan('cyan'));
|
||
log(colors.cyanBright('cyanBright'));
|
||
log(colors.blue('blue'));
|
||
log(colors.blueBright('blueBright'));
|
||
log(colors.yellow('yellow'));
|
||
log(colors.yellowBright('yellowBright'));
|
||
log('');
|
||
log('');
|
||
*/
|
||
|
||
// == Helper methods ==================================================================================================
|
||
|
||
/**
|
||
* track a file by size and filename, provide a mapping (see trackTable)
|
||
* @param data
|
||
* @param mapping
|
||
*/
|
||
let trackFile = (data, mapping) => {
|
||
let fileNameParts = data.fileName.split('.');
|
||
let fileExt = fileNameParts.pop();
|
||
let srcFileName = compressionExt.concat(['map']).includes(fileExt) ? fileNameParts.join('.') : data.fileName;
|
||
let fileData = trackedFiles[srcFileName] || [];
|
||
|
||
// change mapping for *.map files
|
||
switch(fileExt){
|
||
case 'js':
|
||
mapping.all_percent = 'percent';
|
||
break;
|
||
case 'map':
|
||
mapping = {mapFile: 'fileName', map: 'endSize'};
|
||
break;
|
||
case gZipOptions.extension:
|
||
data.all_percent = data.endSize / fileData[0];
|
||
mapping = {gzipFile: 'fileName', gzip_percent: 'percent', gzip: 'endSize', all_percent: 'all_percent'};
|
||
break;
|
||
case brotliOptions.extension:
|
||
data.all_percent = data.endSize / fileData[0];
|
||
mapping = {brotliFile: 'fileName', brotli_percent: 'percent', brotli: 'endSize', all_percent: 'all_percent'};
|
||
break;
|
||
}
|
||
|
||
for(let col in mapping){
|
||
for(let i = 0; i < trackTable.cols.length; i++){
|
||
if(trackTable.cols[i] === col){
|
||
fileData[i - 1] = data[mapping[col]];
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
trackedFiles[srcFileName] = fileData;
|
||
};
|
||
|
||
/**
|
||
* recursive "merge" two config objects
|
||
* @param confUser
|
||
* @param confDefault
|
||
* @returns {*}
|
||
*/
|
||
let mergeConf = (confUser, confDefault) => {
|
||
for (let confKey in confUser) {
|
||
if (confUser.hasOwnProperty(confKey)){
|
||
if(confDefault.hasOwnProperty(confKey)){
|
||
if(
|
||
typeof confUser[confKey] === 'object' &&
|
||
typeof confDefault[confKey] === 'object'
|
||
){
|
||
confUser[confKey] = mergeConf(confUser[confKey], confDefault[confKey]);
|
||
}else if(typeof confUser[confKey] === 'undefined'){
|
||
confUser[confKey] = confDefault[confKey];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return confUser;
|
||
};
|
||
|
||
// == CLI output ======================================================================================================
|
||
|
||
/**
|
||
* print help information for all Gulp tasks
|
||
*/
|
||
let printHelp = () => {
|
||
log(`
|
||
${colors.divider('help')}
|
||
${colors.bold('docs:')} ${colors.info('https://github.com/exodus4d/pathfinder/wiki/GulpJs')}
|
||
${colors.bold('command:')} ${colors.magenta('$ npm run gulp')} ${colors.primary.italic('[task]')} ${colors.yellow.italic('-- [--options] …')}
|
||
${colors.bold('tasks:')}
|
||
${colors.primary('help')} This view
|
||
${colors.primary('default')} Development environment. Working with row src files and file watcher, default:
|
||
${colors.primary('')} ${colors.gray('--jsUglify=false --jsSourcemaps=false --cssSourcemaps=false --jsGzip=false --cssGzip=false --jsBrotli=false --cssBrotli=false --imgActive=true')}
|
||
${colors.primary('production')} Production build. Concat and uglify static resources, default:
|
||
${colors.primary('')} ${colors.gray('--jsUglify=true --jsSourcemaps=true --cssSourcemaps=true --jsGzip=true --cssGzip=true --jsBrotli=true --cssBrotli=true --imgActive=true')}
|
||
${colors.bold('helper tasks:')}
|
||
${colors.primary('images')} Build images. Convert src *.png images to *.webp and *.jpg. Crop & resize tasks
|
||
${colors.primary('')} ${colors.gray('--imgActive=true')}
|
||
|
||
${colors.bold('options:')}
|
||
${colors.yellow('--tag')} Set build version. ${colors.gray(`default: --tag="${tagVersion}" -> dest path: public/js/${tagVersion}`)}
|
||
${colors.yellow('--jsUglify')} Set js uglification. ${colors.gray('(true || false)')}
|
||
${colors.yellow('--jsSourcemaps')} Set js sourcemaps generation. ${colors.gray('(true || false)')}
|
||
${colors.yellow('--jsGzip')} Set js "gzip" compression mode. ${colors.gray('(true || false)')}
|
||
${colors.yellow('--jsBrotli')} Set js "brotli" compression mode. ${colors.gray('(true || false)')}
|
||
${colors.yellow('--cssSourcemaps')} Set CSS sourcemaps generation. ${colors.gray('(true || false)')}
|
||
${colors.yellow('--cssGzip')} Set CSS "gzip" compression mode. ${colors.gray('(true || false)')}
|
||
${colors.yellow('--cssBrotli')} Set CSS "brotli" compression mode. ${colors.gray('(true || false)')}
|
||
${colors.yellow('--imgActive')} Disables all image tasks. ${colors.gray('(true || false)')}
|
||
${colors.yellow('--debug')} Set debug mode (more output). ${colors.gray('(true || false)')}
|
||
${colors.divider(' ')}
|
||
`);
|
||
};
|
||
|
||
/**
|
||
* print JS summary table
|
||
*/
|
||
let printJsSummary = () => {
|
||
let tableHead = trackTable.cols;
|
||
let byteCols = [1,3,6,9,12];
|
||
let percentCols = [2, 5, 8, 10];
|
||
let sortCol = (CONF.JS.BROTLI || CONF.CSS.BROTLI || CONF.JS.GZIP || CONF.CSS.GZIP || CONF.JS.UGLIFY) ? 10 : CONF.JS.SOURCEMAPS ? 3 : 1;
|
||
let refAllCol = (CONF.JS.BROTLI || CONF.CSS.BROTLI) ? 9 : (CONF.JS.GZIP || CONF.CSS.GZIP) ? 6 : CONF.JS.UGLIFY ? 3 : CONF.JS.SOURCEMAPS ? 3 : 1;
|
||
let highLightSections = {src_percent: [], gzip_percent: [], brotli_percent: [], all_percent: []};
|
||
let highLightRow = {
|
||
success: JSON.parse(JSON.stringify(highLightSections)),
|
||
warning: JSON.parse(JSON.stringify(highLightSections)),
|
||
danger: JSON.parse(JSON.stringify(highLightSections))
|
||
};
|
||
|
||
let numFormatCols = byteCols.concat(percentCols);
|
||
let sumRow = [];
|
||
let widthCol = [35, 10, 8, 10, 35, 8, 10, 35, 8, 10, 8, 35, 10];
|
||
|
||
let table = new Table({
|
||
borderStyle: 2,
|
||
horizontalLine: true,
|
||
width: widthCol,
|
||
rightPadding: 0,
|
||
leftPadding: 0
|
||
});
|
||
|
||
// -- Table header --------------------------------------------------------
|
||
table.push(tableHead.map(label => label.replace(/^(.*?)_(percent?).*/i, '$1 %')));
|
||
|
||
// convert JSON to array
|
||
let tableData = [];
|
||
for (let fileName in trackedFiles) {
|
||
let rowData = trackedFiles[fileName];
|
||
rowData.unshift(fileName);
|
||
tableData.push(rowData);
|
||
}
|
||
|
||
let tableHeight = tableData.length + 1;
|
||
let tableWidth = tableHead.length;
|
||
|
||
tableData.sort((a,b) => a[sortCol] - b[sortCol]);
|
||
|
||
table.attr(0, sortCol, {
|
||
color: 'cyan'
|
||
});
|
||
|
||
// -- Table body ----------------------------------------------------------
|
||
let tmpMapping = {byteCols: byteCols, percentCols: percentCols};
|
||
|
||
// sum column data for footer
|
||
let sumCols = (arr, mapping) => arr.map((x, rowIdx) =>
|
||
x.map((y, i) => {
|
||
if(mapping.byteCols.includes(i)) {
|
||
sumRow[i] = (sumRow[i]) ? sumRow[i] + y : y;
|
||
}
|
||
return y;
|
||
})
|
||
);
|
||
|
||
// format table cell data
|
||
let formatCols = (arr, mapping) => arr.map((x, rowIdx) =>
|
||
x.map((y, i) => {
|
||
if(mapping.byteCols.includes(i)) {
|
||
return prettyBytes(y);
|
||
}else if(mapping.percentCols.includes(i)){
|
||
// 0.0% diff is "success" in case of Uglify is disabled
|
||
let isSuccess = (
|
||
y === 1 &&
|
||
!CONF.JS.UGLIFY &&
|
||
tableHead[i] === 'src_percent'
|
||
);
|
||
|
||
if(y < 0.3 || isSuccess) {
|
||
highLightRow.success[tableHead[i]].push(rowIdx);
|
||
}else if(y < 0.5 ){
|
||
highLightRow.warning[tableHead[i]].push(rowIdx);
|
||
}else{
|
||
highLightRow.danger[tableHead[i]].push(rowIdx);
|
||
}
|
||
|
||
return y ? (100 * (1 - y )).toFixed(1) + '%' : '';
|
||
}else{
|
||
return y;
|
||
}
|
||
})
|
||
);
|
||
|
||
tableData = sumCols(tableData, tmpMapping);
|
||
|
||
// -- Table footer --------------------------------------------------------
|
||
// percent cell src
|
||
sumRow[2] = ((sumRow[3] / sumRow[1]));
|
||
// percent cell gzip
|
||
sumRow[5] = (((sumRow[6] || 0) / sumRow[3]));
|
||
// percent cell brotli
|
||
sumRow[8] = (((sumRow[9] || 0) / sumRow[3]));
|
||
// percent cell all
|
||
sumRow[10] = (((sumRow[refAllCol] || 0) / sumRow[1]));
|
||
|
||
tableData.push(sumRow);
|
||
tableData = formatCols(tableData, tmpMapping);
|
||
|
||
// add rows
|
||
for(let i = 0; i < tableData.length; i++){
|
||
table.push(tableData[i]);
|
||
}
|
||
|
||
// -- Table format --------------------------------------------------------
|
||
for(let i = 0; i < numFormatCols.length; i++){
|
||
table.attrRange({row: [0], column: [numFormatCols[i], numFormatCols[i] + 1]}, {
|
||
align: 'right',
|
||
rightPadding: 1
|
||
});
|
||
}
|
||
|
||
for (let highLight in highLightRow) {
|
||
if (highLightRow.hasOwnProperty(highLight)){
|
||
for (let highLightSection in highLightRow[highLight]) {
|
||
for(let i = 0; i < highLightRow[highLight][highLightSection].length; i++){
|
||
let rowIdx = highLightRow[highLight][highLightSection][i];
|
||
|
||
let color = (highLight === 'success') ? 'green' : (highLight === 'warning') ? 'yellow' : 'red';
|
||
let colFrom = 0;
|
||
let colTo = 1;
|
||
switch(highLightSection){
|
||
case 'src_percent':
|
||
colTo = 1;
|
||
colTo = 4;
|
||
break;
|
||
case 'gzip_percent':
|
||
colFrom = 4;
|
||
colTo = 7;
|
||
break;
|
||
case 'brotli_percent':
|
||
colFrom = 7;
|
||
colTo = 10;
|
||
break;
|
||
case 'all_percent':
|
||
colFrom = 10;
|
||
colTo = 11;
|
||
break;
|
||
}
|
||
table.attrRange({row: [rowIdx + 1, rowIdx + 2], column: [colFrom, colTo]},{
|
||
color: color
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// -- Remove irrelevant columns -------------------------------------------
|
||
|
||
if(!CONF.JS.SOURCEMAPS){
|
||
table.removeColumn(12);
|
||
table.removeColumn(11);
|
||
|
||
if(!CONF.JS.BROTLI && !CONF.CSS.BROTLI && !CONF.JS.GZIP && !CONF.CSS.GZIP){
|
||
table.removeColumn(10);
|
||
|
||
table.removeColumn(9);
|
||
table.removeColumn(8);
|
||
table.removeColumn(7);
|
||
|
||
table.removeColumn(6);
|
||
table.removeColumn(5);
|
||
table.removeColumn(4);
|
||
|
||
if(!CONF.JS.UGLIFY){
|
||
table.removeColumn(3);
|
||
table.removeColumn(2);
|
||
}
|
||
}
|
||
}
|
||
|
||
console.log(table.toString());
|
||
|
||
// reset tracked files for next run e.g. watch change
|
||
trackedFiles = {};
|
||
};
|
||
|
||
// == clean up tasks ==================================================================================================
|
||
|
||
/**
|
||
* clean temp JS dest dir
|
||
*/
|
||
gulp.task('task:cleanJsDestBuild', () => del([PATH.JS.DEST_BUILD]));
|
||
|
||
/**
|
||
* clean JS dest dir
|
||
*/
|
||
gulp.task('task:cleanJsDest', () => del([PATH.JS.DEST + '/' + CONF.TAG]));
|
||
|
||
/**
|
||
* clean CSS dest dir
|
||
*/
|
||
gulp.task('task:cleanCssDest', () => del([PATH.ASSETS.DEST + '/css/' + CONF.TAG]));
|
||
|
||
/**
|
||
* clean IMG dest dir
|
||
*/
|
||
gulp.task('task:cleanImgDest', () => del([PATH.ASSETS.DEST + '/img/' + CONF.TAG]));
|
||
|
||
// == Dev tasks (code analyses) =======================================================================================
|
||
gulp.task('task:hintJS', () => {
|
||
return gulp.src([PATH.JS.SRC, '!' + PATH.JS.SRC_LIBS])
|
||
.pipe(jshint(__dirname + PATH.JS_HINT.CONF))
|
||
.pipe(jshint.reporter(stylish));
|
||
});
|
||
|
||
// == JS build tasks ==================================================================================================
|
||
|
||
/**
|
||
* concat/build JS files by modules
|
||
*/
|
||
gulp.task('task:concatJS', () => {
|
||
let modules = ['login', 'mappage', 'setup', 'admin', 'pnotify.loader', 'datatables.loader', 'summernote.loader'];
|
||
let srcModules = ['./js/app/*(' + modules.join('|') + ').js'];
|
||
|
||
return gulp.src(srcModules, {base: 'js'})
|
||
|
||
.pipe(gulpif(CONF.JS.SOURCEMAPS, sourcemaps.init()))
|
||
.pipe(requirejsOptimize(function(file){
|
||
|
||
return {
|
||
name: file.stem,
|
||
baseUrl: 'js',
|
||
mainConfigFile: './js/app.js',
|
||
optimize: 'none',
|
||
inlineText: false,
|
||
removeCombined: true,
|
||
preserveLicenseComments: false, // required for sourcemaps
|
||
findNestedDependencies: false,
|
||
include: ['text'],
|
||
// excludeShallow: ['app'],
|
||
// excludeShallow: ['./js/app.js'],
|
||
// exclude: ['app.js'],
|
||
// path: {
|
||
// pp: './../js/app' // the main config file will not be build
|
||
// },
|
||
onModuleBundleComplete: function(data){
|
||
// collect all combined js files
|
||
combinedJsFiles = [...new Set(combinedJsFiles.concat(data.included))];
|
||
}
|
||
};
|
||
}))
|
||
.pipe(bytediff.start())
|
||
.pipe(gulpif(CONF.JS.UGLIFY, minify(uglifyJsOptions).on('warnings', log)))
|
||
.pipe(gulpif(CONF.JS.SOURCEMAPS, sourcemaps.write('.', {includeContent: false, sourceRoot: '/js'}))) // prod (minify)
|
||
.pipe(bytediff.stop(data => {
|
||
trackFile(data, {src: 'startSize', src_percent: 'percent', uglify: 'endSize'});
|
||
return colors.green('Build concat file "' + data.fileName + '"');
|
||
}))
|
||
.pipe(gulp.dest(PATH.JS.DEST_BUILD));
|
||
});
|
||
|
||
/**
|
||
* build standalone JS files
|
||
*/
|
||
gulp.task('task:diffJS', () => {
|
||
return gulp.src(PATH.JS.SRC, {base: 'js', since: gulp.lastRun('task:diffJS')})
|
||
.pipe(filter(file => {
|
||
return combinedJsFiles.indexOf(slash(file.path)) < 0;
|
||
}))
|
||
.pipe(debug({title: 'Copy JS src: ', showFiles: false}))
|
||
.pipe(bytediff.start())
|
||
.pipe(gulpif(CONF.JS.SOURCEMAPS, sourcemaps.init()))
|
||
.pipe(gulpif(CONF.JS.UGLIFY, minify(uglifyJsOptions)))
|
||
.pipe(gulpif(CONF.JS.SOURCEMAPS, sourcemaps.write('.', {includeContent: false, sourceRoot: '/js'})))
|
||
.pipe(bytediff.stop(data => {
|
||
trackFile(data, {src: 'startSize', src_percent: 'percent', uglify: 'endSize'});
|
||
return colors.iconSuccess(`build → ${colors.magenta(data.fileName)}`);
|
||
}))
|
||
.pipe(gulp.dest(PATH.JS.DEST_BUILD, {overwrite: false}));
|
||
});
|
||
|
||
/**
|
||
* get assets filter options e.g. for gZip or Brotli assets
|
||
* @param compression
|
||
* @param fileExt
|
||
* @returns {{fileFilter, srcModules: string[]}}
|
||
*/
|
||
let getAssetFilterOptions = (compression, fileExt) => {
|
||
return {
|
||
srcModules: [
|
||
PATH.ASSETS.DEST +'/**/*.' + fileExt,
|
||
'!' + PATH.ASSETS.DEST + '/js/' + CONF.TAG + '{,/**/*}'
|
||
],
|
||
fileFilter: filter(file => CONF[fileExt.toUpperCase()][compression.toUpperCase()])
|
||
};
|
||
};
|
||
|
||
/**
|
||
* build gZip assets
|
||
* @param config
|
||
* @param taskName
|
||
* @returns {*}
|
||
*/
|
||
let gzipAssets = (config, taskName) => {
|
||
return gulp.src(config.srcModules, {base: 'public', since: gulp.lastRun(taskName)})
|
||
.pipe(config.fileFilter)
|
||
.pipe(debug({title: 'Gzip asses dest: ', showFiles: false}))
|
||
.pipe(bytediff.start())
|
||
.pipe(gzip(gZipOptions))
|
||
.pipe(bytediff.stop(data => {
|
||
trackFile(data, {gzipFile: 'fileName', gzip: 'endSize'});
|
||
if(fileExtension(data.fileName) === gZipOptions.extension){
|
||
return colors.green('Gzip generate "' + data.fileName + '"');
|
||
}else{
|
||
return colors.gray('Gzip skip "' + data.fileName + '". Size < ' + gZipOptions.threshold + ' (threehold)');
|
||
}
|
||
}))
|
||
.pipe(gulp.dest(PATH.ASSETS.DEST));
|
||
};
|
||
|
||
/**
|
||
* build Brotli assets
|
||
* @param config
|
||
* @param taskName
|
||
* @returns {*}
|
||
*/
|
||
let brotliAssets = (config, taskName) => {
|
||
return gulp.src(config.srcModules, {base: 'public', since: gulp.lastRun(taskName)})
|
||
.pipe(config.fileFilter)
|
||
.pipe(debug({title: 'Brotli asses dest: ', showFiles: false}))
|
||
.pipe(bytediff.start())
|
||
.pipe(brotli(brotliOptions))
|
||
.pipe(bytediff.stop(data => {
|
||
trackFile(data, {brotliFile: 'fileName', brotli: 'endSize'});
|
||
if(fileExtension(data.fileName) === brotliOptions.extension){
|
||
return colors.green('Brotli generate "' + data.fileName + '"');
|
||
}else{
|
||
return colors.gray('Brotli skip "' + data.fileName + '"');
|
||
}
|
||
}))
|
||
.pipe(gulp.dest(PATH.ASSETS.DEST));
|
||
};
|
||
|
||
gulp.task('task:gzipJsAssets', () => {
|
||
return gzipAssets(getAssetFilterOptions('gzip', 'js'), 'task:gzipJsAssets');
|
||
});
|
||
|
||
gulp.task('task:gzipCssAssets', () => {
|
||
return gzipAssets(getAssetFilterOptions('gzip', 'css'), 'task:gzipCssAssets');
|
||
});
|
||
|
||
gulp.task('task:brotliJsAssets', () => {
|
||
return brotliAssets(getAssetFilterOptions('brotli', 'js'), 'task:brotliJsAssets');
|
||
});
|
||
|
||
gulp.task('task:brotliCssAssets', () => {
|
||
return brotliAssets(getAssetFilterOptions('brotli', 'css'), 'task:brotliCssAssets');
|
||
});
|
||
|
||
/**
|
||
* rename "temp" build JS folder to final dest folder
|
||
* (keep "old" build data as long as possible in case of build failure)
|
||
*/
|
||
gulp.task('task:renameJsDest', () => {
|
||
let fileExt = ['js', 'map'].concat(compressionExt);
|
||
return gulp.src( PATH.JS.DEST_BUILD + '/**/*.{' + fileExt.join(',') + '}', {base: PATH.JS.DEST_BUILD, since: gulp.lastRun('task:renameJsDest')})
|
||
.pipe(debug({title: 'Rename JS dest: ', showFiles: false}))
|
||
.pipe(gulp.dest(PATH.JS.DEST_BUILD + '/../' + CONF.TAG));
|
||
});
|
||
|
||
// == CSS build tasks =================================================================================================
|
||
|
||
/**
|
||
* build CSS rom SASS files
|
||
* -> 1. node-sass
|
||
* 2. autoprefixer
|
||
*/
|
||
gulp.task('task:sass', () => {
|
||
return gulp.src(PATH.CSS.SRC)
|
||
.pipe(gulpif(CONF.CSS.SOURCEMAPS, sourcemaps.init()))
|
||
.pipe(sass(sassOptions).on('error', sass.logError))
|
||
.pipe(bytediff.start())
|
||
.pipe(bytediff.stop(data => {
|
||
trackFile(data, {src: 'startSize', src_percent: 'percent', uglify: 'endSize'});
|
||
return colors.iconSuccess(`build → ${colors.magenta(data.fileName)}`);
|
||
}))
|
||
.pipe(gulpif(CONF.CSS.SOURCEMAPS, sourcemaps.write('.', {includeContent: false, sourceRoot: '../../../sass'})))
|
||
.pipe(gulp.dest(PATH.ASSETS.DEST + '/css/' + CONF.TAG))
|
||
|
||
.pipe(filter(['!*.map']))
|
||
.pipe(gulpif(CONF.CSS.SOURCEMAPS, sourcemaps.init({loadMaps: true})))
|
||
.pipe(autoprefixer(autoprefixerOptions))
|
||
.pipe(bytediff.start())
|
||
.pipe(bytediff.stop(data => {
|
||
trackFile(data, {src: 'startSize', src_percent: 'percent', uglify: 'endSize'});
|
||
return colors.green('Autoprefix CSS file "' + data.fileName + '"');
|
||
}))
|
||
.pipe(gulpif(CONF.CSS.SOURCEMAPS, sourcemaps.write('.', {includeContent: false})))
|
||
.pipe(gulp.dest(PATH.ASSETS.DEST + '/css/' + CONF.TAG));
|
||
});
|
||
|
||
/**
|
||
* css-clean can be used to "optimize" generated CSS [optional]
|
||
*/
|
||
gulp.task('task:cleanCss', () => {
|
||
return gulp.src('public/css/' + CONF.TAG + '/*.css')
|
||
.pipe(gulpif(CONF.CSS.SOURCEMAPS, sourcemaps.init({loadMaps: true})))
|
||
.pipe(cleanCSS(cleanCssOptions))
|
||
.pipe(gulpif(CONF.CSS.SOURCEMAPS, sourcemaps.write('.', {includeContent: false})))
|
||
.pipe(gulp.dest(PATH.ASSETS.DEST + '/css/' + CONF.TAG));
|
||
});
|
||
|
||
// == Image build tasks ===============================================================================================
|
||
|
||
let imgWebpHandler = (config, taskName) => {
|
||
return gulp.src(config.src, {base: config.base, since: gulp.lastRun(taskName)})
|
||
.pipe(imagemin([imageminWebp(config.options)], {verbose: true}))
|
||
.pipe(rename({extname: '.webp'}))
|
||
.pipe(gulp.dest(PATH.ASSETS.DEST + '/img/' + CONF.TAG));
|
||
};
|
||
|
||
gulp.task('task:imgHeadToWEBP', () => {
|
||
return imgWebpHandler({
|
||
src: PATH.ASSETS.DEST + '/img/' + CONF.TAG + '/header/**/*.png',
|
||
base: PATH.ASSETS.DEST + '/img/' + CONF.TAG,
|
||
options: imageminWebpOptions
|
||
}, 'task:imgHeadToWEBP');
|
||
});
|
||
|
||
gulp.task('task:imgGalleryToWEBP', () => {
|
||
return imgWebpHandler({
|
||
src: PATH.ASSETS.DEST + '/img/' + CONF.TAG + '/gallery/**/*.jpg',
|
||
base: PATH.ASSETS.DEST + '/img/' + CONF.TAG,
|
||
options: Object.assign({}, imageminWebpOptions, {quality: 90}) // unfortunately src jpg´s are already high compressed
|
||
}, 'task:imgGalleryToWEBP');
|
||
});
|
||
|
||
let imgResizeHandler = (config, taskName) => {
|
||
return gulp.src(config.src, {base: 'img', since: gulp.lastRun(taskName)})
|
||
.pipe(imageResize(config.options))
|
||
.pipe(gulpif(config.rename, rename(config.rename)))
|
||
.pipe(gulp.dest(PATH.ASSETS.DEST + '/img/' + CONF.TAG));
|
||
};
|
||
|
||
let imgResizeHeaderTasks = [];
|
||
[480, 780, 1200, 1600, 3840].forEach(size => {
|
||
['png', 'jpg'].forEach(format => {
|
||
let taskName = `task:imgHead${size}${format}`;
|
||
gulp.task(taskName, () => {
|
||
return imgResizeHandler({
|
||
src: PATH.IMG.SRC_HEADER,
|
||
options: Object.assign({}, imgResizeOptions, {format: format, width: size}),
|
||
rename: {suffix: `-${size}`}
|
||
}, taskName);
|
||
});
|
||
imgResizeHeaderTasks.push(taskName);
|
||
});
|
||
});
|
||
|
||
gulp.task('task:imgHeadResize', gulp.series(
|
||
gulp.parallel(...imgResizeHeaderTasks),
|
||
'task:imgHeadToWEBP'
|
||
));
|
||
|
||
gulp.task('task:imgGalleryCopy', () => {
|
||
return gulp.src(PATH.IMG.SRC_GALLERY, {base: 'img', since: gulp.lastRun('task:imgGalleryCopy')})
|
||
.pipe(gulp.dest(PATH.ASSETS.DEST + '/img/' + CONF.TAG));
|
||
});
|
||
|
||
gulp.task('task:imgGallery', gulp.series(
|
||
'task:imgGalleryCopy',
|
||
'task:imgGalleryToWEBP'
|
||
));
|
||
|
||
gulp.task('task:imgRest', () => {
|
||
return gulp.src([
|
||
PATH.IMG.SRC,
|
||
`!${PATH.IMG.SRC_HEADER}`,
|
||
`!${PATH.IMG.SRC_GALLERY}`,
|
||
`!${PATH.IMG.SRC_SVG}`
|
||
], {base: 'img', since: gulp.lastRun('task:imgRest')})
|
||
.pipe(debug({title: 'Copy img "rest" dest: ', showFiles: false}))
|
||
.pipe(gulp.dest(PATH.ASSETS.DEST + '/img/' + CONF.TAG));
|
||
});
|
||
|
||
gulp.task('task:imgSVG', () => {
|
||
return gulp.src(PATH.IMG.SRC_SVG, {base: 'img', since: gulp.lastRun('task:imgSVG')})
|
||
.pipe(debug({title: 'Copy img SVG dest: ', showFiles: false}))
|
||
.pipe(gulp.dest(PATH.ASSETS.DEST + '/img/' + CONF.TAG));
|
||
});
|
||
|
||
// == Helper tasks ====================================================================================================
|
||
|
||
/**
|
||
* print Gulp help information
|
||
*/
|
||
gulp.task('task:printHelp', done => {
|
||
printHelp();
|
||
done();
|
||
});
|
||
|
||
/**
|
||
* print JS build task summary as table (e.g. show saved file size)
|
||
*/
|
||
gulp.task('task:printJsSummary', done => {
|
||
printJsSummary();
|
||
done();
|
||
});
|
||
|
||
/**
|
||
* print task configuration (e.g. CLI parameters)
|
||
*/
|
||
gulp.task('task:printConfig', done => {
|
||
let error = colors.danger;
|
||
let columnLength = Math.round(cliBoxLength / 2);
|
||
|
||
log(colors.divider('config'));
|
||
|
||
let configFlat = flatten(CONF);
|
||
for (let key in configFlat) {
|
||
if (configFlat.hasOwnProperty(key)){
|
||
let value = configFlat[key];
|
||
let success = colors.success;
|
||
switch(key){
|
||
case 'TASK': success = colors.primary; break;
|
||
}
|
||
// format value
|
||
value = padEnd((typeof value === 'undefined') ? 'undefined': value, columnLength);
|
||
log(
|
||
colors.yellow(padEnd(key, columnLength)),
|
||
configFlat[key] ? success(value) : error(value)
|
||
);
|
||
}
|
||
}
|
||
log(colors.divider(' '));
|
||
done();
|
||
});
|
||
|
||
/**
|
||
* check CLI parameters and task config
|
||
*/
|
||
gulp.task('task:checkConfig', done => {
|
||
if(!CONF.TAG){
|
||
printError(`Missing TAG version. Add param: ${colors.yellow('--tag')}`, `$ npm run gulp default -- --tag="${tagVersion}"`);
|
||
process.exit(0);
|
||
}
|
||
done();
|
||
});
|
||
|
||
/**
|
||
* configure "develop" task
|
||
*/
|
||
gulp.task('task:configDevelop',
|
||
gulp.series(
|
||
done => {
|
||
let CONF_DEVELOP = {
|
||
JS: {
|
||
UGLIFY: false,
|
||
SOURCEMAPS: false,
|
||
GZIP: false,
|
||
BROTLI: false
|
||
},
|
||
CSS: {
|
||
SOURCEMAPS: true,
|
||
GZIP: false,
|
||
BROTLI: false
|
||
},
|
||
IMG: {
|
||
ACTIVE: true
|
||
}
|
||
};
|
||
|
||
CONF = mergeConf(CONF, CONF_DEVELOP);
|
||
done();
|
||
},
|
||
'task:printConfig',
|
||
'task:checkConfig'
|
||
)
|
||
);
|
||
/**
|
||
* configure "production" task
|
||
*/
|
||
gulp.task('task:configProduction',
|
||
gulp.series(
|
||
done => {
|
||
let CONF_PRODUCTION = {
|
||
JS: {
|
||
UGLIFY: true,
|
||
SOURCEMAPS: true,
|
||
GZIP: true,
|
||
BROTLI: true
|
||
},
|
||
CSS: {
|
||
SOURCEMAPS: true,
|
||
GZIP: true,
|
||
BROTLI: true
|
||
},
|
||
IMG: {
|
||
ACTIVE: true
|
||
}
|
||
};
|
||
|
||
CONF = mergeConf(CONF, CONF_PRODUCTION);
|
||
done();
|
||
},
|
||
'task:printConfig',
|
||
'task:checkConfig'
|
||
)
|
||
);
|
||
|
||
/**
|
||
* configure "image" task (standalone)
|
||
*/
|
||
gulp.task('task:configImages',
|
||
gulp.series(
|
||
done => {
|
||
let CONF_IMAGES = {
|
||
IMG: {
|
||
ACTIVE: true
|
||
}
|
||
};
|
||
|
||
CONF = mergeConf(['TASK', 'TAG', 'IMG'].reduce((obj, key) => ({ ...obj, [key]: CONF[key] }), {}), CONF_IMAGES);
|
||
done();
|
||
},
|
||
'task:printConfig',
|
||
'task:checkConfig'
|
||
)
|
||
);
|
||
|
||
/**
|
||
* updates JS destination move to (final) dir
|
||
*/
|
||
gulp.task('task:updateJsDest', gulp.series(
|
||
gulp.parallel(
|
||
'task:gzipJsAssets',
|
||
'task:brotliJsAssets'
|
||
),
|
||
'task:renameJsDest',
|
||
// 'task:printJsSummary',
|
||
'task:cleanJsDestBuild'
|
||
));
|
||
|
||
/**
|
||
* build JS dest files (concat, uglify, sourcemaps)
|
||
*/
|
||
gulp.task('task:buildJs', gulp.series(
|
||
'task:concatJS',
|
||
'task:diffJS',
|
||
'task:cleanJsDest',
|
||
'task:updateJsDest'
|
||
));
|
||
|
||
/**
|
||
* build SCSS dest files
|
||
*/
|
||
gulp.task('task:buildCss', gulp.series(
|
||
'task:sass'
|
||
));
|
||
|
||
/**
|
||
* build IMG dest files
|
||
*/
|
||
gulp.task('task:buildImg', gulp.parallel(
|
||
'task:imgHeadResize',
|
||
'task:imgGallery',
|
||
'task:imgRest',
|
||
'task:imgSVG'
|
||
));
|
||
|
||
// == Notification tasks ==============================================================================================
|
||
|
||
/**
|
||
* JS Build done notification
|
||
*/
|
||
gulp.task('task:notifyJsDone', done => {
|
||
notifier.notify({
|
||
title: 'Done JS build',
|
||
message: 'JS build task finished',
|
||
icon: PATH.ASSETS.DEST + '/img/logo.png',
|
||
wait: false
|
||
});
|
||
done();
|
||
});
|
||
|
||
/**
|
||
* CSS Build done notification
|
||
*/
|
||
gulp.task('task:notifyCssDone', done => {
|
||
notifier.notify({
|
||
title: 'Done CSS build',
|
||
message: 'CSS build task finished',
|
||
icon: PATH.ASSETS.DEST + '/img/logo.png',
|
||
wait: false
|
||
});
|
||
done();
|
||
});
|
||
|
||
// == Watcher tasks ===================================================================================================
|
||
|
||
/**
|
||
* task for JS src file changes
|
||
*/
|
||
gulp.task(
|
||
'task:watchJsSrc',
|
||
gulp.series(
|
||
'task:hintJS',
|
||
'task:diffJS',
|
||
'task:updateJsDest',
|
||
'task:notifyJsDone'
|
||
)
|
||
);
|
||
|
||
/**
|
||
* task for JS src file changes
|
||
*/
|
||
gulp.task(
|
||
'task:watchCss',
|
||
gulp.series(
|
||
'task:buildCss',
|
||
// 'task:cleanCss',
|
||
gulp.parallel(
|
||
'task:gzipCssAssets',
|
||
'task:brotliCssAssets'
|
||
),
|
||
// 'task:printJsSummary',
|
||
'task:notifyCssDone'
|
||
)
|
||
);
|
||
|
||
gulp.task(
|
||
'task:watchImg',
|
||
gulp.series(
|
||
'task:buildImg'
|
||
)
|
||
);
|
||
|
||
/**
|
||
* watch files for changes
|
||
*/
|
||
|
||
gulp.task('task:setWatcherJs', () => gulp.watch(PATH.JS.SRC, gulp.series('task:watchJsSrc', 'task:printJsSummary')));
|
||
gulp.task('task:setWatcherCss', () => gulp.watch(PATH.CSS.SRC, gulp.series('task:watchCss', 'task:printJsSummary')));
|
||
gulp.task('task:setWatcherImg', () => gulp.watch(PATH.IMG.SRC, gulp.series('task:watchImg', 'task:printJsSummary')));
|
||
|
||
gulp.task('task:setWatcher',
|
||
gulp.parallel(
|
||
'task:setWatcherJs',
|
||
'task:setWatcherCss',
|
||
'task:setWatcherImg'
|
||
)
|
||
);
|
||
|
||
// == Default/Main tasks ==============================================================================================
|
||
|
||
gulp.task(
|
||
'help',
|
||
gulp.series(
|
||
'task:printHelp'
|
||
)
|
||
);
|
||
|
||
gulp.task(
|
||
'default',
|
||
gulp.series(
|
||
'task:configDevelop',
|
||
gulp.parallel(
|
||
gulp.series(
|
||
'task:cleanJsDestBuild',
|
||
'task:watchJsSrc'
|
||
),
|
||
gulp.series(
|
||
'task:cleanCssDest',
|
||
'task:watchCss'
|
||
),
|
||
gulp.series(
|
||
'task:cleanImgDest',
|
||
'task:watchImg'
|
||
)
|
||
),
|
||
'task:printJsSummary',
|
||
'task:setWatcher'
|
||
)
|
||
);
|
||
|
||
gulp.task(
|
||
'production',
|
||
gulp.series(
|
||
'task:configProduction',
|
||
gulp.parallel(
|
||
gulp.series(
|
||
'task:cleanJsDestBuild',
|
||
'task:buildJs'
|
||
),
|
||
gulp.series(
|
||
'task:cleanCssDest',
|
||
'task:watchCss'
|
||
),
|
||
gulp.series(
|
||
'task:cleanImgDest',
|
||
'task:watchImg'
|
||
)
|
||
),
|
||
'task:printJsSummary'
|
||
)
|
||
);
|
||
|
||
gulp.task(
|
||
'images',
|
||
gulp.series(
|
||
'task:configImages',
|
||
'task:cleanImgDest',
|
||
'task:watchImg',
|
||
//'task:printJsSummary'
|
||
)
|
||
);
|
||
|