<commit_before>const gulp = require('gulp');
const axios = require('axios');
const fs = require('fs');
const del = require('del');
const path = require('path');
const version = require('./package.json').version;

const nodejsLink86 = 'https://nodejs.org/download/release/v14.19.3/node-v14.19.3-x86.msi';
const nodejsLink64 = 'https://nodejs.org/download/release/v14.19.3/node-v14.19.3-x64.msi';

function download(url, file) {
    const directoryName = path.dirname(file);
    if (!fs.existsSync(directoryName)) {
        fs.mkdirSync(directoryName);
    }

    return axios(url, {responseType: 'arraybuffer'})
        .then(response => fs.writeFileSync(file, response.data));
}

gulp.task('0-clean', () => del(['tmp/**/*', 'build/.windows-ready/**/*', 'delivery/**/*']));
gulp.task('1-nodex86', () => download(nodejsLink86, __dirname + '/build/windows/nodejs/node.msi'));
gulp.task('2-nodex64', () => download(nodejsLink64, __dirname + '/build/windows/nodejs/node-x64.msi'));
gulp.task('3-0-replaceWindowsVersion', async () => {
    await checkFiles(['build/windows/nodejs/node.msi', 'build/windows/nodejs/node-x64.msi']);

    !fs.existsSync(__dirname + '/build/.windows-ready') && fs.mkdirSync(__dirname + '/build/.windows-ready');

    let iss = fs.readFileSync(__dirname + '/build/windows/ioBroker.iss').toString('utf8');
    iss = iss.replace('@@version', version)
    fs.writeFileSync(__dirname + '/build/.windows-ready/ioBroker.iss', iss);

    fs.writeFileSync(`${__dirname}/build/.windows-ready/createSetup.bat`, `"${__dirname}/build/windows/InnoSetup6/ISCC.exe" "${__dirname}/build/.windows-ready/ioBroker.iss"`);
});

gulp.task('3-1-copy', async () =>
    gulp.src([
        'build/windows/*.js',
        'build/windows/*.ico',
        'build/windows/*.bat',
        'build/windows/*.json',
        '!service_ioBroker.bat',
        '!_service_ioBroker.bat',
        '!*.iss'
    ])
        .pipe(gulp.dest('build/.windows-ready')));

gulp.task('3-2-copy-nodejs', async () =>
    gulp.src([
        'build/windows/nodejs/**/*',
    ])
        .pipe(gulp.dest('build/.windows-ready/nodejs')));

function _checkFiles(files, callback, index) {
    index = index || 0;

    if (files.find(file => !fs.existsSync(__dirname + '/' + file))) {
        if (index < 5) {
            setTimeout(() => _checkFiles(files, callback, index + 1), 3000);
        } else {
            callback('timeout ' + files.join(', '));
        }
    } else {
        callback(null);
    }
}

function checkFiles(files) {
    return new Promise((resolve, reject) => _checkFiles(files, error => error ? reject(error) : resolve()));
}

function runMSI() {
    return new Promise((resolve, reject) => {
        checkFiles([
            'build/.windows-ready/ioBroker.iss',
            'build/.windows-ready/nodejs/node.msi',
            'build/.windows-ready/nodejs/node-x64.msi',
            'build/.windows-ready/ioBroker.ico'
        ])
            .then(() => {
                // Install node modules
                const cwd = __dirname.replace(/\\/g, '/') + '/build/windows/';

                const cmd = `"${__dirname}\\build\\windows\\InnoSetup6\\ISCC.exe" "${__dirname}\\build\\.windows-ready\\ioBroker.iss"`;
                console.log(`"${cmd} in ${cwd}`);

                // System call used for update of js-controller itself,
                // because during installation npm packet will be deleted too, but some files must be loaded even during the installation process.
                const exec = require('child_process').exec;
                const child = exec(cmd, {cwd});

                child.stderr.pipe(process.stderr);
                child.stdout.pipe(process.stdout);

                child.on('exit', (code /* , signal */) => {
                    // code 1 is strange error that cannot be explained. Everything is installed but error :(
                    if (code && code !== 1) {
                        reject(new Error('Cannot install: ' + code));
                    } else {
                        console.log(`"${cmd} in ${cwd} finished.`);
                        // command succeeded
                        resolve();
                    }
                });
            })
            .catch(error => reject(error));
    });
}

function signExe() {
    return new Promise((resolve, reject) => {
        checkFiles([`delivery/ioBrokerInstaller.exe`])
            .then(() => {
                // Install node modules
                if (process.env.CERT_FILE) {
                    fs.writeFileSync(__dirname + '/ioBrokerCodeSigningCertificate.pfx', Buffer.from(process.env.CERT_FILE, 'base64'));
                    console.log('Saved ' + Buffer.from(process.env.CERT_FILE, 'base64').length  + ' bytes in certificate');
                } else if (fs.existsSync(__dirname + '/ioBrokerCodeSigningCertificate.pfx') && !fs.existsSync(__dirname + '/ioBrokerCodeSigningCertificate.base64.txt')) {
                    fs.writeFileSync(__dirname + '/ioBrokerCodeSigningCertificate.base64.txt', fs.readFileSync(__dirname + '/ioBrokerCodeSigningCertificate.pfx').toString('base64'));
                } else if (!fs.existsSync(__dirname + '/ioBrokerCodeSigningCertificate.pfx')) {
                    throw new Error('NO cert file found');
                }

                const cmd = `${__dirname}\\build\\windows\\ezsign\\EZSignIt.exe ` +
                    `/sn "${__dirname}\\delivery\\ioBrokerInstaller.exe" ` +
                    `/f "${__dirname}\\ioBrokerCodeSigningCertificate.pfx" ` +
                    `/p ${process.env.CERT_PASSWORD} ` +
                    `/fd sha256 ` +
                    `/nse ` +
                    `/d "ioBroker windows installer" ` +
                    `/trs2 "http://timestamp.comodoca.com/?td=sha256"`;

                console.log(`"${cmd.replace(process.env.CERT_PASSWORD, '*****' + ((process.env.CERT_PASSWORD || '').lenght + 5))}`);

                // System call used for update of js-controller itself,
                // because during installation npm packet will be deleted too, but some files must be loaded even during the installation process.
                const exec = require('child_process').exec;
                const child = exec(cmd);

                child.stderr.pipe(process.stderr);
                child.stdout.pipe(process.stdout);

                child.on('exit', (code /* , signal */) => {
                    // code 1 is strange error that cannot be explained. Everything is installed but error :(
                    if (code) {
                        const exitCodes = [
                            '0 = Success',
                            '1 = Invalid command-line or general program error',
                            '2 = Certificate password is incorrect',
                            '3 = Certificate could not be added / General certificate signing error',
                            '4 = Timestamp not added / Timestamp server-related error',
                            '5 = Signature Validation failed',
                        ];

                        reject(new Error(`Cannot sign: ${exitCodes[code]} (${code})`));
                    } else {
                        console.log(`"${cmd} finished.`);
                        // command succeeded
                        resolve();
                    }
                });
            })
            .catch(error => reject(error));
    });
}

gulp.task('3-3-runMSI', runMSI);
gulp.task('3-4-rename', async () => {
    if (fs.existsSync(`${__dirname}/delivery/ioBrokerInstaller.${version}.exe`) && fs.existsSync(`${__dirname}/delivery/ioBrokerInstaller.exe`)) {
        fs.unlinkSync(`${__dirname}/delivery/ioBrokerInstaller.exe`);
    }
    fs.renameSync(`${__dirname}/delivery/ioBrokerInstaller.${version}.exe`, `${__dirname}/delivery/ioBrokerInstaller.exe`);
});
gulp.task('3-5-signExe-manually', signExe);

if (/^win/.test(process.platform)) {
    gulp.task('3-windows-msi', gulp.series(['3-0-replaceWindowsVersion', '3-1-copy', '3-2-copy-nodejs', '3-3-runMSI', '3-4-rename']));
} else {
    gulp.task('3-windows-msi', async () => console.warn('Cannot create windows setup, while host is not windows'));
}

gulp.task('default',  gulp.series(['0-clean', '1-nodex86', '2-nodex64', '3-windows-msi']));
<commit_msg>Added signing<commit_after>const gulp = require('gulp');
const axios = require('axios');
const fs = require('fs');
const del = require('del');
const path = require('path');
const version = require('./package.json').version;

const nodejsLink86 = 'https://nodejs.org/download/release/v14.19.3/node-v14.19.3-x86.msi';
const nodejsLink64 = 'https://nodejs.org/download/release/v14.19.3/node-v14.19.3-x64.msi';

function download(url, file) {
    const directoryName = path.dirname(file);
    if (!fs.existsSync(directoryName)) {
        fs.mkdirSync(directoryName);
    }

    return axios(url, {responseType: 'arraybuffer'})
        .then(response => fs.writeFileSync(file, response.data));
}

gulp.task('0-clean', () => del(['tmp/**/*', 'build/.windows-ready/**/*', 'delivery/**/*']));
gulp.task('1-nodex86', () => download(nodejsLink86, __dirname + '/build/windows/nodejs/node.msi'));
gulp.task('2-nodex64', () => download(nodejsLink64, __dirname + '/build/windows/nodejs/node-x64.msi'));
gulp.task('3-0-replaceWindowsVersion', async () => {
    await checkFiles(['build/windows/nodejs/node.msi', 'build/windows/nodejs/node-x64.msi']);

    !fs.existsSync(__dirname + '/build/.windows-ready') && fs.mkdirSync(__dirname + '/build/.windows-ready');

    let iss = fs.readFileSync(__dirname + '/build/windows/ioBroker.iss').toString('utf8');
    iss = iss.replace('@@version', version)
    fs.writeFileSync(__dirname + '/build/.windows-ready/ioBroker.iss', iss);

    fs.writeFileSync(`${__dirname}/build/.windows-ready/createSetup.bat`, `"${__dirname}/build/windows/InnoSetup6/ISCC.exe" "${__dirname}/build/.windows-ready/ioBroker.iss"`);
});

