import { Server, Socket } from "net"; import browserSync = require("browser-sync"); import browserify = require("browserify"); import logger = require("fancy-log"); import { emptyDir, mkdirp, pathExists, remove } from "fs-extra"; import { dest, parallel, series, src, TaskFunction, watch } from "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 * as dartSass from "sass"; import { parseArgsStringToArgv } from "string-argv"; import tsify = require("tsify"); import { changeExt, dirname, join, parse, relative } from "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. * * @returns * The formatted message. */ const watchFinishMessage = (errorCount: number): string => { 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. * * @returns * The parsed arguments. */ function ParseArgs(args: string[]): minimist.ParsedArgs { 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(): Promise { let directories = [ "javascript", "css", "templates", "assets" ]; for (let directory of directories) { await emptyDir(settings.RootPath(directory)); } if (await pathExists(settings.TestThemePath())) { await remove(settings.TestThemePath()); } await mkdirp(settings.TestThemePath()); await remove(settings.TestThemePath()); // eslint-disable-next-line @typescript-eslint/no-var-requires await require("create-symlink")(settings.RootPath(), settings.TestThemePath(), { type: "junction" }); } Clean.description = "Cleans the project."; /** * Builds the project in watched mode. * * @param done * A callback which is executed once the task has finished. */ 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`. * * @param filePath * A glob-path which points to the files which must be reloaded. * * @returns * The actual task. */ 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(): Promise { return new Promise( (resolve, reject) => { if (settings.Watch) { syncer.init({ open: false, proxy: "http://localhost", port: 3000, ui: { port: 3001 }, ghostMode: false, online: false }); watch(settings.ThemeSource("**"), { usePolling: true }, series(Theme, BrowserSync("*.css"))); watch(settings.TemplateSource("**"), { usePolling: true }, series(Templates, BrowserSync())); } parallel(Library, Theme, Templates)( (error) => { if (error) { reject(error); } else { resolve(); } }); }); } /** * Builds the TypeScript- and JavaScript-library. * * @returns * The pipeline to execute. */ export function Library(): NodeJS.ReadWriteStream { let errorMessages: string[] = []; let streams: NodeJS.ReadWriteStream[] = []; let queue: NodeJS.ReadWriteStream[] = []; let tsConfigFile = settings.TypeScriptProjectRoot("tsconfig.json"); // eslint-disable-next-line @typescript-eslint/no-var-requires let tsConfig = require(tsConfigFile); let optionBase: browserify.Options = { ...Watchify.args, node: true, ignoreMissing: true, debug: settings.Debug }; let files = (tsConfig.files as string[]).map( (file) => relative(settings.TypeScriptSourceRoot(), settings.TypeScriptProjectRoot(file))); if (settings.Watch) { logger.info(watchStartMessage); } for (let file of files) { let builder = (): NodeJS.ReadWriteStream => { let stream: NodeJS.ReadWriteStream; let bundler = browserify( { ...optionBase, basedir: settings.LibraryPath(dirname(file)), entries: [ settings.TypeScriptSourceRoot(file) ], standalone: join(dirname(file), parse(file).name) }); if (settings.Watch) { bundler = Watchify(bundler, { poll: true }); } bundler.plugin( tsify, { project: tsConfigFile }); stream = bundler.bundle().on( "error", (error) => { let message: string = error.message; if (!errorMessages.includes(message)) { let result = new RegExp(`^(${error["fileName"]})\\((\\d+|\\d+(,\\d+){1,3})\\): .* TS([\\d]+): (.*)$`).exec(message); errorMessages.push(message); console.log(`${relative(process.cwd(), result[1])}(${result[2]}): ${result[4]} ${result[5]}`); } } ).pipe( vinylSourceStream(changeExt(file, "js")) ).pipe( buffer() ).pipe( gulpIf( !settings.Debug, terser() ) ).pipe( dest(settings.LibraryPath()) ).on( "end", () => { if (settings.Watch) { if (queue.includes(stream)) { queue.splice(queue.indexOf(stream), 1); } if (queue.length === 0) { logger.info(watchFinishMessage(errorMessages.length)); if (errorMessages.length === 0) { syncer.reload("*.js"); } errorMessages.splice(0, errorMessages.length); } } }); if (settings.Watch) { bundler.once( "update", () => { console.log(`Update called for ${file}: ${queue.length}`); if (queue.length === 0) { logger.info(incrementalMessage); } queue.push(builder()); }); } return stream; }; let stream = builder(); queue.push(stream); streams.push(stream); } return merge(streams); } Library.description = "Builds the TypeScript- and JavaScript-library."; /** * Builds the theme. * * @returns * The pipeline to execute. */ export function Theme(): NodeJS.ReadWriteStream { if (settings.Watch) { logger.info("Building scss-code."); } return src( settings.ThemeSource("main.scss"), { sourcemaps: settings.Debug, base: settings.StylePath() }).pipe( sass(dartSass).sync( { importer: require("node-sass-tilde-importer") } ).on("error", (error) => { console.log( JSON.stringify( { status: error.status, file: error.file, line: error.line, column: error.column, message: error.messageOriginal, formatted: error.formatted }, null, 4)); }) ).pipe( rename( (parsedPath) => { parsedPath.dirname = ""; parsedPath.basename = "mantra"; }) ).pipe( dest( settings.StylePath(), settings.Debug ? { sourcemaps: true } : undefined) ).on( "end", () => { if (settings.Watch) { logger.info("Building scss-code finished."); } }); } Theme.description = "Builds the theme."; /** * Builds the templates. * * @returns * The pipeline to execute. */ export function Templates(): NodeJS.ReadWriteStream { return src( settings.TemplateSource("**")).pipe( dest(settings.TemplatePath())); } Templates.description = "Builds the templates."; /** * Stops a watch-task. */ export async function Stop(): Promise { 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 { logger.info("The specified task is not running."); } } Stop.description = "Stops a watch-task";