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("**"), { usePolling: true }, gulp.series(Theme, BrowserSync("*.css"))); gulp.watch(settings.TemplateSource("**"), { usePolling: true }, gulp.series(Templates, BrowserSync())); } await Promise.all( [ Library(), Theme(), Templates() ]); } /** * Builds the TypeScript- and JavaScript-library. */ export async function Library() { let streams: Array> = []; 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, { poll: true }); } bundler.plugin( require("tsify"), { project: tsConfigFile }); let bundle = async () => { return new Promise( (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" } ).on("error", (error) => { log.error( `${error.relativePath}:${error.line}:${error.column}\n` + `${error.messageFormatted}`); }) ).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";