gulp.task('3-1-copy', async () =>
    gulp.src([
        'build/windows/*.js',
        'build/windows/*.ico',
        'build/windows/*.bat',
        'build/windows/*.json',
        '!service_ioBroker.bat',
        '!_service_ioBroker.bat',
        '!*.iss'
    ])
        .pipe(gulp.dest('build/.windows-ready')));

gulp.task('3-2-copy-nodejs', async () =>
    gulp.src([
        'build/windows/nodejs/**/*',
    ])
        .pipe(gulp.dest('build/.windows-ready/nodejs')));

function _checkFiles(files, callback, index) {
    index = index || 0;

    if (files.find(file => !fs.existsSync(__dirname + '/' + file))) {
        if (index < 5) {
            setTimeout(() => _checkFiles(files, callback, index + 1), 3000);
        } else {
            callback('timeout ' + files.join(', '));
        }
    } else {
        callback(null);
    }
}

function checkFiles(files) {
    return new Promise((resolve, reject) => _checkFiles(files, error => error ? reject(error) : resolve()));
}

function runMSI() {
    return new Promise((resolve, reject) => {
        checkFiles([
            'build/.windows-ready/ioBroker.iss',
            'build/.windows-ready/nodejs/node.msi',
            'build/.windows-ready/nodejs/node-x64.msi',
            'build/.windows-ready/ioBroker.ico'
        ])
            .then(() => {
                // Install node modules
                const cwd = __dirname.replace(/\\/g, '/') + '/build/windows/';

                const cmd = `"${__dirname}\\build\\windows\\InnoSetup6\\ISCC.exe" "${__dirname}\\build\\.windows-ready\\ioBroker.iss"`;
                console.log(`"${cmd} in ${cwd}`);

                // System call used for update of js-controller itself,
                // because during installation npm packet will be deleted too, but some files must be loaded even during the installation process.
                const exec = require('child_process').exec;
                const child = exec(cmd, {cwd});

                child.stderr.pipe(process.stderr);
                child.stdout.pipe(process.stdout);

                child.on('exit', (code /* , signal */) => {
                    // code 1 is strange error that cannot be explained. Everything is installed but error :(
                    if (code && code !== 1) {
                        reject(new Error('Cannot install: ' + code));
                    } else {
                        console.log(`"${cmd} in ${cwd} finished.`);
                        // command succeeded
                        resolve();
                    }
                });
            })
            .catch(error => reject(error));
    });
}

function signExe() {
    return new Promise((resolve, reject) => {
        checkFiles([`delivery/ioBrokerInstaller.exe`])
            .then(() => {
                // Install node modules
                if (process.env.CERT_FILE) {
                    fs.writeFileSync(__dirname + '/ioBrokerCodeSigningCertificate.pfx', Buffer.from(process.env.CERT_FILE, 'base64'));
                    console.log('Saved ' + Buffer.from(process.env.CERT_FILE, 'base64').length  + ' bytes in certificate');
                } else if (fs.existsSync(__dirname + '/ioBrokerCodeSigningCertificate.pfx') && !fs.existsSync(__dirname + '/ioBrokerCodeSigningCertificate.base64.txt')) {
                    fs.writeFileSync(__dirname + '/ioBrokerCodeSigningCertificate.base64.txt', fs.readFileSync(__dirname + '/ioBrokerCodeSigningCertificate.pfx').toString('base64'));
                } else if (!fs.existsSync(__dirname + '/ioBrokerCodeSigningCertificate.pfx')) {
                    throw new Error('NO cert file found');
                }

                const cmd = `${__dirname}\\build\\windows\\ezsign\\EZSignIt.exe ` +
                    `/sn "${__dirname}\\delivery\\ioBrokerInstaller.exe" ` +
                    `/f "${__dirname}\\ioBrokerCodeSigningCertificate.pfx" ` +
                    `/p ${process.env.CERT_PASSWORD} ` +
                    `/fd sha256 ` +
                    `/nse ` +
                    `/d "ioBroker windows installer" ` +
                    `/trs2 "http://timestamp.comodoca.com/?td=sha256"`;

                console.log(`"${cmd.replace(process.env.CERT_PASSWORD, '*****' + ((process.env.CERT_PASSWORD || '').length + 5))}`);

                // System call used for update of js-controller itself,
                // because during installation npm packet will be deleted too, but some files must be loaded even during the installation process.
                const exec = require('child_process').exec;
                const child = exec(cmd);

                child.stderr.pipe(process.stderr);
                child.stdout.pipe(process.stdout);

                child.on('exit', (code /* , signal */) => {
                    // code 1 is strange error that cannot be explained. Everything is installed but error :(
                    if (code) {
                        const exitCodes = [
                            '0 = Success',
                            '1 = Invalid command-line or general program error',
                            '2 = Certificate password is incorrect',
                            '3 = Certificate could not be added / General certificate signing error',
                            '4 = Timestamp not added / Timestamp server-related error',
                            '5 = Signature Validation failed',
                        ];

                        reject(new Error(`Cannot sign: ${exitCodes[code]} (${code})`));
                    } else {
                        console.log(`"${cmd} finished.`);
                        // command succeeded
                        resolve();
                    }
                });
            })
            .catch(error => reject(error));
    });
}

gulp.task('3-3-runMSI', runMSI);
gulp.task('3-4-rename', async () => {
    if (fs.existsSync(`${__dirname}/delivery/ioBrokerInstaller.${version}.exe`) && fs.existsSync(`${__dirname}/delivery/ioBrokerInstaller.exe`)) {
        fs.unlinkSync(`${__dirname}/delivery/ioBrokerInstaller.exe`);
    }
    fs.renameSync(`${__dirname}/delivery/ioBrokerInstaller.${version}.exe`, `${__dirname}/delivery/ioBrokerInstaller.exe`);
});
gulp.task('3-5-signExe-manually', signExe);

if (/^win/.test(process.platform)) {
    gulp.task('3-windows-msi', gulp.series(['3-0-replaceWindowsVersion', '3-1-copy', '3-2-copy-nodejs', '3-3-runMSI', '3-4-rename']));
} else {
    gulp.task('3-windows-msi', async () => console.warn('Cannot create windows setup, while host is not windows'));
}

gulp.task('default',  gulp.series(['0-clean', '1-nodex86', '2-nodex64', '3-windows-msi']));
<|endoftext|>


<commit_before>// This file is a part of Grid - Copyright (c) 2016 Vladimír Macháček | For the full copyright and license information, please view the file license.md that was distributed with this source code.

"use strict";

const

	// Modules
	autoprefixer  = require('gulp-autoprefixer'),
	cleanCss = require('gulp-clean-css'),
	gulp  = require('gulp'),
	pipe_if = require('gulp-if'),
	plumber = require('gulp-plumber'),
	rename = require('gulp-rename'),
	sass = require('gulp-sass'),
	sourcemaps = require('gulp-sourcemaps'),
	fs = require('fs'),

	// Setup
	directories = {
		dist: 'dist',
		src: 'src',
		tests: 'tests',
		watch: 'src/**/*.sass',
		sourcemaps: '/',
		ampTestParts: 'tests/amp-test-parts'
	},

	targetFiles = {
		grid: directories.src + '/grid.sass',
		gridMobile: directories.src + '/grid.mobile.sass',
		testsHelpers: directories.tests + '/helpers/tests-helpers.sass'
	},

	autoprefixerPrefixes = {
		mobile: ['iOS >= 6', 'Android >= 4.1', 'UCAndroid >= 11', 'ExplorerMobile >= 11'],
		desktop: ['> 5%', 'last 2 versions', 'IE 10'],
	},

	autoprefixerSettings = autoprefixerPrefixes.desktop.concat(autoprefixerPrefixes.desktop),
	minVersionSuffix = 'min',
	defaultFileType = 'css',

	tasks = {
		grid: null,

		gridMin: {
			minify: true
		},

		gridDev: {
			noPrefixes: true,
			cssMap: false,
			fileType: 'scss',
		},

		gridMobile: {
			targetFile: targetFiles.gridMobile,
			prefixes:  autoprefixerPrefixes.mobile
		},

		gridMobileMin: {
			targetFile: targetFiles.gridMobile,
			minify: true
		},

		gridMobileDev: {
			targetFile: targetFiles.gridMobile,
			noPrefixes: true,
			cssMap: false,
			fileType: 'scss',
		},

		gridTestable: {
			targetFile: targetFiles.grid,
			prefixes: autoprefixerSettings.concat(['Safari >= 4']),
			outputDir: directories.tests,
			cssMap: false
		},

		testsHelpers: {
			minify: true,
			targetFile: targetFiles.testsHelpers,
			outputDir: directories.tests,
			cssMap: false
		}
	};

// Watch and default tasks
gulp
	.task('compile', compile)

	.task('watch', function() {
		gulp.watch(directories.watch, ['compile']);
	})

	.task('default', ['compile', 'watch'])

	.task ('generate-amp-test-file', generateAmpTestFile);

/**
 * @param {object} settings
 * @param {string} settingsOption
 * @param {string} settingsType
 * @returns {boolean}
 */
function settingsExist(settings, settingsOption, settingsType) {
	return settingsOption in settings && typeof settings[settingsOption] === settingsType;
}

function compile() {

	for (var task in tasks) {
		var minify = false,
			targetFile = targetFiles.grid,
			prefixes = autoprefixerSettings,
			prefixesAllowed = true,
			fileType = defaultFileType,
			fileSuffix = minVersionSuffix,
			outputDir = directories.dist,
			cssMap = true,

			taskSettings = tasks[task];

		if (taskSettings && typeof taskSettings === 'object') {
			if (settingsExist(taskSettings, 'minify', 'boolean')) minify = taskSettings.minify;
			if (settingsExist(taskSettings, 'targetFile', 'string')) targetFile = taskSettings.targetFile;
			if (settingsExist(taskSettings, 'cssMap', 'boolean')) cssMap = taskSettings.cssMap;
			if (settingsExist(taskSettings, 'fileType', 'string')) fileType = taskSettings.fileType;
			if (settingsExist(taskSettings, 'noPrefixes', 'boolean')) prefixesAllowed = ! taskSettings.noPrefixes;
			if (settingsExist(taskSettings, 'prefixes', 'object')) prefixes = taskSettings.prefixes;
			if (settingsExist(taskSettings, 'outputDir', 'string')) outputDir = taskSettings.outputDir;
		}

		gulp.src(targetFile)
			.pipe(plumber())
			.pipe(sourcemaps.init())
			.pipe(sass())
			.pipe(pipe_if(prefixesAllowed, autoprefixer({
					browsers: prefixes
				})
			))
			.pipe(pipe_if(minify, cleanCss()))
			.pipe(pipe_if(minify, rename({
					suffix: '.' + fileSuffix
				})
			))
			.pipe(rename({
					extname: '.' + fileType
				})
			)
			.pipe(pipe_if(cssMap, sourcemaps.write(directories.sourcemaps)))
			.pipe(gulp.dest(outputDir));
	}

}

