mantra/gulpfile.ts

479 lines
13 KiB
TypeScript

import { Server, Socket } from "net";
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 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.
*
* @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<void>
{
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());
// 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<void>
{
return new Promise(
(resolve, reject) =>
{
if (settings.Watch)
{
log.info(watchStartMessage);
syncer.init({
open: false,
proxy: "http://localhost",
port: 3000,
ui: {
port: 3001
},
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()));
}
gulp.parallel(Library, Theme, Templates)(
(error) =>
{
if (error)
{
reject(error);
}
else
{
resolve();
}
});
});
}
/**
* Builds the TypeScript- and JavaScript-library.
*
* @returns
* The pipeline to execute.
*/
export async function Library(): Promise<NodeJS.ReadWriteStream>
{
let streams: Array<Promise<NodeJS.ReadWriteStream>> = [];
let queue = new PromiseQueue();
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 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(
// eslint-disable-next-line @typescript-eslint/no-var-requires
require("tsify"),
{
project: tsConfigFile
});
let bundle = async (): Promise<NodeJS.ReadWriteStream> =>
{
return new Promise<NodeJS.ReadWriteStream>(
(resolve) =>
{
let 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(`${Path.relative(process.cwd(), result[1])}(${result[2]}): ${result[4]} ${result[5]}`);
}
}
).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 = (): Promise<NodeJS.ReadWriteStream> => 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.
*
* @returns
* The pipeline to execute.
*/
export async function Theme(): Promise<NodeJS.ReadWriteStream>
{
if (settings.Watch)
{
console.log("Rebuilding scss-code.");
}
return gulp.src(
settings.ThemeSource("main.scss"),
{
sourcemaps: settings.Debug,
base: settings.StylePath()
}).pipe(
sass(
{
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(
gulp.dest(
settings.StylePath(),
settings.Debug ?
{
sourcemaps: true
} :
undefined)
).on(
"end",
() =>
{
if (settings.Watch)
{
console.log("Rebuilding scss-code finished.");
}
});
}
Theme.description = "Builds the theme.";
/**
* Builds the templates.
*
* @returns
* The pipeline to execute.
*/
export function Templates(): NodeJS.ReadWriteStream
{
return gulp.src(
settings.TemplateSource("**")).pipe(
gulp.dest(settings.TemplatePath()));
}
Templates.description = "Builds the templates.";
/**
* Stops a watch-task.
*/
export async function Stop(): Promise<void>
{
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";