Newer
Older
/* eslint-disable one-var */
const autoprefixer = require('autoprefixer'),
chalk = require('chalk'),
concat = require('gulp-concat'),
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'),
which = require('which'),
fs = require('fs'),
path = require('path'),
os = require('os'),
yargs = require('yargs'),
cfg = require('./build.config.json');
// defines build options which can be supplied from the command line
// example: npx gulp --minify --verbosity 6
const args = yargs.options({
release: {type: 'boolean', default: false},
embedsourcemaps: {type: 'boolean', default: false},
sourcemapsincludecontent: {type: 'boolean', default: false}
}).argv;
/** Filename for the temporary output. Tweego will write here and then it will be moved into the output dir */
const htmlOut = "tmp.html";
log(args.verbosity);
// -------------- 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;
}
/**
* @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.
*/
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;
}
/**
* @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));
}
/**
* Creates a pipeline that sorts and combines files
*/
function concatFiles(srcGlob, destDir, destFileName) {
return gulp.src(srcGlob)
.pipe(concat(destFileName))
.pipe(gulp.dest(destDir));
}
/**
* 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(addSourcemaps ? sourcemaps.init() : noop())
.pipe(concat(destFileName))
.pipe(addSourcemaps ?
sourcemaps.write(args.embedsourcemaps ? undefined : '.', {
includeContent: args.sourcemapsincludecontent,
sourceRoot: prefix,
sourceMappingURLPrefix: path.relative(cfg.dirs.output, destDir)
}) :
noop())
.pipe(gulp.dest(destDir));
}
/**
* 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(addSourcemaps ? sourcemaps.init() : noop())
.pipe(concat(destFileName))
.pipe(cfg.options.css.autoprefix ?
postcss([autoprefixer({overrideBrowserslist: ['last 2 versions']})]) :
noop())
.pipe(addSourcemaps ?
sourcemaps.write(args.embedsourcemaps ? undefined : '.', {
includeContent: args.sourcemapsincludecontent,
sourceRoot: prefix,
sourceMappingURLPrefix: path.relative(cfg.dirs.output, destDir)
}) :
noop())
.pipe(gulp.dest(destDir));
}
/**
* 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;
}
/** 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) {
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);
cb();
}
});
}
/** 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();
}
// --------------- 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}));
/**
* Creates tasks for preparing intermidiate files for a component
* @param {*} name "story" or "module"
*/
function prepareComponent(name) {
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) {
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;
}
/**
* 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`);
/** 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);
}
// 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
`${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}));
/** 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();
// Creates task to assemble components in the intermediate dir where they are ready for tweego
gulp.task('prepare', gulp.parallel(prepareComponent("module"), prepareComponent("story")));
// 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));
exports.clean = gulp.parallel(removeIntermediateFiles);
gulp.task('buildHTML', gulp.series(exports.clean, 'prepare', 'compileStory'));
// Creates theme build task for each subdirectory in the 'themes' dir
const themeTasks = fs.readdirSync('themes')
.filter(entry => fs.statSync(path.join('themes', entry)).isDirectory())
.map(entry => makeThemeCompilationTask(entry));
exports.html = gulp.series('buildHTML', args.minify ? 'moveAndMinifyHTML' : moveHTMLInPlace);
exports.themes = gulp.parallel(themeTasks);
// Set the default target which is invoked when no target is explicitly provided by user
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}))