function generateAmpTestFile() {
	var
		partBeforeStyle = fs.readFileSync(directories.ampTestParts + "/before-style.txt", 'utf8'),
		partAfterStyle = fs.readFileSync(directories.ampTestParts + "/after-style.txt", 'utf8'),
		gridMobileCss = fs.readFileSync(directories.dist + "/grid.mobile.css", 'utf8'),
		fileContent = partBeforeStyle + gridMobileCss + partAfterStyle;

	fs.writeFile(directories.tests + '/amp-test.html', fileContent, function () {
		console.log ('Amp test file successfully generated')
	});
}
<commit_msg>- v2.0 - Updated gulpfile<commit_after>// This file is a part of Grid - Copyright (c) 2016 Vladimír Macháček | For the full copyright and license information, please view the file license.md that was distributed with this source code.

"use strict";

const

	// Modules
	autoprefixer  = require('gulp-autoprefixer'),
	cleanCss = require('gulp-clean-css'),
	gulp  = require('gulp'),
	pipe_if = require('gulp-if'),
	plumber = require('gulp-plumber'),
	rename = require('gulp-rename'),
	sass = require('gulp-sass'),
	sourcemaps = require('gulp-sourcemaps'),
	fs = require('fs'),

	// Setup
	directories = {
		dist: './dist',
		src: './src',
		tests: './tests',
		watch: './src/**/*.sass',
		sourcemaps: './',
		ampTestParts: './tests/amp-test-parts'
	},

	targetFiles = {
		grid: directories.src + '/grid.sass',
		gridMobile: directories.src + '/grid.mobile.sass',
		testsHelpers: directories.tests + '/helpers/tests-helpers.sass'
	},

	autoprefixerPrefixes = {
		mobile: ['iOS >= 6', 'Android >= 4.1', 'UCAndroid >= 11', 'ExplorerMobile >= 11'],
		desktop: ['> 5%', 'last 2 versions', 'IE 10'],
	},

	autoprefixerSettings = autoprefixerPrefixes.desktop.concat(autoprefixerPrefixes.desktop),
	minVersionSuffix = 'min',
	defaultFileType = 'css',

	tasks = {
		grid: null,

		gridMin: {
			minify: true
		},

		gridDev: {
			noPrefixes: true,
			cssMap: false,
			fileType: 'scss',
		},

		gridMobile: {
			targetFile: targetFiles.gridMobile,
			prefixes:  autoprefixerPrefixes.mobile
		},

		gridMobileMin: {
			targetFile: targetFiles.gridMobile,
			minify: true
		},

		gridMobileDev: {
			targetFile: targetFiles.gridMobile,
			noPrefixes: true,
			cssMap: false,
			fileType: 'scss',
		},

		gridTestable: {
			targetFile: targetFiles.grid,
			prefixes: autoprefixerSettings.concat(['Safari >= 4']),
			outputDir: directories.tests,
			cssMap: false
		},

		testsHelpers: {
			minify: true,
			targetFile: targetFiles.testsHelpers,
			outputDir: directories.tests,
			cssMap: false
		}
	};

// Watch and default tasks
gulp
	.task('compile', compile)

	.task('watch', function() {
		gulp.watch(directories.watch, ['compile']);
	})

	.task('default', ['compile', 'watch'])

	.task ('generate-amp-test-file', generateAmpTestFile);

/**
 * @param {object} settings
 * @param {string} settingsOption
 * @param {string} settingsType
 * @returns {boolean}
 */
function settingsExist(settings, settingsOption, settingsType) {
	return settingsOption in settings && typeof settings[settingsOption] === settingsType;
}

function compile() {

	for (var task in tasks) {
		var minify = false,
			targetFile = targetFiles.grid,
			prefixes = autoprefixerSettings,
			prefixesAllowed = true,
			fileType = defaultFileType,
			fileSuffix = minVersionSuffix,
			outputDir = directories.dist,
			cssMap = true,

			taskSettings = tasks[task];

		if (taskSettings && typeof taskSettings === 'object') {
			if (settingsExist(taskSettings, 'minify', 'boolean')) minify = taskSettings.minify;
			if (settingsExist(taskSettings, 'targetFile', 'string')) targetFile = taskSettings.targetFile;
			if (settingsExist(taskSettings, 'cssMap', 'boolean')) cssMap = taskSettings.cssMap;
			if (settingsExist(taskSettings, 'fileType', 'string')) fileType = taskSettings.fileType;
			if (settingsExist(taskSettings, 'noPrefixes', 'boolean')) prefixesAllowed = ! taskSettings.noPrefixes;
			if (settingsExist(taskSettings, 'prefixes', 'object')) prefixes = taskSettings.prefixes;
			if (settingsExist(taskSettings, 'outputDir', 'string')) outputDir = taskSettings.outputDir;
		}

		gulp.src(targetFile)
			.pipe(plumber())
			.pipe(sourcemaps.init())
			.pipe(sass())
			.pipe(pipe_if(prefixesAllowed, autoprefixer({
					browsers: prefixes
				})
			))
			.pipe(pipe_if(minify, cleanCss()))
			.pipe(pipe_if(minify, rename({
					suffix: '.' + fileSuffix
				})
			))
			.pipe(rename({
					extname: '.' + fileType
				})
			)
			.pipe(pipe_if(cssMap, sourcemaps.write(directories.sourcemaps)))
			.pipe(gulp.dest(outputDir));
	}

}

function generateAmpTestFile() {
	console.log(__dirname);
	var
		partBeforeStyle = fs.readFileSync(directories.ampTestParts + "/before-style.txt", 'utf8'),
		partAfterStyle = fs.readFileSync(directories.ampTestParts + "/after-style.txt", 'utf8'),
		gridMobileCss = fs.readFileSync(directories.dist + "/grid.mobile.css", 'utf8'),
		fileContent = partBeforeStyle + gridMobileCss + partAfterStyle;

	fs.writeFile(directories.tests + '/amp-test.html', fileContent, function () {
		console.log ('Amp test file successfully generated')
	});
}
<|endoftext|>


<commit_before>var _ = require('lodash');
var $ = require('gulp-load-plugins')({lazy: true});
var args = require('yargs').argv;
var browserSync = require('browser-sync');
var config = require('./gulp.config')();
var del = require('del');
var gulp = require('gulp');
var path = require('path');
var port = process.env.PORT || config.defaultPort;

gulp.task('help', $.taskListing);

gulp.task('default', ['help']);

gulp.task('vet', function() {
    log('Analyzing source with JSHint and JSCS.');

    return gulp
        .src(config.alljs)
        .pipe($.if(args.verbose, $.print()))
        .pipe($.jscs())
        .pipe($.jshint())
        .pipe($.jshint.reporter('jshint-stylish', {verbose: true}))
        .pipe($.jshint.reporter('fail'));
});

gulp.task('styles', ['clean-styles'], function() {
    log('Compiling Less --> CSS');

    return gulp
        .src(config.less)
        // plumber is a nice plug-in to handle errors in our stream.
        // it does something similar to the errorLogger created below.
        // errorLogger actually terminates the stream, whereas plumber doesn't, but does display error information
        .pipe($.plumber())
        .pipe($.less())
        //.on('error', errorLogger)
        .pipe($.autoprefixer({browsers: ['last 2 version', '> 5%']}))
        .pipe(gulp.dest(config.temp));
});

gulp.task('fonts', ['clean-fonts'], function() {
    log('Copying fonts');

    return gulp.src(config.fonts)
        .pipe(gulp.dest(config.build + 'fonts'));
});

gulp.task('images', ['clean-images'], function() {
    log('Copying and compressing the images');

    return gulp.src(config.images)
        .pipe($.imagemin({optimizationLevel: 4}))
        .pipe(gulp.dest(config.build + 'images'));
});

/*
 * while below can be used as a precursor to other task, no stream is being returned,
 * task depending on this won't neccessarily wait until this task completes before running
 * therefor need to update to similar function below that takes a callback function
 * i.e. in clean below would need to ensure the callback function 'done' gets called after
 * del, in this case though, del takes a callback function and will invoke it for us
*/
// gulp.task('clean-styles', function() {
//     var files = config.temp + '**/*.css';
//     clean(files);
// });

gulp.task('clean', function(done) {
    var delconfig = [].concat(config.build, config.temp);
    log('Cleaning: ' + $.util.colors.green(delconfig));
    del(delconfig, done);
});

gulp.task('clean-fonts', function(done) {
    clean(config.build + 'fonts/**/*.*', done);
});

gulp.task('clean-images', function(done) {
    clean(config.build + 'images/**/*.*', done);
});

gulp.task('clean-styles', function(done) {
    clean(config.temp + '**/*.css', done);
});

gulp.task('clean-code', function(done) {
    var files = [].concat(
        config.temp + '**/*.js',
        config.build + '**/*.html',
        config.build + 'js/**/*.js'
    );

    clean(files, done);
});

gulp.task('less-watcher', function() {
    gulp.watch([config.less], ['styles']);
});

//SPECIFIC TO ANGULAR
gulp.task('templatecache', ['clean-code'], function() {
    log('Creating AngularJS $templateCache');

    return gulp.src(config.htmltemplates)
        .pipe($.minifyHtml({empty: true}))
        .pipe($.angularTemplatecache(
            config.templateCache.file,
            config.templateCache.options
        ))
        .pipe(gulp.dest(config.temp));
});

