Skip to content
Snippets Groups Projects
gulpfile.js 11.9 KiB
Newer Older
  • Learn to ignore specific revisions
  • Arkerthan's avatar
    Arkerthan committed
    import {config as minifyConfig, string as minifyString, file as minifyFile} from '@tdewolff/minify';
    
    import yargs from 'yargs';
    import {hideBin} from 'yargs/helpers';
    import autoprefixer from "autoprefixer";
    import chalk from "chalk";
    import gulp from "gulp";
    import concat from "gulp-concat";
    import log from 'fancy-log-levels';
    import noop from "gulp-noop";
    import postcss from "gulp-postcss";
    import shell from "gulp-shell";
    import sort from "gulp-sort";
    import sourcemaps from "gulp-sourcemaps";
    import stripCssJSComments from "gulp-strip-comments";
    import childProcess from "child_process";
    import which from "which";
    import fs from "fs";
    import path from "path";
    import os from "os";
    
    Arkerthan's avatar
    Arkerthan committed
    import through from 'through2';
    
    import cfg from './build.config.json' assert {type: "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(hideBin(process.argv)).options({
    
    ezsh's avatar
    ezsh committed
    	verbosity: {type: 'number', default: 1},
    
    	release: {type: 'boolean', default: false},
    
    Arkerthan's avatar
    Arkerthan committed
    	ci: {type: 'boolean', default: false}, // assumes gitlab CI environment
    
    ezsh's avatar
    ezsh committed
    	minify: {type: 'boolean', default: false},
    
    	embedsourcemaps: {type: 'boolean', default: false},
    	sourcemapsincludecontent: {type: 'boolean', default: false}
    }).argv;
    
    
    minifyConfig({"html-keep-comments": true, "js-keep-var-names": true});
    
    
    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 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)
    			}) :
    
    Arkerthan's avatar
    Arkerthan committed
    		// minify JS. Mostly for the src/ directory, which later minifying won't be able to access
    		.pipe(args.minify ? through.obj((chunk, enc, cb) => {
    			chunk.contents = new Buffer(minifyString("text/javascript", chunk.contents.toString()));
    			cb(null, chunk);
    		}) : noop())
    
    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) {
    
    Arkerthan's avatar
    Arkerthan committed
    	// check if we are in CI mode, if yes, just read out the hash from environment variables
    	const readGitHash = args.ci ? "echo $CI_COMMIT_SHORT_SHA" : 'git rev-parse --short HEAD';
    	childProcess.exec(readGitHash, function(error, stdout) {
    
    ezsh's avatar
    ezsh committed
    		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
    
    Arkerthan's avatar
    Arkerthan committed
    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);
    }
    
    
    function moveAndMinifyHTML(cb) {
    	minifyFile('text/html', path.join(cfg.dirs.intermediate, htmlOut), path.join(cfg.dirs.output, cfg.output));
    	cb();
    
    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
    
    let cleanOp = noop();
    
    ezsh's avatar
    ezsh committed
    if (isGitCheckout()) {
    
    	cleanOp = gulp.parallel(cleanupGit, removeIntermediateFiles);
    	gulp.task('buildHTML', gulp.series(cleanOp, injectGitCommit, 'prepare', 'compileStory', cleanupGit));
    
    Arkerthan's avatar
    Arkerthan committed
    } else if (args.ci) {
    	// CI environment is already clean
    
    	gulp.task('buildHTML', gulp.series(injectGitCommit, 'prepare', 'compileStory'));
    
    	cleanOp = gulp.parallel(removeIntermediateFiles);
    	gulp.task('buildHTML', gulp.series(cleanOp, 'prepare', 'compileStory'));
    
    export const clean = cleanOp;
    
    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
    
    export const html = gulp.series('buildHTML', args.minify ? moveAndMinifyHTML : moveHTMLInPlace);
    export const themes = gulp.parallel(themeTasks);
    
    ezsh's avatar
    ezsh committed
    // Set the default target which is invoked when no target is explicitly provided by user
    
    export default html;
    
    ezsh's avatar
    ezsh committed
    // Convenient shortcut to build everything (HTML and themes)
    
    export const all = gulp.parallel(html, 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'));
    });
    
    
    export const twine = gulp.parallel('twineCSS', 'twineJS');