mantra/gulpfile.ts

410 lines
11 KiB
TypeScript

import browserSync = require("browser-sync");
import browserify = require("browserify");
import log = require("fancy-log");
import FileSystem = require("fs-extra");
import { TaskFunction } from "gulp";
import gulp = require("gulp");
import gulpIf = require("gulp-if");
import rename = require("gulp-rename");
import sass = require("gulp-sass");
import terser = require("gulp-terser");
import merge = require("merge-stream");
import minimist = require("minimist");
import { Server, Socket } from "net";
import PromiseQueue = require("promise-queue");
import { parseArgsStringToArgv } from "string-argv";
import Path = require("upath");
import buffer = require("vinyl-buffer");
import vinylSourceStream = require("vinyl-source-stream");
import Watchify = require("watchify");
import { Settings } from "./.gulp/Settings";
import "./.gulp/TaskFunction";
/**
* The port to listen for stop-requests.
*/
const watchConnectorPort = 50958;
/**
* An object for syncing browsers.
*/
let syncer = browserSync.create();
/**
* The message that is printed when starting the compilation in watch mode.
*/
const watchStartMessage = "Starting compilation in watch mode...";
/**
* The message that is printed when starting an incremental compilation.
*/
const incrementalMessage = "File change detected. Starting incremental compilation...";
/**
* Generates the message that is printed after finishing a compilation in watch mode.
*
* @param errorCount
* The number of errors which occurred.
*/
const watchFinishMessage = (errorCount: number) =>
{
return `Found ${errorCount} errors. Watching for file changes.`;
};
/**
* The arguments passed by the user.
*/
let options = ParseArgs(process.argv.slice(2));
/**
* Parses the specified arguments.
*
* @param args
* The arguments to parse.
*/
function ParseArgs(args: string[])
{
return minimist(
args,
{
string: [
"target"
],
alias: {
target: "t"
},
default: {
target: "Debug"
}
});
}
/**
* The settings for building the project.
*/
let settings = new Settings(options["target"]);
/**
* Cleans the project.
*/
export async function Clean()
{
let directories = [
"javascript",
"css",
"templates",
"assets"
];
for (let directory of directories)
{
await FileSystem.emptyDir(settings.RootPath(directory));
}
if (await FileSystem.pathExists(settings.TestThemePath()))
{
await FileSystem.remove(settings.TestThemePath());
}
await FileSystem.mkdirp(settings.TestThemePath());
await FileSystem.remove(settings.TestThemePath());
await require("create-symlink")(settings.RootPath(), settings.TestThemePath(), { type: "junction" });
}
Clean.description = "Cleans the project.";
/**
* Builds the project in watched mode.
*/
export let Watch: TaskFunction = (done) =>
{
settings.Watch = true;
Build();
let server = new Server(
(socket) =>
{
socket.on(
"data",
(data) =>
{
let args = parseArgsStringToArgv(data.toString());
socket.destroy();
if (args[0] === "stop")
{
let options = ParseArgs(args.slice(1));
if (options["target"] === settings.Target)
{
syncer.exit();
server.close();
done();
process.exit();
}
}
});
});
server.listen(watchConnectorPort);
};
Watch.description = "Builds the project in watched mode.";
/**
* Reloads all browsers using `browser-sync`.
*/
function BrowserSync(filePath?: string): TaskFunction
{
let BrowserSync: TaskFunction = (done) =>
{
if (filePath)
{
syncer.reload(filePath);
}
else
{
syncer.reload();
}
done();
};
return BrowserSync;
}
/**
* Builds the project.
*/
export async function Build()
{
if (settings.Watch)
{
log.info(watchStartMessage);
syncer.init({
open: false,
proxy: "http://localhost",
port: 3000,
ghostMode: false,
online: false
});
gulp.watch(settings.ThemeSource("**"), gulp.series(Theme, BrowserSync("*.css")));
gulp.watch(settings.TemplateSource("**"), gulp.series(Templates, BrowserSync()));
}
await Promise.all(
[
Library(),
Theme(),
Templates()
]);
}
/**
* Builds the TypeScript- and JavaScript-library.
*/
export async function Library()
{
let streams: Array<Promise<NodeJS.ReadWriteStream>> = [];
let queue = new PromiseQueue();
let tsConfigFile = settings.TypeScriptProjectRoot("tsconfig.json");
let tsConfig = require(tsConfigFile);
let optionBase: browserify.Options = {
...Watchify.args,
node: true,
ignoreMissing: true,
debug: settings.Debug
};
{
let errorMessages: string[] = [];
let files = (tsConfig.files as string[]).map(
(file) => Path.relative(settings.TypeScriptSourceRoot(), settings.TypeScriptProjectRoot(file)));
for (let file of files)
{
let bundler = browserify(
{
...optionBase,
basedir: settings.LibraryPath(Path.dirname(file)),
entries: [
settings.TypeScriptSourceRoot(file)
],
standalone: Path.join(Path.dirname(file), Path.parse(file).name)
});
if (settings.Watch)
{
bundler = Watchify(bundler);
}
bundler.plugin(
require("tsify"),
{
project: tsConfigFile
});
let bundle = async () =>
{
return new Promise<NodeJS.ReadWriteStream>(
(resolve) =>
{
let stream = bundler.bundle().on(
"error",
(error) =>
{
let message: string = error.message;
if (!errorMessages.includes(message))
{
errorMessages.push(message);
log.error(message);
}
}
).pipe(
vinylSourceStream(Path.changeExt(file, "js"))
).pipe(
buffer()
).pipe(
gulpIf(
!settings.Debug,
terser()
)
).pipe(
gulp.dest(settings.LibraryPath())
);
stream.on(
"end",
() =>
{
if (settings.Watch && ((queue.getQueueLength() + queue.getPendingLength()) === 1))
{
if (errorMessages.length === 0)
{
syncer.reload("*.js");
}
log.info(watchFinishMessage(errorMessages.length));
}
errorMessages.splice(0, errorMessages.length);
resolve(stream);
});
});
};
if (settings.Watch)
{
bundler.on(
"update",
() =>
{
if ((queue.getQueueLength() + queue.getPendingLength()) === 0)
{
log.info(incrementalMessage);
}
queue.add(
async () =>
{
return bundle();
});
});
}
let build = () => queue.add(bundle);
build.displayName = Build.displayName;
build.description = Build.description;
streams.push(build());
}
}
return merge(await Promise.all(streams)) as NodeJS.ReadWriteStream;
}
Library.description = "Builds the TypeScript- and JavaScript-library.";
/**
* Builds the theme.
*/
export async function Theme()
{
return gulp.src(
settings.ThemeSource("main.scss"),
{
sourcemaps: settings.Debug,
base: settings.StylePath()
}).pipe(
sass(
{
importer: require("node-sass-tilde-importer"),
outputStyle: settings.Debug ? "expanded" : "compressed"
})
).pipe(
rename(
(parsedPath) =>
{
parsedPath.dirname = "";
parsedPath.basename = "mantra";
})
).pipe(
gulp.dest(
settings.StylePath(),
settings.Debug ?
{
sourcemaps: true
}
:
undefined)
);
}
Theme.description = "Builds the theme.";
/**
* Builds the templates.
*/
export function Templates()
{
return gulp.src(
settings.TemplateSource("**")).pipe(
gulp.dest(settings.TemplatePath()));
}
Templates.description = "Builds the templates.";
/**
* Stops a watch-task.
*/
export async function Stop()
{
try
{
await new Promise(
(resolve, reject) =>
{
let client = new Socket();
client.connect(
watchConnectorPort,
"localhost",
async () =>
{
client.write(`stop -t ${settings.Target}`);
});
client.on("close", resolve);
client.on("error", reject);
});
}
catch
{
log.info("The specified task is not running.");
}
}
Stop.description = "Stops a watch-task";