gulp.task('wiredep', function() {
    log('Wire up the bower css, js and our app.js into the html');

    var options = config.getWiredepDefaultOptions();
    var wiredep = require('wiredep').stream;

    return gulp
        .src(config.index) // get index.html
        .pipe(wiredep(options)) // call wiredep, looks in bower.json at runtime dependencies('dependencies' --> not dev)
                                // looks in bower_components/<directory>
                                // main property and it's dependencies for each component
                                // and then injects all of them into the index.html
        .pipe($.inject(gulp.src(config.js)))
        .pipe(gulp.dest(config.client));
});

gulp.task('inject', ['wiredep', 'styles', 'templatecache'], function() {
    log('Wire up the bower css, js and our app.js into the html');

    return gulp
        .src(config.index) // get index.html
        .pipe($.inject(gulp.src(config.css)))
        .pipe(gulp.dest(config.client));
});

gulp.task('build', ['optimize', 'images', 'fonts'], function() {
    log('Building everything');

    var msg = {
        title: 'gulp build',
        subtitle: 'Deployed to the build folder.',
        message: 'Running `gulp serve-build`.'
    };

    del(config.temp);
    log(msg);
    notify(msg);
});

gulp.task('serve-specs', ['build-specs'], function(done) {
    log('run the spec runner');
    serve(true /* isDev */, true /* isSpecRunner */);
    done();
})

gulp.task('build-specs', ['templatecache'], function() {
    log('building the spec runner');

    var wiredep = require('wiredep').stream;
    var options = config.getWiredepDefaultOptions();
    var specs = config.specs;

    options.devDependencies = true;

    if (args.startServers) {
        specs = [].concat(specs, config.serverIntegrationSpecs);
    }

    return gulp.src(config.specRunner)
        .pipe(wiredep(options))
        .pipe($.inject(gulp.src(config.testlibraries), {name:'inject:testlibraries', read: false}))
        .pipe($.inject(gulp.src(config.js)))
        .pipe($.inject(gulp.src(config.specHelpers), {name:'inject:spechelpers', read: false}))
        .pipe($.inject(gulp.src(specs), {name:'inject:specs', read: false}))
        .pipe($.inject(gulp.src(config.temp + config.templateCache.file), {name:'inject:templates', read: false}))
        .pipe(gulp.dest(config.client));
});

gulp.task('optimize', ['inject', 'test'], function() {
    log('Optimizing the javascript, css, html');

    var assets = $.useref.assets({searchPath: './'});
    var templateCache = config.temp + config.templateCache.file;
    var cssFilter = $.filter('**/*.css', {restore: true});
    var jsLibFilter = $.filter('**/' + config.optimized.lib, {restore: true});
    var jsAppFilter = $.filter('**/' + config.optimized.app, {restore: true});

    return gulp.src(config.index)
        .pipe($.plumber())
        .pipe($.inject(
            gulp.src(templateCache, {read: false}), {
            starttag: '<!-- inject:templates:js -->'
        }))
        .pipe(assets)
        .pipe(cssFilter) // filter to css
        .pipe($.csso()) // csso
        .pipe(cssFilter.restore) // remove css filter

        .pipe(jsLibFilter) // filter to lib js
        .pipe($.uglify()) // uglify
        .pipe(jsLibFilter.restore) // remove js filter

        //ANGULAR specific
        .pipe(jsAppFilter) // filter to app js (custom angular js files)
        .pipe($.ngAnnotate())
        .pipe($.uglify()) // uglify
        .pipe(jsAppFilter.restore) // remove js filter

        .pipe($.rev()) // adds hash to js/css file names if contents have changed
        .pipe(assets.restore())
        .pipe($.useref())
        .pipe($.revReplace()) // replaces references to js/css file names in files with hash appended names
        .pipe(gulp.dest(config.build))
        .pipe($.rev.manifest())
        .pipe(gulp.dest(config.build));
});

/*
 * Bump the version
 * --type=pre will bump the prerelease version *.*.*-x
 * --type=patch or no flag will bump the patch version *.*.x
 * --type=minor will bump the minor version *.x.*
 * --type=major will bump the major version x.*.*
 * --version=1.2.3 will bump to a specific version and ignore other flags
 */
gulp.task('bump', function() {
    var msg = 'Bumping versions';
    var type = args.type;
    var version = args.version;
    var options = {};

    if (version) {
        options.version = version;
        msg += ' to ' + version;
    } else {
        options.type = type;
        msg += ' for a ' + type;
    }

    log(msg);

    return gulp.src(config.packages)
        .pipe($.print())
        .pipe($.bump(options))
        .pipe(gulp.dest(config.root));
});

// SPECIFIC TO NODE: detects changes to files on server and can perform actions on events
gulp.task('serve-build', ['build'], function() {
    serve(false /* isDev */);
});

gulp.task('serve-dev', ['inject'], function() {
    serve(true /* isDev */);
});

gulp.task('test', ['vet', 'templatecache'], function(done) {
    startTests(true /* singleRun */, done);
});

gulp.task('autotest', ['vet', 'templatecache'], function(done) {
    startTests(false /* singleRun */, done);
});

//////////////////////////////////////////////////////////////////
function notify(options) {
    var notifier = require('node-notifier');
    var notifyOptions = {
        sound: 'Bottle',
        contentImage: path.join(__dirname, 'gulp.png'),
        icon: path.join(__dirname, 'gulp.png')
    };

    _.assign(notifyOptions, options);
    notifier.notify(notifyOptions);
}

function startTests(singleRun, done) {
    var child;
    var fork = require('child_process').fork;
    var karma = require('karma').server;
    var excludeFiles = [];
    var serverSpecs = config.serverIntegrationSpecs;
    excludeFiles = serverSpecs;
    var options = {
        configFile: __dirname + '/karma.conf.js',
        exclude: excludeFiles,
        singleRun: !!singleRun // !! is a js trick to force singleRun value to a boolean
    };

    if (args.startServers) {
        log('Starting server');

        var savedEnv = process.env;
        savedEnv.NODE_ENV = 'dev';
        savedEnv.PORT = 8888;
        child = fork(config.nodeServer);
    } else {
        if (serverSpecs && serverSpecs.length) {
            excludeFiles = serverSpecs;
        }
    }

    karma.start(options, karmaCompleted);

    function karmaCompleted(karmaResult) {
        log('Karma completed!');

        if (child) {
            log('Shutting down the child process.');
            child.kill();
        }

        if (karmaResult === 1) {
            done('karma: tests failed with code ' + karmaResult);
        } else {
            done();
        }
    }
}

function serve(isDev, isSpecRunner) {
    var nodeOptions = {
        script: config.nodeServer,
        delayTime: 1, // seconds
        env: {
            'PORT': port,
            'NODE_ENV': isDev ? 'dev' : 'build'
        },
        watch: [config.server]
    };

    return $.nodemon(nodeOptions)
        .on('restart', /*['vet'], can list tasks in here to run on events*/ function(event) {
            log('*** nodemon restarted');
            log('files changed on restart:\n' + event);
            setTimeout(function() {
                browserSync.notify('reloading now ...');
                browserSync.reload({stream: false});
            }, config.browserReloadDelay);
        })
        .on('start', function() {
            log('*** nodemon started');
            startBrowserSync(isDev, isSpecRunner);
        })
        .on('crash', function() {
            log('*** nodemon crashed: script crashed for some reason');
        })
        .on('exit', function() {
            log('*** nodemon exited cleanly');
        });
}

function changeEvent(event) {
    var srcPattern = new RegExp('/.*(?=/' + config.source + ')/');
    log('File ' + event.path.replace(srcPattern, '') + ' ' + event.type);
}

function startBrowserSync(isDev, isSpecRunner) {
    if (args.nosync || browserSync.active) {
        return;
    }

    log('Starting browser-sync on port ' + port);

    if (isDev) {
        gulp.watch([config.less], ['styles'])
            .on('change', function(event) { changeEvent(event); });
    } else {
        gulp.watch([config.less, config.js, config.html], ['optimize', browserSync.reload])
            .on('change', function(event) { changeEvent(event); });
    }

    var options = {
        proxy: 'localhost:' + port,
        port: 3000,
        files: isDev ? [
            config.client + '**/*.*',
            '!' + config.less,
            config.temp + '**/*.css'
        ] : [],
        ghostMode: {
            clicks: true,
            location: false,
            forms: true,
            scroll: true
        },
        injectChanges: true,
        logFileChanges: true,
        logLevel: 'debug',
        logPrefix: 'gulp-patterns',
        notify: true,
        reloadDelay: 1000 //ms
    };

    if (isSpecRunner) {
        options.startPath = config.specRunnerFile;
    }

    browserSync(options);
}

// function errorLogger(error) {
//     log('*** Start of Error ***');
//     log(error);
//     log('*** End of Error ***');
//     this.emit('end');
// }

function clean(path, done) {
    log('Cleaning: ' + $.util.colors.green(path));
    del(path, done);
}

function log(message) {
    if (typeof(message) === 'object') {
        for (var item in message) {
            if (message.hasOwnProperty(item)) {
                $.util.log($.util.colors.green(message[item]));
            }
        }
    } else {
        $.util.log($.util.colors.green(message));
    }
}
<commit_msg>Migrating to Gulp 4 module<commit_after>var _ = require('lodash');
var $ = require('gulp-load-plugins')({lazy: true});
var args = require('yargs').argv;
var browserSync = require('browser-sync');
var config = require('./gulp.config')();
var del = require('del');
var gulp = require('gulp');
var path = require('path');
var port = process.env.PORT || config.defaultPort;

gulp.task('help', $.taskListing);

gulp.task('default', ['help']);

gulp.task('vet', function() {
    log('Analyzing source with JSHint and JSCS.');

    return gulp
        .src(config.alljs)
        .pipe($.if(args.verbose, $.print()))
        .pipe($.jscs())
        .pipe($.jshint())
        .pipe($.jshint.reporter('jshint-stylish', {verbose: true}))
        .pipe($.jshint.reporter('fail'));
});

gulp.task('styles', ['clean-styles'], function() {
    log('Compiling Less --> CSS');

    return gulp
        .src(config.less)
        // plumber is a nice plug-in to handle errors in our stream.
        // it does something similar to the errorLogger created below.
        // errorLogger actually terminates the stream, whereas plumber doesn't, but does display error information
        .pipe($.plumber())
        .pipe($.less())
        //.on('error', errorLogger)
        .pipe($.autoprefixer({browsers: ['last 2 version', '> 5%']}))
        .pipe(gulp.dest(config.temp));
});

