From ef6144ba29c8f6fb5bf47dc31e3e441cc6497418 Mon Sep 17 00:00:00 2001 From: wartoshika Date: Mon, 19 Nov 2018 18:49:40 +0100 Subject: [PATCH] Added support for watching over file changes in the source directory to automaticly trigger the transpile process --- CHANGELOG.md | 2 +- package-lock.json | 60 ++++++++---- package.json | 2 + src/cli/CommandLine.ts | 80 ++++++++++++++-- src/cli/FileWatcher.ts | 100 ++++++++++++++++++++ src/cli/Logger.ts | 38 ++++++++ src/index.ts | 25 +++-- test/integration/IntegrationTestBase.ts | 1 + test/integration/LuaIntegrationTest.spec.ts | 2 +- 9 files changed, 271 insertions(+), 39 deletions(-) create mode 100644 src/cli/FileWatcher.ts create mode 100644 src/cli/Logger.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ee4db9..b99144f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ The qhun-transpiler project uses [Semantic Versioning](https://semver.org/spec/v ## Implemented but unreleased -- Nothing +- Added support for watching over file changes in the source directory to automaticly trigger the transpile process. `-w` flag on the cli. ## **0.7.2** released 2018-11-18 diff --git a/package-lock.json b/package-lock.json index f3ad912..2fac474 100644 --- a/package-lock.json +++ b/package-lock.json @@ -139,6 +139,15 @@ "integrity": "sha512-/xUgezxxYePeXhg5S04hUjxG9JZi+rJTs1+4NwpYPfSaS7BeDa6tVJkH6lN9Cb6rl8d24Fi2uX0s0Ngg2JT6gg==", "dev": true }, + "@types/md5": { + "version": "2.1.33", + "resolved": "https://registry.npmjs.org/@types/md5/-/md5-2.1.33.tgz", + "integrity": "sha512-8+X960EtKLoSblhauxLKy3zzotagjoj3Jt1Tx9oaxUdZEPIBl+mkrUz6PNKpzJgkrKSN9YgkWTA29c0KnLshmA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/mkdirp": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/@types/mkdirp/-/mkdirp-0.5.2.tgz", @@ -1311,6 +1320,12 @@ "integrity": "sha512-9ZTaoBaePSCFvNlNGrsyI8ZVACP2svUtq0DkM7t4K2ClAa96sqOIRjAzDTc8zXzFt1cZR46rRzLTiHFSJ+Qw0g==", "dev": true }, + "charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=", + "dev": true + }, "check-error": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", @@ -1823,6 +1838,12 @@ "which": "^1.2.9" } }, + "crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=", + "dev": true + }, "crypto-browserify": { "version": "3.12.0", "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", @@ -2487,14 +2508,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2509,20 +2528,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -2639,8 +2655,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -2652,7 +2667,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -2667,7 +2681,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -2675,14 +2688,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -2701,7 +2712,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -2782,8 +2792,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -2795,7 +2804,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -2917,7 +2925,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -3899,6 +3906,17 @@ "object-visit": "^1.0.0" } }, + "md5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz", + "integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=", + "dev": true, + "requires": { + "charenc": "~0.0.1", + "crypt": "~0.0.1", + "is-buffer": "~1.1.1" + } + }, "md5.js": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", diff --git a/package.json b/package.json index a4fe2a3..0e1e536 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@types/chai": "^4.1.4", "@types/command-line-args": "^5.0.0", "@types/command-line-usage": "^5.0.1", + "@types/md5": "^2.1.33", "@types/mkdirp": "^0.5.2", "@types/mocha": "^5.2.4", "@types/mock-fs": "^3.6.30", @@ -58,6 +59,7 @@ "command-line-args": "^5.0.2", "command-line-usage": "^5.0.5", "mkdirp": "^0.5.1", + "md5": "^2.2.1", "typescript": "^3.0.3", "typescript-mix": "^3.1.3" } diff --git a/src/cli/CommandLine.ts b/src/cli/CommandLine.ts index c80e2bf..f8aa9b3 100644 --- a/src/cli/CommandLine.ts +++ b/src/cli/CommandLine.ts @@ -10,6 +10,8 @@ import { Compiler } from "../compiler/Compiler"; import { CommandLineColors } from "./CommandLineColors"; import { ValidationError } from "../error/ValidationError"; import { ExternalModuleService } from "../compiler/ExternalModuleService"; +import { FileWatcher } from "./FileWatcher"; +import { Logger } from "./Logger"; // tslint:disable-next-line const packageJson = require("../../package.json"); @@ -18,7 +20,8 @@ declare type ProgramArguments = { help: boolean, project: string, target: string, - file: string + file: string, + watch: boolean }; export class CommandLine { @@ -49,6 +52,11 @@ export class CommandLine { type: String, description: "The file that shoule be transpiled", typeLabel: "" + }, { + name: "watch", + alias: "w", + type: Boolean, + description: "Watches over edited and created files to automaticly trigger the transpiling process" } ]; @@ -57,6 +65,16 @@ export class CommandLine { */ private programArguments: ProgramArguments; + /** + * the current project reference + */ + private currentProject: Project; + + /** + * the file watcher instance + */ + private fileWatcher: FileWatcher; + /** * @param args the arguments */ @@ -71,9 +89,9 @@ export class CommandLine { } /** - * executes the command line tool with the given arguments + * prepares the execution of the transpiling process */ - public execute(): boolean { + public prepare(): boolean { // evaluate help page if (this.programArguments.help || this.args.length === 0) { @@ -89,17 +107,49 @@ export class CommandLine { return false; } + // print an execution header + this.printProgramExecuteInfo(); + + // save project reference + this.currentProject = project; + + // watch for file changes + if (this.programArguments.watch) { + + this.fileWatcher = new FileWatcher(this.currentProject, this.execute.bind(this)); + } + } + + /** + * executes the command line tool with the given arguments + */ + public execute(): boolean { + + // check if state is prepared + if (!this.currentProject) { + return false; + } + // construct the compiler - const compiler = new Compiler(project); + const compiler = new Compiler(this.currentProject); // start everything else - const result = compiler.compile(project.parsedCommandLine.fileNames); + const result = compiler.compile(this.currentProject.parsedCommandLine.fileNames); // print the final result this.printResult(result); return result !== false; } + + /** + * test if the cli is watching over files + */ + public isWatchingFiles(): boolean { + + return !!this.fileWatcher; + } + /** * get the project config from either json reader or argument reader */ @@ -132,7 +182,7 @@ export class CommandLine { if (e instanceof ValidationError) { // write the error - console.error(`${CommandLineColors.RED}${e.message}${CommandLineColors.RESET}`); + Logger.error(e.message, undefined, CommandLineColors.RED); // no transpiling! return false; @@ -177,14 +227,26 @@ export class CommandLine { // only print something about external modules when some are referenced if (embededModules.length > 0) { embededModules.forEach(moduleName => { - console.log(`${CommandLineColors.GREEN}%s${CommandLineColors.RESET}`, `Added ${moduleName} as external module.`); + Logger.log(`Added ${moduleName} as external module.`, undefined, CommandLineColors.GREEN); }); } - console.log(`${CommandLineColors.GREEN}%s${CommandLineColors.RESET}`, `Successfully transpiled ${result} files.`); + Logger.log(`Successfully transpiled ${result} files.`, undefined, CommandLineColors.GREEN); } else { - console.log(`${CommandLineColors.RED}%s${CommandLineColors.RESET}`, `An error occured while transpiling your files.`); + Logger.log(`An error occured while transpiling your files.`, undefined, CommandLineColors.GREEN); } } + /** + * print some program metadata for the command line + */ + private printProgramExecuteInfo(): void { + + // read package.json file + const packageObject = packageJson; + + Logger.log(); + Logger.log(`${packageObject.name} (${packageObject.version})`, ""); + Logger.log(); + } } diff --git a/src/cli/FileWatcher.ts b/src/cli/FileWatcher.ts new file mode 100644 index 0000000..ce54487 --- /dev/null +++ b/src/cli/FileWatcher.ts @@ -0,0 +1,100 @@ +import { Project } from "../config/Project"; +import * as fs from "fs"; +import * as path from "path"; +import * as md5 from "md5"; +import { Logger } from "./Logger"; + +export class FileWatcher { + + private watchedRootPath: string; + + /** + * contains current hash for files because multiple events throw + * when changing a file + */ + private fileContentHash: { + [fileName: string]: { + fsWait: boolean, + md5: string + } + } = {}; + + constructor( + private project: Project, + private executeOnChange: (filenameFromSourceRoot: string) => any + ) { + + // get watching directory + this.watchedRootPath = this.getPathToWatch(); + + // start fs watching + fs.watch(this.watchedRootPath, { + recursive: true + }, this.onFileChange.bind(this)); + + // print info + Logger.log("Starting to watch files in " + this.watchedRootPath); + } + + /** + * builds the path to watch for file changes + */ + private getPathToWatch(): string { + + return path.resolve(...[ + this.project.rootDir, this.project.stripOutDir ? this.project.stripOutDir : undefined + ]); + } + + /** + * the handler for file changes + * @param event the event name + * @param filename the filename that has been changed + */ + private onFileChange(event: string, filename: string): void { + + if (event !== "change" || !filename) { + return; + } + + // get md5 from file + const buffer = fs.readFileSync(path.join(this.watchedRootPath, filename)); + const fileMd5 = md5(buffer); + + // prepare fileContentHash + this.fileContentHash[filename] = this.fileContentHash[filename] || { + fsWait: false, + md5: undefined + }; + + // check if file content realy has been changed + if (this.fileContentHash[filename].fsWait === false && this.fileContentHash[filename].md5 !== fileMd5) { + + // yes, changed. + this.executeFileChangeHandler(filename); + + // update md5 + this.fileContentHash[filename] = { + md5: fileMd5, + fsWait: true + }; + + // set fsWait to false + setTimeout(() => { + this.fileContentHash[filename].fsWait = false; + }, 50); + } + } + + /** + * executes the handler for file changes + * @param filename the filename that has been changed + */ + private executeFileChangeHandler(filename: string): void { + + // print some info + Logger.log("Detected file change: " + filename + " > Transpiling started."); + + this.executeOnChange(filename); + } +} diff --git a/src/cli/Logger.ts b/src/cli/Logger.ts new file mode 100644 index 0000000..b1232d6 --- /dev/null +++ b/src/cli/Logger.ts @@ -0,0 +1,38 @@ +import { CommandLineColors } from "./CommandLineColors"; + +export class Logger { + + /** + * logs text onto the console + */ + public static log(message: string = "", prefix: string = "> ", color?: CommandLineColors): void { + + if (message === "") { + prefix = ""; + } + + if (color) { + const textToLog = `${color}%s${CommandLineColors.RESET}`; + console.log(textToLog, `${prefix}${message}`); + } else { + console.log(`${prefix}${message}`); + } + } + + /** + * logs error text onto the console + */ + public static error(message: string = "", prefix: string = "> ", color?: CommandLineColors): void { + + if (message === "") { + prefix = ""; + } + + if (color) { + const textToLog = `${color}%s${CommandLineColors.RESET}`; + console.error(textToLog, `${prefix}${message}`); + } else { + console.error(`${prefix}${message}`); + } + } +} diff --git a/src/index.ts b/src/index.ts index 8903718..dffccd8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,22 @@ import { CommandLine } from "./cli/CommandLine"; -const result = new CommandLine(process.argv.splice(2)).execute(); +// create new cli instance +const cli = new CommandLine(process.argv.splice(2)); -// exit the program with the correct exit status -if (!result) { - process.exit(1); -} +// prepare env +cli.prepare(); + +// get result +const result = cli.execute(); + +// dont exit the program if there are file watchers +if (!cli.isWatchingFiles()) { -// everything worked fine -process.exit(0); + // exit the program with the correct exit status + if (!result) { + process.exit(1); + } + + // everything worked fine + process.exit(0); +} diff --git a/test/integration/IntegrationTestBase.ts b/test/integration/IntegrationTestBase.ts index 7f4c6a2..724f1c6 100644 --- a/test/integration/IntegrationTestBase.ts +++ b/test/integration/IntegrationTestBase.ts @@ -69,6 +69,7 @@ export class IntegrationTestBase extends Test { const cli = new CommandLine([ "-p", "testTranspilerConfig.json" ]); + cli.prepare(); return cli.execute(); } diff --git a/test/integration/LuaIntegrationTest.spec.ts b/test/integration/LuaIntegrationTest.spec.ts index 79b7d13..bdce3b5 100644 --- a/test/integration/LuaIntegrationTest.spec.ts +++ b/test/integration/LuaIntegrationTest.spec.ts @@ -55,7 +55,7 @@ import { IntegrationTestBase } from "./IntegrationTestBase"; const cli = new CommandLine([ "-t", "lua", "-f", "myFile.ts" ]); - + cli.prepare(); cli.execute(); expect(fs.existsSync("myFile.lua")).to.be.true;