UNPKG

@qooxdoo/framework

Version:

The JS Framework for Coders

561 lines (500 loc) 16.9 kB
/* ************************************************************************ qooxdoo - the new era of web development http://qooxdoo.org Copyright: 2017 Zenesis Ltd License: MIT: https://opensource.org/licenses/MIT See the LICENSE file in the project's top-level directory for details. Authors: * John Spackman (john.spackman@zenesis.com, @johnspackman) * Henner Kollmann (Henner.Kollmann@gmx.de, @hkollmann) ************************************************************************ */ const fs = require("fs"); const path = require("upath"); const chokidar = require("chokidar"); /** * @ignore(setImmediate) */ qx.Class.define("qx.tool.cli.Watch", { extend: qx.core.Object, construct(maker) { super(); this.__maker = maker; this.__stats = { classesCompiled: 0 }; this.__debounceChanges = {}; this.__configFilenames = []; this.__runWhenWatching = {}; maker.addListener("writtenApplication", this._onWrittenApplication, this); }, properties: { debug: { init: false, check: "Boolean" } }, events: { making: "qx.event.type.Event", remaking: "qx.event.type.Event", made: "qx.event.type.Event", configChanged: "qx.event.type.Event", /** * @typedef {Object} FileChangedEvent * @property {qx.tool.compiler.app.Library} library the library that contains the file * @property {String} filename the filename relative to the library root * @property {String} fileType either "source", "resource" or "theme" * * This event is fired when a file is changed, the data is {FileChangedEvent} */ fileChanged: "qx.event.type.Data" }, members: { __runningPromise: null, __applications: null, __watcherReady: false, __maker: null, __stats: null, __making: null, __stopping: false, __outOfDate: null, __makeTimerId: null, __debounceChanges: null, __configFilenames: null, __madeApplications: null, /** @type{Map<String,Object>} list of runWhenWatching configurations, indexed by app name */ __runWhenWatching: null, async setConfigFilenames(arr) { if (!arr) { this.__configFilenames = []; } else { this.__configFilenames = arr.map(filename => path.resolve(filename)); } }, setRunWhenWatching(appName, config) { this.__runWhenWatching[appName] = config; let arr = qx.tool.utils.Utils.parseCommand(config.command); config._cmd = arr.shift(); config._args = arr; }, async _onWrittenApplication(evt) { let appInfo = evt.getData(); let name = appInfo.application.getName(); let config = this.__runWhenWatching[name]; if (!config) { return; } if (config._process) { try { await qx.tool.utils.Utils.killTree(config._process.pid); } catch (ex) { //Nothing } if (config._processPromise) { await config._processPromise; } config._process = null; } console.log( "Starting application: " + config._cmd + " " + config._args.join(" ") ); config._processPromise = new qx.Promise((resolve, reject) => { let child = (config._process = require("child_process").spawn( config._cmd, config._args )); child.stdout.setEncoding("utf8"); child.stdout.on("data", data => console.log(data)); child.stderr.setEncoding("utf8"); child.stderr.on("data", data => console.log(data)); child.on("close", function (code) { console.log("Application has terminated"); config._process = null; resolve(); }); child.on("error", err => console.error("Application has failed: " + err) ); }); }, start() { if (this.isDebug()) { qx.tool.compiler.Console.debug("DEBUG: Starting watch"); } if (this.__runningPromise) { throw new Error("Cannot start watching more than once"); } this.__runningPromise = qx.tool.utils.Utils.newExternalPromise(); var dirs = []; var analyser = this.__maker.getAnalyser(); analyser.addListener("compiledClass", () => { this.__stats.classesCompiled++; }); dirs.push(qx.tool.config.Compile.config.fileName); dirs.push("compile.js"); analyser.getLibraries().forEach(function (lib) { let dir = path.join(lib.getRootDir(), lib.getSourcePath()); dirs.push(dir); dir = path.join(lib.getRootDir(), lib.getResourcePath()); dirs.push(dir); dir = path.join(lib.getRootDir(), lib.getThemePath()); dirs.push(dir); }); if (analyser.getProxySourcePath()) { dirs.push(path.resolve(analyser.getProxySourcePath())); } var applications = (this.__applications = []); this.__maker.getApplications().forEach(function (application) { var data = { application: application, dependsOn: {}, outOfDate: false }; applications.push(data); let dir = application.getBootPath(); if (dir && !dirs.includes(dir)) { dirs.push(dir); } let localModules = application.getLocalModules(); for (let requireName in localModules) { let dir = localModules[requireName]; if (dir && !dirs.includes(dir)) { dirs.push(dir); } } }); if (this.isDebug()) { qx.tool.compiler.Console.debug( `DEBUG: applications=${JSON.stringify( applications.map(d => d.application.getName()), 2 )}` ); qx.tool.compiler.Console.debug( `DEBUG: dirs=${JSON.stringify(dirs, 2)}` ); } var confirmed = []; Promise.all( dirs.map( dir => new Promise((resolve, reject) => { dir = path.resolve(dir); fs.stat(dir, function (err) { if (err) { if (err.code == "ENOENT") { resolve(); } else { reject(err); } return; } // On case insensitive (but case preserving) filing systems, qx.tool.utils.files.Utils.correctCase // is needed corrects because chokidar needs the correct case in order to detect changes. qx.tool.utils.files.Utils.correctCase(dir).then(dir => { confirmed.push(dir); resolve(); }); }); }) ) ).then(() => { if (this.isDebug()) { qx.tool.compiler.Console.debug( `DEBUG: confirmed=${JSON.stringify(confirmed, 2)}` ); } this.__make().then(() => { var watcher = (this._watcher = chokidar.watch(confirmed, { //ignored: /(^|[\/\\])\../ })); watcher.on("change", filename => this.__onFileChange("change", filename) ); watcher.on("add", filename => this.__onFileChange("add", filename)); watcher.on("unlink", filename => this.__onFileChange("unlink", filename) ); watcher.on("ready", () => { qx.tool.compiler.Console.log(`Start watching ...`); this.__watcherReady = true; }); watcher.on("error", err => { qx.tool.compiler.Console.print( err.code == "ENOSPC" ? "qx.tool.cli.watch.enospcError" : "qx.tool.cli.watch.watchError", err ); }); }); }); process.on("beforeExit", this.__onStop.bind(this)); process.on("exit", this.__onStop.bind(this)); return this.__runningPromise; }, async stop() { this.__stopping = true; this._watcher.close(); if (this.__making) { await this.__making; } }, __make() { if (this.__making) { this.__makeNeedsRestart = true; return this.__making; } this.fireEvent("making"); var t = this; var Console = qx.tool.compiler.Console; function make() { Console.print("qx.tool.cli.watch.makingApplications"); t.__madeApplications = null; var startTime = new Date().getTime(); t.__stats.classesCompiled = 0; t.__outOfDate = false; return t.__maker .make() .then(() => { if (t.__stopping) { Console.print("qx.tool.cli.watch.makeStopping"); return null; } if (t.__outOfDate) { return new qx.Promise(resolve => { setImmediate(function () { Console.print("qx.tool.cli.watch.restartingMake"); t.fireEvent("remaking"); make().then(resolve); }); }); } var analyser = t.__maker.getAnalyser(); var db = analyser.getDatabase(); var promises = []; t.__applications.forEach(data => { data.dependsOn = {}; var deps = data.application.getDependencies(); deps.forEach(function (classname) { let info = db.classInfo[classname]; let lib = analyser.findLibrary(info.libraryName); let parts = [lib.getRootDir(), lib.getSourcePath()].concat( classname.split(".") ); let filename = path.resolve.apply(path, parts) + ".js"; data.dependsOn[filename] = true; }); let localModules = data.application.getLocalModules(); for (let requireName in localModules) { let filename = path.resolve(localModules[requireName]); data.dependsOn[filename] = true; } var filename = path.resolve(data.application.getLoaderTemplate()); promises.push( qx.tool.utils.files.Utils.correctCase(filename).then( filename => (data.dependsOn[filename] = true) ) ); }); return Promise.all(promises).then(() => { var endTime = new Date().getTime(); Console.print( "qx.tool.cli.watch.compiledClasses", t.__stats.classesCompiled, qx.tool.utils.Utils.formatTime(endTime - startTime) ); t.fireEvent("made"); }); }) .then(() => { t.__making = null; }) .catch(err => { Console.print("qx.tool.cli.watch.compileFailed", err); t.__making = null; t.fireEvent("made"); }); } const runIt = () => make().then(() => { if (this.__makeNeedsRestart) { delete this.__makeNeedsRestart; return runIt(); } return null; }); this.__making = runIt(); return this.__making; }, __scheduleMake() { if (this.__making) { this.__makeNeedsRestart = true; return this.__making; } if (this.__makeTimerId) { clearTimeout(this.__makeTimerId); } this.__makeTimerId = setTimeout(() => this.__make()); return null; }, __onFileChange(type, filename) { const Console = qx.tool.compiler.Console; if (!this.__watcherReady) { return null; } filename = path.normalize(filename); const handleFileChange = async () => { var outOfDate = false; if (this.__configFilenames.find(str => str == filename)) { if (this.isDebug()) { Console.debug(`DEBUG: onFileChange: configChanged`); } this.fireEvent("configChanged"); return; } let outOfDateApps = {}; this.__applications.forEach(data => { if (data.dependsOn[filename]) { outOfDateApps[data.application.getName()] = data.application; outOfDate = true; } else { var boot = data.application.getBootPath(); if (boot) { boot = path.resolve(boot); if (filename.startsWith(boot)) { outOfDateApps[data.application.getName()] = true; outOfDate = true; } } } }); let outOfDateAppNames = Object.keys(outOfDateApps); if (this.isDebug()) { if (outOfDateAppNames.length) { Console.debug( `DEBUG: onFileChange: ${filename} impacted applications: ${JSON.stringify( outOfDateAppNames, 2 )}` ); } } let analyser = this.__maker.getAnalyser(); let fName = ""; let fileType = null; let fileLibrary = null; for (let lib of analyser.getLibraries()) { var dir = path.resolve( path.join(lib.getRootDir(), lib.getResourcePath()) ); if (filename.startsWith(dir)) { fName = path.relative(dir, filename); fileType = "resource"; fileLibrary = lib; break; } dir = path.resolve(path.join(lib.getRootDir(), lib.getThemePath())); if (filename.startsWith(dir)) { fName = path.relative(dir, filename); fileType = "theme"; fileLibrary = lib; break; } dir = path.resolve(path.join(lib.getRootDir(), lib.getSourcePath())); if (filename.startsWith(dir)) { fName = path.relative(dir, filename); fileType = "source"; fileLibrary = lib; break; } } this.fireDataEvent("fileChanged", { filename: fName, fileType: fileType, library: fileLibrary }); if (fileType == "resource" || fileType == "theme") { let rm = analyser.getResourceManager(); let target = this.__maker.getTarget(); if (this.isDebug()) { Console.debug(`DEBUG: onFileChange: ${filename} is a resource`); } let asset = rm.getAsset(fName, type != "unlink"); if (asset && type != "unlink") { await asset.sync(target); let dota = asset.getDependsOnThisAsset(); if (dota) { await qx.Promise.all(dota.map(asset => asset.sync(target))); } } } if (outOfDate) { this.__outOfDate = true; this.__scheduleMake(); } }; const runIt = dbc => handleFileChange().then(() => { if (dbc.restart) { delete dbc.restart; return runIt(dbc); } return null; }); let dbc = this.__debounceChanges[filename]; if (!dbc) { dbc = this.__debounceChanges[filename] = { types: {} }; } dbc.types[type] = true; if (dbc.promise) { if (this.isDebug()) { Console.debug( `DEBUG: onFileChange: seen '${filename}', but restarting promise` ); } dbc.restart = 1; return dbc.promise; } if (dbc.timerId) { clearTimeout(dbc.timerId); dbc.timerId = null; } if (this.isDebug()) { Console.debug(`DEBUG: onFileChange: seen '${filename}', queuing`); } dbc.timerId = setTimeout(() => { dbc.promise = runIt(dbc).then( () => delete this.__debounceChanges[filename] ); }, 150); return null; }, __onStop() { this.__runningPromise.resolve(); } }, defer() { qx.tool.compiler.Console.addMessageIds({ "qx.tool.cli.watch.makingApplications": ">>> Making the applications...", "qx.tool.cli.watch.restartingMake": ">>> Code changed during make, restarting...", "qx.tool.cli.watch.makeStopping": ">>> Not restarting make because make is stopping...", "qx.tool.cli.watch.compiledClasses": ">>> Compiled %1 classes in %2" }); qx.tool.compiler.Console.addMessageIds( { "qx.tool.cli.watch.compileFailed": ">>> Fatal error during compile: %1", "qx.tool.cli.watch.enospcError": ">>> ENOSPC error occured - try increasing fs.inotify.max_user_watches", "qx.tool.cli.watch.watchError": ">>> Error occured while watching files - file modifications may not be detected; error: %1" }, "error" ); } });