gulp.task('fonts', ['clean-fonts'], function() {
    log('Copying fonts');

    return gulp.src(config.fonts)
        .pipe(gulp.dest(config.build + 'fonts'));
});

gulp.task('images', ['clean-images'], function() {
    log('Copying and compressing the images');

    return gulp.src(config.images)
        .pipe($.imagemin({optimizationLevel: 4}))
        .pipe(gulp.dest(config.build + 'images'));
});

/*
 * while below can be used as a precursor to other task, no stream is being returned,
 * task depending on this won't neccessarily wait until this task completes before running
 * therefor need to update to similar function below that takes a callback function
 * i.e. in clean below would need to ensure the callback function 'done' gets called after
 * del, in this case though, del takes a callback function and will invoke it for us
*/
// gulp.task('clean-styles', function() {
//     var files = config.temp + '**/*.css';
//     clean(files);
// });

gulp.task('clean', function(done) {
    var delconfig = [].concat(config.build, config.temp);
    log('Cleaning: ' + $.util.colors.green(delconfig));
    del(delconfig, done);
});

gulp.task('clean-fonts', function(done) {
    clean(config.build + 'fonts/**/*.*', done);
});

gulp.task('clean-images', function(done) {
    clean(config.build + 'images/**/*.*', done);
});

gulp.task('clean-styles', function(done) {
    clean(config.temp + '**/*.css', done);
});

gulp.task('clean-code', function(done) {
    var files = [].concat(
        config.temp + '**/*.js',
        config.build + '**/*.html',
        config.build + 'js/**/*.js'
    );

    clean(files, done);
});

gulp.task('less-watcher', function() {
    gulp.watch([config.less], ['styles']);
});

//SPECIFIC TO ANGULAR
gulp.task('templatecache', ['clean-code'], function() {
    log('Creating AngularJS $templateCache');

    return gulp.src(config.htmltemplates)
        .pipe($.minifyHtml({empty: true}))
        .pipe($.angularTemplatecache(
            config.templateCache.file,
            config.templateCache.options
        ))
        .pipe(gulp.dest(config.temp));
});

gulp.task('wiredep', function() {
    log('Wire up the bower css, js and our app.js into the html');

    var options = config.getWiredepDefaultOptions();
    var wiredep = require('wiredep').stream;

    return gulp
        .src(config.index) // get index.html
        .pipe(wiredep(options)) // call wiredep, looks in bower.json at runtime dependencies('dependencies' --> not dev)
                                // looks in bower_components/<directory>
                                // main property and it's dependencies for each component
                                // and then injects all of them into the index.html
        .pipe($.inject(gulp.src(config.js)))
        .pipe(gulp.dest(config.client));
});

gulp.task('inject', ['wiredep', 'styles', 'templatecache'], function() {
    log('Wire up the bower css, js and our app.js into the html');

    return gulp
        .src(config.index) // get index.html
        .pipe($.inject(gulp.src(config.css)))
        .pipe(gulp.dest(config.client));
});

gulp.task('build', ['optimize', 'images', 'fonts'], function() {
    log('Building everything');

    var msg = {
        title: 'gulp build',
        subtitle: 'Deployed to the build folder.',
        message: 'Running `gulp serve-build`.'
    };

    del(config.temp);
    log(msg);
    notify(msg);
});

gulp.task('serve-specs', ['build-specs'], function(done) {
    log('run the spec runner');
    serve(true /* isDev */, true /* isSpecRunner */);
    done();
})

gulp.task('build-specs', ['templatecache'], function() {
    log('building the spec runner');

    var wiredep = require('wiredep').stream;
    var options = config.getWiredepDefaultOptions();
    var specs = config.specs;

    options.devDependencies = true;

    if (args.startServers) {
        specs = [].concat(specs, config.serverIntegrationSpecs);
    }

    return gulp.src(config.specRunner)
        .pipe(wiredep(options))
        .pipe($.inject(gulp.src(config.testlibraries), {name:'inject:testlibraries', read: false}))
        .pipe($.inject(gulp.src(config.js)))
        .pipe($.inject(gulp.src(config.specHelpers), {name:'inject:spechelpers', read: false}))
        .pipe($.inject(gulp.src(specs), {name:'inject:specs', read: false}))
        .pipe($.inject(gulp.src(config.temp + config.templateCache.file), {name:'inject:templates', read: false}))
        .pipe(gulp.dest(config.client));
});

gulp.task('optimize', ['inject', 'test'], function() {
    log('Optimizing the javascript, css, html');

    var assets = $.useref.assets({searchPath: './'});
    var templateCache = config.temp + config.templateCache.file;
    var cssFilter = $.filter('**/*.css', {restore: true});
    var jsLibFilter = $.filter('**/' + config.optimized.lib, {restore: true});
    var jsAppFilter = $.filter('**/' + config.optimized.app, {restore: true});

    return gulp.src(config.index)
        .pipe($.plumber())
        .pipe($.inject(
            gulp.src(templateCache, {read: false}), {
            starttag: '<!-- inject:templates:js -->'
        }))
        .pipe(assets)
        .pipe(cssFilter) // filter to css
        .pipe($.csso()) // csso
        .pipe(cssFilter.restore) // remove css filter

        .pipe(jsLibFilter) // filter to lib js
        .pipe($.uglify()) // uglify
        .pipe(jsLibFilter.restore) // remove js filter

        //ANGULAR specific
        .pipe(jsAppFilter) // filter to app js (custom angular js files)
        .pipe($.ngAnnotate())
        .pipe($.uglify()) // uglify
        .pipe(jsAppFilter.restore) // remove js filter

        .pipe($.rev()) // adds hash to js/css file names if contents have changed
        .pipe(assets.restore())
        .pipe($.useref())
        .pipe($.revReplace()) // replaces references to js/css file names in files with hash appended names
        .pipe(gulp.dest(config.build))
        .pipe($.rev.manifest())
        .pipe(gulp.dest(config.build));
});

/*
 * Bump the version
 * --type=pre will bump the prerelease version *.*.*-x
 * --type=patch or no flag will bump the patch version *.*.x
 * --type=minor will bump the minor version *.x.*
 * --type=major will bump the major version x.*.*
 * --version=1.2.3 will bump to a specific version and ignore other flags
 */
gulp.task('bump', function() {
    var msg = 'Bumping versions';
    var type = args.type;
    var version = args.version;
    var options = {};

    if (version) {
        options.version = version;
        msg += ' to ' + version;
    } else {
        options.type = type;
        msg += ' for a ' + type;
    }

    log(msg);

    return gulp.src(config.packages)
        .pipe($.print())
        .pipe($.bump(options))
        .pipe(gulp.dest(config.root));
});

// SPECIFIC TO NODE: detects changes to files on server and can perform actions on events
gulp.task('serve-build', ['build'], function() {
    serve(false /* isDev */);
});

gulp.task('serve-dev', ['inject'], function() {
    serve(true /* isDev */);
});

gulp.task('test', ['vet', 'templatecache'], function(done) {
    startTests(true /* singleRun */, done);
});

gulp.task('autotest', ['vet', 'templatecache'], function(done) {
    startTests(false /* singleRun */, done);
});

//////////////////////////////////////////////////////////////////
function notify(options) {
    var notifier = require('node-notifier');
    var notifyOptions = {
        sound: 'Bottle',
        contentImage: path.join(__dirname, 'gulp.png'),
        icon: path.join(__dirname, 'gulp.png')
    };

    _.assign(notifyOptions, options);
    notifier.notify(notifyOptions);
}

function startTests(singleRun, done) {
    var child;
    var fork = require('child_process').fork;
    var karma = require('karma').server;
    var excludeFiles = [];
    var serverSpecs = config.serverIntegrationSpecs;
    excludeFiles = serverSpecs;
    var options = {
        configFile: __dirname + '/karma.conf.js',
        exclude: excludeFiles,
        singleRun: !!singleRun // !! is a js trick to force singleRun value to a boolean
    };

    if (args.startServers) {
        log('Starting server');

        var savedEnv = process.env;
        savedEnv.NODE_ENV = 'dev';
        savedEnv.PORT = 8888;
        child = fork(config.nodeServer);
    } else {
        if (serverSpecs && serverSpecs.length) {
            excludeFiles = serverSpecs;
        }
    }

    karma.start(options, karmaCompleted);

    function karmaCompleted(karmaResult) {
        log('Karma completed!');

        if (child) {
            log('Shutting down the child process.');
            child.kill();
        }

        if (karmaResult === 1) {
            done('karma: tests failed with code ' + karmaResult);
        } else {
            done();
        }
    }
}

function serve(isDev, isSpecRunner) {
    var nodeOptions = {
        script: config.nodeServer,
        delayTime: 1, // seconds
        env: {
            'PORT': port,
            'NODE_ENV': isDev ? 'dev' : 'build'
        },
        watch: [config.server]
    };

    return $.nodemon(nodeOptions)
        .on('restart', /*['vet'], can list tasks in here to run on events*/ function(event) {
            log('*** nodemon restarted');
            log('files changed on restart:\n' + event);
            setTimeout(function() {
                browserSync.notify('reloading now ...');
                browserSync.reload({stream: false});
            }, config.browserReloadDelay);
        })
        .on('start', function() {
            log('*** nodemon started');
            startBrowserSync(isDev, isSpecRunner);
        })
        .on('crash', function() {
            log('*** nodemon crashed: script crashed for some reason');
        })
        .on('exit', function() {
            log('*** nodemon exited cleanly');
        });
}

function changeEvent(event) {
    var srcPattern = new RegExp('/.*(?=/' + config.source + ')/');
    log('File ' + event.path.replace(srcPattern, '') + ' ' + event.type);
}

