Skip to content
Snippets Groups Projects
gulpfile.js 11.9 KiB
Newer Older
  • Learn to ignore specific revisions
  • ezsh's avatar
    ezsh committed
    /* eslint-disable one-var */
    const autoprefixer = require('autoprefixer'),
    	chalk = require('chalk'),
    
    	concat = require('gulp-concat'),
    
    ezsh's avatar
    ezsh committed
    	gulp = require('gulp'),
    
    	log = require('fancy-log-levels'),
    	noop = require('gulp-noop'),
    	postcss = require('gulp-postcss'),
    	shell = require('gulp-shell'),
    	sort = require('gulp-sort'),
    	sourcemaps = require('gulp-sourcemaps'),
    
    	stripCssJSComments = require('gulp-strip-comments'),
    
    ezsh's avatar
    ezsh committed
    	childProcess  = require('child_process'),
    
    	which = require('which'),
    	fs = require('fs'),
    	path = require('path'),
    	os = require('os'),
    	yargs = require('yargs'),
    	cfg = require('./build.config.json');
    
    
    ezsh's avatar
    ezsh committed
    // defines build options which can be supplied from the command line
    // example: npx gulp --minify --verbosity 6
    
    const args = yargs.options({
    
    ezsh's avatar
    ezsh committed
    	verbosity: {type: 'number', default: 1},
    
    	release: {type: 'boolean', default: false},
    
    ezsh's avatar
    ezsh committed
    	minify: {type: 'boolean', default: false},
    
    	embedsourcemaps: {type: 'boolean', default: false},
    	sourcemapsincludecontent: {type: 'boolean', default: false}
    }).argv;
    
    
    ezsh's avatar
    ezsh committed
    /** Filename for the temporary output. Tweego will write here and then it will be moved into the output dir */
    
    ezsh's avatar
    ezsh committed
    // set log verbosity basing on the command line argument
    
    ezsh's avatar
    ezsh committed
    // -------------- Helper functions -----------------------
    
    /**
     * @summary Locates tweego executable.
     *
     *  Looks in the host $PATH, otherwise uses one of the bundled in devTools/tweeGo
     */
    
    function tweeCompilerExecutable() {
    	const systemTweego = which.sync('tweego', {nothrow: true});
    	if (systemTweego) {
    		log.info('Found system tweego at ', systemTweego);
    		return systemTweego;
    	}
    	const archSuffix = os.arch() === 'x64' ? '64' : '86';
    	const platformSuffix = {
    		'Darwin': 'osx',
    		'Linux': 'nix',
    		'Windows_NT': 'win'
    	}[os.type()];
    	const extension = os.type() === 'Windows_NT' ? '.exe' : '';
    	const res = path.join('.', 'devTools', 'tweeGo', `tweego_${platformSuffix}${archSuffix}${extension}`);
    	log.info('Using bundled tweego at ', res);
    	return res;
    }
    
    
    ezsh's avatar
    ezsh committed
    /**
     * @summary Selects minify executable
     *
     * Similar to tweeCompilerExecutable(), but returns path to the minify executable. However, since
     * there are executables only for amd64, it will report error and throw (which will in turn stop
     * the build) on other architectures.
     */
    
    ezsh's avatar
    ezsh committed
    function minifierExecutable() {
    	if (os.arch() !== 'x64') {
    		log.error(chalk.red("Minification only available on amd64 systems."));
    		throw "Build interrupted";
    	}
    	const archSuffix = 'amd64';
    	const platformSuffix = {
    		'Darwin': 'darwin',
    		'Linux': 'linux',
    		'Windows_NT': 'win'
    	}[os.type()];
    	const extension = os.type() === 'Windows_NT' ? '.exe' : '';
    	const res = path.join('.', 'devTools', 'minify', `minify_${platformSuffix}_${archSuffix}${extension}`);
    	log.info('Using minifier at ', res);
    	return res;
    }
    
    
    ezsh's avatar
    ezsh committed
    /**
     * @summary Composes tweego invocation command
     *
     * Combines paths to tweego and options defined in the build.config.json file to
     * return a full tweego launch command, which will combine all story elements, pick up modules,
     * and generate an HTML file in the intermidiate directory
     */
    
    function tweeCompileCommand() {
    	let sources = [path.join(cfg.dirs.intermediate, "story")];
    	sources.push(...cfg.sources.story.media);
    
    	let modules = [path.join(cfg.dirs.intermediate, "module")];
    	let moduleArgs = modules.map(fn => `--module=${fn}`);
    	return `${tweeCompilerExecutable()} -f ${cfg.twineformat} --head=${cfg.sources.head} -o ${path.join(cfg.dirs.intermediate, htmlOut)} ${moduleArgs.join(' ')} ${sources.join(' ')}`;
    }
    
    
    ezsh's avatar
    ezsh committed
    /**
     * Creates a pipeline that sorts and combines files
     */
    
    function concatFiles(srcGlob, destDir, destFileName) {
    	return gulp.src(srcGlob)
    		.pipe(sort())
    		.pipe(concat(destFileName))
    		.pipe(gulp.dest(destDir));
    }
    
    
    ezsh's avatar
    ezsh committed
    /**
     * Creates a pipeline for processing JS scripts
     *
     * The pipeline collects sources, sorts them, concatenates and
     * saves in the destination dir. If no "release" command line switch was
     * supplied, sourcemaps will be written for the files
     */
    
    function processScripts(srcGlob, destDir, destFileName) {
    	const addSourcemaps = !args.release;
    	const prefix = path.relative(destDir, srcGlob.substr(0, srcGlob.indexOf('*')));
    	return gulp.src(srcGlob)
    		.pipe(sort())
    		.pipe(addSourcemaps ? sourcemaps.init() : noop())
    		.pipe(concat(destFileName))
    		.pipe(addSourcemaps ?
    
    ezsh's avatar
    ezsh committed
    			sourcemaps.write(args.embedsourcemaps ? undefined : '.', {
    				includeContent: args.sourcemapsincludecontent,
    				sourceRoot: prefix,
    				sourceMappingURLPrefix: path.relative(cfg.dirs.output, destDir)
    			}) :
    
    ezsh's avatar
    ezsh committed
    /**
     * Creates a pipeline for processing CSS stylesheets
     *
     * The pipeline collects sources, sorts them, concatenates, pass through
     * an autoprefixer and saves in the destination dir. If no "release" command
     * line switch was supplied, sourcemaps will be written for the files
     */
    
    function processStylesheets(srcGlob, destDir, destFileName) {
    	const addSourcemaps = !args.release;
    	const prefix = path.relative(destDir, srcGlob.substr(0, srcGlob.indexOf('*')));
    	return gulp.src(srcGlob)
    		.pipe(sort())
    		.pipe(addSourcemaps ? sourcemaps.init() : noop())
    		.pipe(concat(destFileName))
    		.pipe(cfg.options.css.autoprefix ?
    			postcss([autoprefixer({overrideBrowserslist: ['last 2 versions']})]) :
    			noop())
    		.pipe(addSourcemaps ?
    
    ezsh's avatar
    ezsh committed
    			sourcemaps.write(args.embedsourcemaps ? undefined : '.', {
    				includeContent: args.sourcemapsincludecontent,
    				sourceRoot: prefix,
    				sourceMappingURLPrefix: path.relative(cfg.dirs.output, destDir)
    			}) :
    
    ezsh's avatar
    ezsh committed
    /**
     * Creates tasks for processing sources with provided function
     *
     * This function is a workaround. Gulp can handle multiple globs at time, but
     * for writing sourcemaps we have to process them one by one, because we detect
     * where to write source maps from each glob pattern in order to make them accessible
     * from the compiled HTML
     */
    
    function processSrc(name, processorFunc, globs, destDir, destFileName, ...args) {
    	let tasks = [];
    	if (!Array.isArray(globs) || globs.length === 1) {
    		const src = Array.isArray(globs) ? globs[0] : globs;
    		tasks.push(() => processorFunc(src, destDir, destFileName, args));
    		tasks[tasks.length - 1].displayName = "process-" + name;
    	} else { // many globs
    		const ext = path.extname(destFileName);
    		const bn = path.basename(destFileName, ext);
    		for (let i = 0; i < globs.length; ++i) {
    			tasks.push(() => processorFunc(globs[i], destDir, `${bn}-${i}${ext}`, args));
    			tasks[tasks.length - 1].displayName = `process-${name}-${i}`;
    		}
    	}
    	const res = gulp.parallel(...tasks);
    	res.displayName = name;
    	return res;
    }
    
    
    ezsh's avatar
    ezsh committed
    /** Returns true if the working directory is a Git repository */
    function isGitCheckout() {
    	return fs.statSync('.git').isDirectory();
    }
    
    /** Invokes git and writes hash of the head commit to the file, specified in the 'gitVersionFile' build config property */
    
    function injectGitCommit(cb) {
    
    ezsh's avatar
    ezsh committed
    	childProcess.exec('git rev-parse --short HEAD', function(error, stdout) {
    		if (!error) {
    			log.info('current git hash: ', stdout);
    			fs.writeFile(cfg.gitVersionFile, `App.Version.commitHash = '${stdout.trimEnd()}';\n`, cb);
    
    ezsh's avatar
    ezsh committed
    			log.error(chalk.red(`Error running git. Error: ${error}`));
    
    ezsh's avatar
    ezsh committed
    /** Ensures the file with the git commit hash does not exists */
    
    function cleanupGit(cb) {
    
    	if (fs.existsSync(cfg.gitVersionFile)) {
    		fs.unlink(cfg.gitVersionFile, cb);
    	} else {
    		cb();
    	}
    
    ezsh's avatar
    ezsh committed
    // --------------- build tasks definitions -----------------
    /*
    The main build process, which produces the assembled HTML file, consists of several stages.
    The game sources, which are .twee, .js, and .css files, belong to one of the two categories:
    they are either part of the story in Tweego terms, or modules. These categories are called
    "components" below. For each component files of the same type are processed in the same way
    and results for each component/file type pair are concatenated file in the intermediate builds
    directory and a source map for that file.
    When all intermediate files are ready, tweego picks them up and assembles the HTML file.
    */
    
    // Create task to execute tweego
    gulp.task('compileStory', shell.task(tweeCompileCommand(), {env: {...process.env, ...cfg.options.twee.environment}, verbose: args.verbosity >= 3}));
    
    ezsh's avatar
    ezsh committed
    /**
     * Creates tasks for preparing intermidiate files for a component
     * @param {*} name "story" or "module"
     */
    
    function prepareComponent(name) {
    
    ezsh's avatar
    ezsh committed
    	const processors = {
    		"css": {
    			func: processStylesheets,
    			output: "-styles.css"
    		},
    		"js": {
    			func: processScripts,
    			output: "-script.js"
    		},
    		"twee": {
    			func: concatFiles,
    			output: "-story.twee"
    		},
    		"media": {
    			func: null
    		}
    	};
    
    
    	const c = cfg.sources[name];
    	const outDir = path.join(cfg.dirs.intermediate, name);
    	const subTasks = [];
    	for (const srcType in c) {
    		const proc = processors[srcType];
    		if (proc.func) {
    
    ezsh's avatar
    ezsh committed
    			subTasks.push(processSrc(`${name}-${srcType}`, proc.func, c[srcType], outDir, `${name}${proc.output}`, cfg.options[srcType]));
    
    		}
    	}
    	let r = gulp.parallel(subTasks);
    	r.displayName = "prepare-" + name;
    	return r;
    }
    
    
    ezsh's avatar
    ezsh committed
    /**
     *  Creates a task for compiling a theme
     * @param {string} themeName theme directory name
     */
    
    function makeThemeCompilationTask(themeName) {
    	const taskName = `make-theme-${themeName}`;
    	gulp.task(taskName, function() {
    
    		return concatFiles(`themes/${themeName}/**/*.css`, cfg.dirs.output, `${themeName}.css`);
    
    	});
    	return taskName;
    }
    
    
    ezsh's avatar
    ezsh committed
    /** Moves compiled HTML file from the intermidiate location to the final output */
    
    function moveHTMLInPlace(cb) {
    	fs.rename(path.join(cfg.dirs.intermediate, htmlOut), path.join(cfg.dirs.output, cfg.output), cb);
    }
    
    
    ezsh's avatar
    ezsh committed
    // If we were asked to minify the file, instead of moving invoke the minify utility with output in the final dir
    if (args.minify) { // do not touch minifierExecutable(), which throws on errors, unconditionally
    
    ezsh's avatar
    ezsh committed
    	gulp.task('moveAndMinifyHTML', shell.task(
    
    ezsh's avatar
    ezsh committed
    		`${minifierExecutable()} --html-keep-comments --js-keep-var-names -o "${path.join(cfg.dirs.output, cfg.output)}" "${path.join(cfg.dirs.intermediate, htmlOut)}"`, {verbose: args.verbosity >= 3}));
    
    ezsh's avatar
    ezsh committed
    }
    
    
    ezsh's avatar
    ezsh committed
    /** Removes intermediate compilation files if any */
    function removeIntermediateFiles(cb) {
    	if (fs.existsSync(cfg.dirs.intermediate)) {
    		fs.rm(cfg.dirs.intermediate, {recursive: true}, cb);
    	} else {
    		cb();
    
    ezsh's avatar
    ezsh committed
    // Creates task to assemble components in the intermediate dir where they are ready for tweego
    gulp.task('prepare', gulp.parallel(prepareComponent("module"), prepareComponent("story")));
    
    ezsh's avatar
    ezsh committed
    // Create the main build and clean tasks, which include writing Git commit hash if we are working in a Git repo
    if (isGitCheckout()) {
    	exports.clean = gulp.parallel(cleanupGit, removeIntermediateFiles);
    	gulp.task('buildHTML', gulp.series(exports.clean, 'prepare', injectGitCommit, 'compileStory', cleanupGit));
    
    ezsh's avatar
    ezsh committed
    	exports.clean = gulp.parallel(removeIntermediateFiles);
    	gulp.task('buildHTML', gulp.series(exports.clean, 'prepare', 'compileStory'));
    
    ezsh's avatar
    ezsh committed
    // Creates theme build task for each subdirectory in the 'themes' dir
    
    ezsh's avatar
    ezsh committed
    const themeTasks = fs.readdirSync('themes')
    	.filter(entry => fs.statSync(path.join('themes', entry)).isDirectory())
    	.map(entry => makeThemeCompilationTask(entry));
    
    ezsh's avatar
    ezsh committed
    // Create user-invocable targets for building HTML and themes
    
    ezsh's avatar
    ezsh committed
    exports.html = gulp.series('buildHTML', args.minify ? 'moveAndMinifyHTML' : moveHTMLInPlace);
    
    exports.themes = gulp.parallel(themeTasks);
    
    
    ezsh's avatar
    ezsh committed
    // Set the default target which is invoked when no target is explicitly provided by user
    
    exports.default = exports.html;
    
    ezsh's avatar
    ezsh committed
    // Convenient shortcut to build everything (HTML and themes)
    
    exports.all = gulp.parallel(exports.html, exports.themes);
    
    
    // legacy tasks
    
    gulp.task('twineCSS', function() {
    	return concatFiles([...cfg.sources.module.css, ...cfg.sources.story.css], 'devNotes', 'twine CSS.txt');
    });
    
    gulp.task('twineJS', function() {
    	return gulp.src([...cfg.sources.module.js, ...cfg.sources.story.js, '!src/art/assistantArt.js'])
    		.pipe(stripCssJSComments({trim: true}))
    		.pipe(sort())
    		.pipe(concat('twine JS.txt'))
    		.pipe(gulp.dest('devNotes'));
    });
    
    exports.twine = gulp.parallel('twineCSS', 'twineJS');