mantra/gulpfile.ts

479 lines
12 KiB
TypeScript

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 { 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<void>
{
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<void>
{
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(
{
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<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
{
logger.info("The specified task is not running.");
}
}
Stop.description = "Stops a watch-task";