function startBrowserSync(isDev, isSpecRunner) {
    if (args.nosync || browserSync.active) {
        return;
    }

    log('Starting browser-sync on port ' + port);

    if (isDev) {
        gulp.watch([config.less], ['styles'])
            .on('change', function(event) { changeEvent(event); });
    } else {
        gulp.watch([config.less, config.js, config.html], ['optimize', browserSync.reload])
            .on('change', function(event) { changeEvent(event); });
    }

    var options = {
        proxy: 'localhost:' + port,
        port: 3000,
        files: isDev ? [
            config.client + '**/*.*',
            '!' + config.less,
            config.temp + '**/*.css'
        ] : [],
        ghostMode: {
            clicks: true,
            location: false,
            forms: true,
            scroll: true
        },
        injectChanges: true,
        logFileChanges: true,
        logLevel: 'debug',
        logPrefix: 'gulp-patterns',
        notify: true,
        reloadDelay: 1000 //ms
    };

    if (isSpecRunner) {
        options.startPath = config.specRunnerFile;
    }

    browserSync(options);
}

// function errorLogger(error) {
//     log('*** Start of Error ***');
//     log(error);
//     log('*** End of Error ***');
//     this.emit('end');
// }

function clean(path, done) {
    log('Cleaning: ' + $.util.colors.green(path));
    del(path, done);
}

function log(message) {
    if (typeof(message) === 'object') {
        for (var item in message) {
            if (message.hasOwnProperty(item)) {
                $.util.log($.util.colors.green(message[item]));
            }
        }
    } else {
        $.util.log($.util.colors.green(message));
    }
}

//////////////////////////////////////////////////////////////////
/************************* GULP 4 NOTES *************************/
// gulp.task('build',
//     gulp.series(
//         gulp.parallel('vet', 'test'), // vet/test run in parallel
//         // wiredep/styles/templatecache waits until vet/test are done before it gets run
//         gulp.paralled('wiredep', 'styles', 'templatecache'),
//         optimize
//     )
// );<|endoftext|>


<commit_before>var gulp = require('gulp');
var mocha = require('gulp-mocha');
var clean = require('gulp-clean');
var tsc = require('gulp-typescript');

var paths = {
    source: ['src/*.ts']
};

gulp.task('clean', function() {
    return gulp.src(['dist'])
        .pipe(clean());
});

gulp.task('tscAMD', ['clean'], function() {
    var tsResult = gulp.src(paths.source)
        .pipe(tsc({
            module: 'amd',
            declarationFiles: true
        }));

    tsResult.dts.pipe(gulp.dest('dist/amd'));

    return tsResult.js.pipe(gulp.dest('dist/amd'));
});

gulp.task('tscCommonJS', ['clean'], function() {
    var tsResult = gulp.src(paths.source)
        .pipe(tsc({
            module: 'commonjs',
            declarationFiles: true
        }));

    tsResult.dts.pipe(gulp.dest('dist/commonjs'));

    return tsResult.js.pipe(gulp.dest('dist/commonjs'));
});

gulp.task('cleanTest', function() {
    return gulp.src(['bin'])
        .pipe(clean());
});

gulp.task('copyDist', ['tscAMD', 'tscCommonJS'], function() {
    return gulp.src('dist/commonjs/*.js')
        .pipe(gulp.dest('bin/src'));
});

gulp.task('tscTest', ['cleanTest', 'copyDist'], function() {
    var tsResult = gulp.src('test/*.ts')
        .pipe(tsc({
            module: 'commonjs'
        }));

    return tsResult.js.pipe(gulp.dest('bin/test'));
});

gulp.task('test', ['tscTest'], function() {
    return gulp.src('bin/test/*.js', {read: false})
        .pipe(mocha());
});

gulp.task('default', ['tscAMD', 'tscCommonJS']);

gulp.task('watch', ['default'], function() {
    return gulp.watch('src/**/*', ['default']);
});
<commit_msg>Oops.<commit_after>var gulp = require('gulp');
var mocha = require('gulp-mocha');
var clean = require('gulp-clean');
var tsc = require('gulp-typescript');

var paths = {
    source: ['src/*.ts']
};

gulp.task('clean', function() {
    return gulp.src(['dist'])
        .pipe(clean());
});

gulp.task('tscAMD', ['clean'], function() {
    var tsResult = gulp.src(paths.source)
        .pipe(tsc({
            module: 'amd',
            target: 'ES5',
            declarationFiles: true
        }));

    tsResult.dts.pipe(gulp.dest('dist/amd'));

    return tsResult.js.pipe(gulp.dest('dist/amd'));
});

gulp.task('tscCommonJS', ['clean'], function() {
    var tsResult = gulp.src(paths.source)
        .pipe(tsc({
            module: 'commonjs',
            target: 'ES5',
            declarationFiles: true
        }));

    tsResult.dts.pipe(gulp.dest('dist/commonjs'));

    return tsResult.js.pipe(gulp.dest('dist/commonjs'));
});

gulp.task('cleanTest', function() {
    return gulp.src(['bin'])
        .pipe(clean());
});

gulp.task('copyDist', ['tscAMD', 'tscCommonJS'], function() {
    return gulp.src('dist/commonjs/*.js')
        .pipe(gulp.dest('bin/src'));
});

gulp.task('tscTest', ['cleanTest', 'copyDist'], function() {
    var tsResult = gulp.src('test/*.ts')
        .pipe(tsc({
            module: 'commonjs',
            target: 'ES5'
        }));

    return tsResult.js.pipe(gulp.dest('bin/test'));
});

gulp.task('test', ['tscTest'], function() {
    return gulp.src('bin/test/*.js', {
            read: false
        })
        .pipe(mocha());
});

gulp.task('default', ['tscAMD', 'tscCommonJS']);

gulp.task('watch', ['default'], function() {
    return gulp.watch('src/**/*', ['default']);
});<|endoftext|>


<commit_before>import loaderIcon from '../images/loaders/default.svg';
import Loader from './Loader';
import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
import fetch from '../util/fetch';
import { getPinnedArticles } from '../util/pins';
import { getFeed } from '../util/feeds';
import getPlaceholderImageURL from '../util/getPlaceholderImageURL';
import moment from 'moment';
import ArticleListItem from './ArticleListItem';
import Waypoint from 'react-waypoint';
import Img from 'react-image';
import Popover from 'react-popover';

class RSSArticleList extends React.Component {
	constructor(props) {
		super(props);
		this.state = {
			articleCursor: 1,
			menuIsOpen: false,
			sortBy: 'latest',
			newArticlesAvailable: false,
		};
		this.getRSSFeed = this.getRSSFeed.bind(this);
		this.getRSSArticles = this.getRSSArticles.bind(this);
		this.getFollowState = this.getFollowState.bind(this);
		this.toggleMenu = this.toggleMenu.bind(this);

		this.contentsEl = React.createRef();
	}

	toggleMenu() {
		this.setState({
			menuIsOpen: !this.state.menuIsOpen,
		});
	}

	subscribeToStreamFeed(rssFeedID, streamFeedToken) {
		this.subscription = window.streamClient
			.feed('rss', rssFeedID, streamFeedToken)
			.subscribe(() => {
				this.setState({
					newArticlesAvailable: true,
				});
			});
	}

	unsubscribeFromStreamFeed() {
		this.subscription.cancel();
	}

	componentDidMount() {
		// send analytic event that they viewed a feed
		window.streamAnalyticsClient.trackEngagement({
			label: 'viewed_rss_feed',
			content: `rss:${this.props.match.params.rssFeedID}`,
		});

		this.getRSSFeed(this.props.match.params.rssFeedID);
		this.getFollowState(this.props.match.params.rssFeedID);
		this.getRSSArticles(this.props.match.params.rssFeedID);
		getPinnedArticles(this.props.dispatch);
		getFeed(this.props.dispatch, 'article', 0, 20);
		// subscribe to feed updates
		if (this.props.rssFeed) {
			this.subscribeToStreamFeed(
				this.props.rssFeed._id,
				this.props.rssFeed.streamToken,
			);
		}
	}
	componentWillReceiveProps(nextProps) {
		if (nextProps.match.params.rssFeedID !== this.props.match.params.rssFeedID) {
			window.streamAnalyticsClient.trackEngagement({
				label: 'viewed_rss_feed',
				content: `rss:${nextProps.match.params.rssFeedID}`,
			});

			// if navigating between rss feeds
			this.setState(
				{
					articleCursor: 1,
				},
				() => {
					this.getRSSFeed(nextProps.match.params.rssFeedID);
					this.getFollowState(nextProps.match.params.rssFeedID);
					this.getRSSArticles(nextProps.match.params.rssFeedID);
					getPinnedArticles(this.props.dispatch);
					getFeed(this.props.dispatch, 'article', 0, 20);
				},
			);
			// if we're switching rss feeds, unsubscribe from the old rss feed, and subscribe to the new rss feed
			this.unsubscribeFromStreamFeed();
			this.subscribeToStreamFeed(
				nextProps.rssFeed._id,
				nextProps.rssFeed.streamToken,
			);
		}

		// if we didn't have a rss feed before, subscribe to the new rss feed
		if (!this.props.rssFeed && nextProps.rssFeed) {
			this.subscribeToStreamFeed(
				nextProps.rssFeed._id,
				nextProps.rssFeed.streamToken,
			);
		}

		// scroll down to last saved position, then delete from localStorage
		// note from Ken - this works because we've still got all the articles loaded into the frontend - no need to maintain pagination position
		if (this.contentsEl.current && localStorage['rss-article-list-scroll-position']) {
			this.contentsEl.current.scrollTop =
				localStorage['rss-article-list-scroll-position'];
			delete localStorage['rss-article-list-scroll-position'];
		}
	}
	componentWillUnmount() {
		this.unsubscribeFromStreamFeed();
	}
	getRSSFeed(rssFeedID) {
		return fetch('GET', `/rss/${rssFeedID}`)
			.then(res => {
				this.props.dispatch({
					rssFeed: res.data,
					type: 'UPDATE_RSS_FEED',
				});
			})
			.catch(err => {
				console.log(err); // eslint-disable-line no-console
			});
	}
	getFollowState(rssFeedID) {
		return fetch(
			'get',
			'/follows',
			{},
			{
				rss: rssFeedID,
				user: localStorage['authedUser'],
			},
		).then(response => {
			for (let followRelationship of response.data) {
				this.props.dispatch({
					rssFeedID: followRelationship.rss._id,
					type: 'FOLLOW_RSS_FEED',
					userID: followRelationship.user._id,
				});
			}
		});
	}
	getRSSArticles(rssFeedID) {
		// get rss articles
		return fetch(
			'GET',
			'/articles',
			{},
			{
				page: this.state.articleCursor,
				per_page: 10,
				rss: rssFeedID,
				sort_by: 'publicationDate,desc',
			},
		)
			.then(res => {
				if (res.data.length === 0) {
					this.setState({
						reachedEndOfFeed: true,
					});
				}

				this.props.dispatch({
					articles: res.data,
					type: 'BATCH_UPDATE_ARTICLES',
				});
			})
			.catch(err => {
				console.log(err); // eslint-disable-line no-console
			});
	}
	follow() {
		// if not currently following, optimistic dispatch follow, make post call, then handle error
		this.props.dispatch({
			rssFeedID: this.props.match.params.rssFeedID,
			type: 'FOLLOW_RSS_FEED',
			userID: localStorage['authedUser'],
		});
		fetch('post', '/follows', null, {
			rss: this.props.match.params.rssFeedID,
			type: 'rss',
		}).catch(error => {
			console.log(error); // eslint-disable-line no-console
			this.props.dispatch({
				rssFeedID: this.props.match.params.rssFeedID,
				type: 'UNFOLLOW_RSS_FEED',
				userID: localStorage['authedUser'],
			});
		});
	}
	unfollow() {
		// if currently following, optimistic dispatch unfollow, make delete call, then handle error
		this.props.dispatch({
			rssFeedID: this.props.match.params.rssFeedID,
			type: 'UNFOLLOW_RSS_FEED',
			userID: localStorage['authedUser'],
		});
		fetch('delete', '/follows', null, {
			rss: this.props.match.params.rssFeedID,
			type: 'rss',
		}).catch(error => {
			console.log(error); // eslint-disable-line no-console
			this.props.dispatch({
				rssFeedID: this.props.match.params.rssFeedID,
				type: 'FOLLOW_RSS_FEED',
				userID: localStorage['authedUser'],
			});
		});
	}
	render() {
		let sortedArticles = [...this.props.articles];
		sortedArticles.sort((a, b) => {
			return (
				moment(b.publicationDate).valueOf() - moment(a.publicationDate).valueOf()
			);
		});

		let menuContent = (

 {
							if (this.props.following) {
								this.unfollow();
							} else {
								this.follow();
							}
						}}
					>
						{this.props.following ? 'Unfollow' : 'Follow'}

		);

