Skip to content
Snippets Groups Projects
gulpfile.js 12.1 KiB
Newer Older
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()} --head=${cfg.sources.head} -o ${path.join(cfg.dirs.intermediate, htmlOut)} ${moduleArgs.join(' ')} ${sources.join(' ')}`;
/**
 * gulp-sort uses String.localeCompare() by default, which may be case-insensitive,
 * while we require case -sensitive sorting for sources
 */
function pathcmp(a, b) {
	return (a.path < b.path ? -1 : (a.path > b.path ? 1 : 0));
}

ezsh's avatar
ezsh committed
/**
 * Creates a pipeline that sorts and combines files
 */
function concatFiles(srcGlob, destDir, destFileName) {
	return gulp.src(srcGlob)
		.pipe(sort(pathcmp))
		.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(pathcmp))
		.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(pathcmp))
		.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(pathcmp))
		.pipe(concat('twine JS.txt'))
		.pipe(gulp.dest('devNotes'));
});

exports.twine = gulp.parallel('twineCSS', 'twineJS');