479 lines
12 KiB
TypeScript
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";
|