		if (this.props.loading) {
			return <Loader />;
		} else {
			let rightContents;
			if (this.props.articles.length === 0) {
				rightContents = (

{"We haven't found any articles for this RSS feed yet :("}

							{
								"It might be because the RSS feed doesn't have any articles, or because it just got added and we're still parsing them. Come check back in a few minutes?"
							}

							{
								"If you're pretty sure there's supposed to be some articles here, and they aren't showing up, please file a "
							}

								GitHub Issue
							.

				);
			} else {
				rightContents = (
					<React.Fragment>
						{sortedArticles.map(article => {
							return (
								<ArticleListItem
									key={article._id}
									onNavigation={() => {
										localStorage[
											'rss-article-list-scroll-position'
										] = this.contentsEl.current.scrollTop;
									}}
									{...article}
								/>
							);
						})}

						{this.state.reachedEndOfFeed ? (

{"That's it! No more articles here."}

									{
										"What, did you think that once you got all the way around, you'd just be back at the same place that you started? Sounds like some real round-feed thinking to me."
									}

						) : (

								<Waypoint
									onEnter={() => {
										this.setState(
											{
												articleCursor:
													this.state.articleCursor + 1,
											},
											() => {
												this.getRSSArticles(
													this.props.match.params.rssFeedID,
												);
											},
										);
									}}
								/>

						)}
					</React.Fragment>
				);
			}
			return (
				<React.Fragment>

{this.props.rssFeed.title}

								<Popover
									body={menuContent}
									isOpen={this.state.menuIsOpen}
									onOuterAction={this.toggleMenu}
									place="below"
									tipSize={0.1}
								>

								</Popover>

						{this.state.newArticlesAvailable ? (

 {
									this.getRSSFeed(this.props.match.params.rssFeedID);
									this.getFollowState(
										this.props.match.params.rssFeedID,
									);
									this.getRSSArticles(
										this.props.match.params.rssFeedID,
									);
									getPinnedArticles(this.props.dispatch);
									getFeed(this.props.dispatch, 'article', 0, 20);
									this.setState({
										newArticlesAvailable: false,
									});
								}}
							>
								New episodes available - click to refresh

						) : null}
						{rightContents}

				</React.Fragment>
			);
		}
	}
}

RSSArticleList.defaultProps = {
	articles: [],
	loading: true,
	following: false,
};

RSSArticleList.propTypes = {
	articles: PropTypes.array,
	dispatch: PropTypes.func.isRequired,
	following: PropTypes.bool,
	match: PropTypes.shape({
		params: PropTypes.shape({
			rssFeedID: PropTypes.string.isRequired,
		}),
	}),
	loading: PropTypes.bool,
	rssFeed: PropTypes.shape({
		_id: PropTypes.string,
		images: PropTypes.shape({
			featured: PropTypes.string,
			og: PropTypes.string,
		}),
		title: PropTypes.string,
		streamToken: PropTypes.string.isRequired,
	}),
};

const mapStateToProps = (state, ownProps) => {
	let rssFeed = { images: {} };

	// first, check to see if the RSS feed has been loaded into redux state. if not, return ownProps + loading.

	if ('rssFeeds' in state && ownProps.match.params.rssFeedID in state.rssFeeds) {
		rssFeed = { ...state.rssFeeds[ownProps.match.params.rssFeedID] };
	} else {
		return {
			...ownProps,
			loading: true,
		};
	}

	// then, attach "follow" state to rss feed
	let following = false;
	if (
		state.followedRssFeeds &&
		state.followedRssFeeds[localStorage['authedUser']] &&
		state.followedRssFeeds[localStorage['authedUser']][
			ownProps.match.params.rssFeedID
		]
	) {
		following = true;
	}

	// then, check to see if articles have been loaded into redux state yet. if not, return ownProps + loading.
	let articles = [];
	if (state.articles) {
		for (let articleID of Object.keys(state.articles)) {
			if (state.articles[articleID].rss === ownProps.match.params.rssFeedID) {
				articles.push({ ...state.articles[articleID] }); // from @kenhoff - derp, make sure that you're pushing a _copy_ of the object, not the object itself, otherwise redux state is boom :(
			}
		}
	} else {
		return {
			...ownProps,
			loading: true,
		};
	}

	// then, attach pin state, "recent" state, and rss parent to all articles
	for (let article of articles) {
		// attach pinned state
		if (state.pinnedArticles && state.pinnedArticles[article._id]) {
			article.pinned = true;
			article.pinID = state.pinnedArticles[article._id]._id;
		} else {
			article.pinned = false;
		}

		// attach "recent" state
		if (state.feeds && state.feeds[`user_article:${localStorage['authedUser']}`]) {
			if (
				state.feeds[`user_article:${localStorage['authedUser']}`].indexOf(
					article._id,
				) < 20 &&
				state.feeds[`user_article:${localStorage['authedUser']}`].indexOf(
					article._id,
				) !== -1
			) {
				article.recent = true;
			} else {
				article.recent = false;
			}
		}

		// attach rss feed
		article.rss = { ...rssFeed };
	}

	// last, sort the articles, because the IDs might not be in chronological order
	articles.sort((a, b) => {
		return moment(b.publicationDate).valueOf() - moment(a.publicationDate).valueOf();
	});

	return {
		articles,
		following,
		loading: false,
		rssFeed,
		...ownProps,
	};
};

export default connect(mapStateToProps)(RSSArticleList);
<commit_msg>episodes->articles typo<commit_after>import loaderIcon from '../images/loaders/default.svg';
import Loader from './Loader';
import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
import fetch from '../util/fetch';
import { getPinnedArticles } from '../util/pins';
import { getFeed } from '../util/feeds';
import getPlaceholderImageURL from '../util/getPlaceholderImageURL';
import moment from 'moment';
import ArticleListItem from './ArticleListItem';
import Waypoint from 'react-waypoint';
import Img from 'react-image';
import Popover from 'react-popover';

class RSSArticleList extends React.Component {
	constructor(props) {
		super(props);
		this.state = {
			articleCursor: 1,
			menuIsOpen: false,
			sortBy: 'latest',
			newArticlesAvailable: false,
		};
		this.getRSSFeed = this.getRSSFeed.bind(this);
		this.getRSSArticles = this.getRSSArticles.bind(this);
		this.getFollowState = this.getFollowState.bind(this);
		this.toggleMenu = this.toggleMenu.bind(this);

		this.contentsEl = React.createRef();
	}

	toggleMenu() {
		this.setState({
			menuIsOpen: !this.state.menuIsOpen,
		});
	}

	subscribeToStreamFeed(rssFeedID, streamFeedToken) {
		this.subscription = window.streamClient
			.feed('rss', rssFeedID, streamFeedToken)
			.subscribe(() => {
				this.setState({
					newArticlesAvailable: true,
				});
			});
	}

	unsubscribeFromStreamFeed() {
		this.subscription.cancel();
	}

