Newer
Older
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";
import cfg from './build.config.json' assert {type: "json"};
// defines build options which can be supplied from the command line
// example: npx gulp --minify --verbosity 6
const args = yargs(hideBin(process.argv)).options({
release: {type: 'boolean', default: false},
ci: {type: 'boolean', default: false}, // assumes gitlab CI environment
embedsourcemaps: {type: 'boolean', default: false},
sourcemapsincludecontent: {type: 'boolean', default: false}
}).argv;
minifyConfig({"html-keep-comments": true, "js-keep-var-names": true});
/** 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 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)
}) :
// 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())
.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) {
// 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) {
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);
}
function moveAndMinifyHTML(cb) {
minifyFile('text/html', path.join(cfg.dirs.intermediate, htmlOut), path.join(cfg.dirs.output, cfg.output));
cb();
/** 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
cleanOp = gulp.parallel(cleanupGit, removeIntermediateFiles);
gulp.task('buildHTML', gulp.series(cleanOp, injectGitCommit, 'prepare', 'compileStory', cleanupGit));
} 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'));
// 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));
export const html = gulp.series('buildHTML', args.minify ? moveAndMinifyHTML : moveHTMLInPlace);
export const themes = gulp.parallel(themeTasks);
// Set the default target which is invoked when no target is explicitly provided by user
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(concat('twine JS.txt'))
.pipe(gulp.dest('devNotes'));
});
export const twine = gulp.parallel('twineCSS', 'twineJS');