	componentDidMount() {
		// send analytic event that they viewed a feed
		window.streamAnalyticsClient.trackEngagement({
			label: 'viewed_rss_feed',
			content: `rss:${this.props.match.params.rssFeedID}`,
		});

		this.getRSSFeed(this.props.match.params.rssFeedID);
		this.getFollowState(this.props.match.params.rssFeedID);
		this.getRSSArticles(this.props.match.params.rssFeedID);
		getPinnedArticles(this.props.dispatch);
		getFeed(this.props.dispatch, 'article', 0, 20);
		// subscribe to feed updates
		if (this.props.rssFeed) {
			this.subscribeToStreamFeed(
				this.props.rssFeed._id,
				this.props.rssFeed.streamToken,
			);
		}
	}
	componentWillReceiveProps(nextProps) {
		if (nextProps.match.params.rssFeedID !== this.props.match.params.rssFeedID) {
			window.streamAnalyticsClient.trackEngagement({
				label: 'viewed_rss_feed',
				content: `rss:${nextProps.match.params.rssFeedID}`,
			});

			// if navigating between rss feeds
			this.setState(
				{
					articleCursor: 1,
				},
				() => {
					this.getRSSFeed(nextProps.match.params.rssFeedID);
					this.getFollowState(nextProps.match.params.rssFeedID);
					this.getRSSArticles(nextProps.match.params.rssFeedID);
					getPinnedArticles(this.props.dispatch);
					getFeed(this.props.dispatch, 'article', 0, 20);
				},
			);
			// if we're switching rss feeds, unsubscribe from the old rss feed, and subscribe to the new rss feed
			this.unsubscribeFromStreamFeed();
			this.subscribeToStreamFeed(
				nextProps.rssFeed._id,
				nextProps.rssFeed.streamToken,
			);
		}

		// if we didn't have a rss feed before, subscribe to the new rss feed
		if (!this.props.rssFeed && nextProps.rssFeed) {
			this.subscribeToStreamFeed(
				nextProps.rssFeed._id,
				nextProps.rssFeed.streamToken,
			);
		}

		// scroll down to last saved position, then delete from localStorage
		// note from Ken - this works because we've still got all the articles loaded into the frontend - no need to maintain pagination position
		if (this.contentsEl.current && localStorage['rss-article-list-scroll-position']) {
			this.contentsEl.current.scrollTop =
				localStorage['rss-article-list-scroll-position'];
			delete localStorage['rss-article-list-scroll-position'];
		}
	}
	componentWillUnmount() {
		this.unsubscribeFromStreamFeed();
	}
	getRSSFeed(rssFeedID) {
		return fetch('GET', `/rss/${rssFeedID}`)
			.then(res => {
				this.props.dispatch({
					rssFeed: res.data,
					type: 'UPDATE_RSS_FEED',
				});
			})
			.catch(err => {
				console.log(err); // eslint-disable-line no-console
			});
	}
	getFollowState(rssFeedID) {
		return fetch(
			'get',
			'/follows',
			{},
			{
				rss: rssFeedID,
				user: localStorage['authedUser'],
			},
		).then(response => {
			for (let followRelationship of response.data) {
				this.props.dispatch({
					rssFeedID: followRelationship.rss._id,
					type: 'FOLLOW_RSS_FEED',
					userID: followRelationship.user._id,
				});
			}
		});
	}
	getRSSArticles(rssFeedID) {
		// get rss articles
		return fetch(
			'GET',
			'/articles',
			{},
			{
				page: this.state.articleCursor,
				per_page: 10,
				rss: rssFeedID,
				sort_by: 'publicationDate,desc',
			},
		)
			.then(res => {
				if (res.data.length === 0) {
					this.setState({
						reachedEndOfFeed: true,
					});
				}

				this.props.dispatch({
					articles: res.data,
					type: 'BATCH_UPDATE_ARTICLES',
				});
			})
			.catch(err => {
				console.log(err); // eslint-disable-line no-console
			});
	}
	follow() {
		// if not currently following, optimistic dispatch follow, make post call, then handle error
		this.props.dispatch({
			rssFeedID: this.props.match.params.rssFeedID,
			type: 'FOLLOW_RSS_FEED',
			userID: localStorage['authedUser'],
		});
		fetch('post', '/follows', null, {
			rss: this.props.match.params.rssFeedID,
			type: 'rss',
		}).catch(error => {
			console.log(error); // eslint-disable-line no-console
			this.props.dispatch({
				rssFeedID: this.props.match.params.rssFeedID,
				type: 'UNFOLLOW_RSS_FEED',
				userID: localStorage['authedUser'],
			});
		});
	}
	unfollow() {
		// if currently following, optimistic dispatch unfollow, make delete call, then handle error
		this.props.dispatch({
			rssFeedID: this.props.match.params.rssFeedID,
			type: 'UNFOLLOW_RSS_FEED',
			userID: localStorage['authedUser'],
		});
		fetch('delete', '/follows', null, {
			rss: this.props.match.params.rssFeedID,
			type: 'rss',
		}).catch(error => {
			console.log(error); // eslint-disable-line no-console
			this.props.dispatch({
				rssFeedID: this.props.match.params.rssFeedID,
				type: 'FOLLOW_RSS_FEED',
				userID: localStorage['authedUser'],
			});
		});
	}
	render() {
		let sortedArticles = [...this.props.articles];
		sortedArticles.sort((a, b) => {
			return (
				moment(b.publicationDate).valueOf() - moment(a.publicationDate).valueOf()
			);
		});

		let menuContent = (

 {
							if (this.props.following) {
								this.unfollow();
							} else {
								this.follow();
							}
						}}
					>
						{this.props.following ? 'Unfollow' : 'Follow'}

		);

		if (this.props.loading) {
			return <Loader />;
		} else {
			let rightContents;
			if (this.props.articles.length === 0) {
				rightContents = (

{"We haven't found any articles for this RSS feed yet :("}

							{
								"It might be because the RSS feed doesn't have any articles, or because it just got added and we're still parsing them. Come check back in a few minutes?"
							}

							{
								"If you're pretty sure there's supposed to be some articles here, and they aren't showing up, please file a "
							}

								GitHub Issue
							.

				);
			} else {
				rightContents = (
					<React.Fragment>
						{sortedArticles.map(article => {
							return (
								<ArticleListItem
									key={article._id}
									onNavigation={() => {
										localStorage[
											'rss-article-list-scroll-position'
										] = this.contentsEl.current.scrollTop;
									}}
									{...article}
								/>
							);
						})}

						{this.state.reachedEndOfFeed ? (

{"That's it! No more articles here."}

									{
										"What, did you think that once you got all the way around, you'd just be back at the same place that you started? Sounds like some real round-feed thinking to me."
									}

						) : (

								<Waypoint
									onEnter={() => {
										this.setState(
											{
												articleCursor:
													this.state.articleCursor + 1,
											},
											() => {
												this.getRSSArticles(
													this.props.match.params.rssFeedID,
												);
											},
										);
									}}
								/>

						)}
					</React.Fragment>
				);
			}
			return (
				<React.Fragment>

{this.props.rssFeed.title}

								<Popover
									body={menuContent}
									isOpen={this.state.menuIsOpen}
									onOuterAction={this.toggleMenu}
									place="below"
									tipSize={0.1}
								>

								</Popover>

						{this.state.newArticlesAvailable ? (

 {
									this.getRSSFeed(this.props.match.params.rssFeedID);
									this.getFollowState(
										this.props.match.params.rssFeedID,
									);
									this.getRSSArticles(
										this.props.match.params.rssFeedID,
									);
									getPinnedArticles(this.props.dispatch);
									getFeed(this.props.dispatch, 'article', 0, 20);
									this.setState({
										newArticlesAvailable: false,
									});
								}}
							>
								New articles available - click to refresh

						) : null}
						{rightContents}

				</React.Fragment>
			);
		}
	}
}

RSSArticleList.defaultProps = {
	articles: [],
	loading: true,
	following: false,
};

RSSArticleList.propTypes = {
	articles: PropTypes.array,
	dispatch: PropTypes.func.isRequired,
	following: PropTypes.bool,
	match: PropTypes.shape({
		params: PropTypes.shape({
			rssFeedID: PropTypes.string.isRequired,
		}),
	}),
	loading: PropTypes.bool,
	rssFeed: PropTypes.shape({
		_id: PropTypes.string,
		images: PropTypes.shape({
			featured: PropTypes.string,
			og: PropTypes.string,
		}),
		title: PropTypes.string,
		streamToken: PropTypes.string.isRequired,
	}),
};

const mapStateToProps = (state, ownProps) => {
	let rssFeed = { images: {} };

	// first, check to see if the RSS feed has been loaded into redux state. if not, return ownProps + loading.

	if ('rssFeeds' in state && ownProps.match.params.rssFeedID in state.rssFeeds) {
		rssFeed = { ...state.rssFeeds[ownProps.match.params.rssFeedID] };
	} else {
		return {
			...ownProps,
			loading: true,
		};
	}

	// then, attach "follow" state to rss feed
	let following = false;
	if (
		state.followedRssFeeds &&
		state.followedRssFeeds[localStorage['authedUser']] &&
		state.followedRssFeeds[localStorage['authedUser']][
			ownProps.match.params.rssFeedID
		]
	) {
		following = true;
	}

	// then, check to see if articles have been loaded into redux state yet. if not, return ownProps + loading.
	let articles = [];
	if (state.articles) {
		for (let articleID of Object.keys(state.articles)) {
			if (state.articles[articleID].rss === ownProps.match.params.rssFeedID) {
				articles.push({ ...state.articles[articleID] }); // from @kenhoff - derp, make sure that you're pushing a _copy_ of the object, not the object itself, otherwise redux state is boom :(
			}
		}
	} else {
		return {
			...ownProps,
			loading: true,
		};
	}

	// then, attach pin state, "recent" state, and rss parent to all articles
	for (let article of articles) {
		// attach pinned state
		if (state.pinnedArticles && state.pinnedArticles[article._id]) {
			article.pinned = true;
			article.pinID = state.pinnedArticles[article._id]._id;
		} else {
			article.pinned = false;
		}

		// attach "recent" state
		if (state.feeds && state.feeds[`user_article:${localStorage['authedUser']}`]) {
			if (
				state.feeds[`user_article:${localStorage['authedUser']}`].indexOf(
					article._id,
				) < 20 &&
				state.feeds[`user_article:${localStorage['authedUser']}`].indexOf(
					article._id,
				) !== -1
			) {
				article.recent = true;
			} else {
				article.recent = false;
			}
		}

		// attach rss feed
		article.rss = { ...rssFeed };
	}

	// last, sort the articles, because the IDs might not be in chronological order
	articles.sort((a, b) => {
		return moment(b.publicationDate).valueOf() - moment(a.publicationDate).valueOf();
	});

	return {
		articles,
		following,
		loading: false,
		rssFeed,
		...ownProps,
	};
};

export default connect(mapStateToProps)(RSSArticleList);
<|endoftext|>