import * as express from "express" import * as Settings from "./settings" import * as urlModule from "url" import * as pathModule from "path" import { Context } from "../../../cli/lib/context" import * as Leaf from "../../../dist/leaf.module" import { TSConfigParser } from "../../../cli/lib/tsConfigParser" import * as fs from "fs" import * as child_process from "child_process" import * as wrench from "wrench" import * as ws from "ws" import * as http from "http" import * as chokidar from "chokidar" import * as colors from "colors" const findFreePort = require("find-free-port") as Function if (Settings.debug) { console.log("Debug mode: On") } abstract class ComposerServerDesign { constructor(public parser: TSConfigParser) { } abstract start(port: string | number); } export class ComposerServer extends ComposerServerDesign { public app: express.Application public server: http.Server public wsServer: ws.Server public context: Context public port: string | number = 3780 public RCache: string public RJSCache: string public stylePath = "/style/main.css" public styleFilePath: string = "/style/main.css" public scriptPath = "/script/R.js" public rpcManager = new RPCManager() public setStylePath(path: string) { console.log("set style path") this.styleFilePath = path this.stylePath = pathModule.join("/", path); this.applyWatcher() } constructor(parser: TSConfigParser) { super(parser) this.app = express() this.server = http.createServer(this.app) this.wsServer = new ws.Server({ server: this.server }) this.wsServer.on("connection", (con) => { let service = new RPCService() let adapter = new RPCWSAdapter(con) service.setConnection(adapter) this.rpcManager.add(service) adapter.events.listenBy(this, "close", () => { this.rpcManager.remove(service) adapter.events.stopListenBy(this) }) }) this.app.get(this.scriptPath, (req, res, next) => { let context = this.createContext() let result: string; try { result = context.compileJavascript() } catch (e) { console.error("compile error", e) if (this.RJSCache) { res.end(this.RJSCache) } else { res.end("console.error('Compile error')") } } if (result != this.RJSCache) { this.RJSCache = result res.end(result) } else { res.end(this.RJSCache) } }) this.app.get("/composer/script/lib/leaf.js", (req, res, next) => { let ua = req.headers["user-agent"] || "" let target: string if (ua.indexOf("MSIE") >= 0) { console.log("Using IE8 leaf.ts") let js = fs.readFileSync(pathModule.join(Settings.staticPath, "script/lib/leaf.ie8.js"), "utf8") res.end(js) return } next() }) this.app.get("/", (req, res, next) => { let ua = req.headers["user-agent"] || "" let target: string if (ua.indexOf("MSIE") >= 0) { console.log("Using IE8") target = "index.ie8.html" } else { target = "index.html" } let html = fs.readFileSync(pathModule.join(Settings.staticPath, target), "utf8") html = html.replace("{{stylePath}}", this.stylePath).replace("{{scriptPath}}", this.scriptPath) res.end(html) }) this.app.get("/index.html", (req, res, next) => { res.redirect("/") }) console.error(this.parser.getRootPath(), colors.green("run at")) this.app.use("/", express.static(this.parser.getRootPath())) this.app.use("/composer", express.static(Settings.staticPath)) this.applyWatcher() } public tsWatcher: chokidar.FSWatcher public styleWatcher: chokidar.FSWatcher public applyWatcher() { if (this.tsWatcher) { this.tsWatcher.close() this.styleWatcher.close() } this.tsWatcher = chokidar.watch(this.parser.getTemplatePath()) this.styleWatcher = chokidar.watch(pathModule.join(this.parser.getRootPath(), this.styleFilePath)) let change = (filename) => { this.fireChangeEvent() } this.tsWatcher.on("add", change) .on("change", change) this.styleWatcher.on("add", change) .on("change", change) } public createContext() { let context = new Context(this.parser.getTemplatePath()) context.rootName = "TestR" let files = wrench.readdirSyncRecursive(this.parser.getTemplatePath()).filter((item) => { try { return fs.statSync(pathModule.join(this.parser.getTemplatePath(), item)).isFile() } catch (e) { return false } }) for (let file of files) { if (pathModule.extname(file) !== ".html") continue context.includeWidgetByRelativeFilePath(file) } return context } public start(port: string | number) { this.port = port findFreePort(parseInt(this.port.toString()), (err, port) => { if (err) { console.error(colors.red("fail to find any available port")); return } this.server.listen(parseInt(port.toString()), Settings.host, () => { this.port = port console.log(colors.green(`Composer server listen at http://0.0.0.0:${port}`)) }) }) } private fireTimer = null private changeTimerDebounce: number = 300 public fireChangeEvent() { clearTimeout(this.fireTimer) this.fireTimer = setTimeout(() => { for (let rpc of this.rpcManager.rpcs) { rpc.emitRemoteEvent("change") } }, this.changeTimerDebounce) } } class RPCManager { rpcs: RPCService[] = [] constructor() { } add(rpc: RPCService) { this.rpcs.push(rpc) } remove(rpc: RPCService) { this.rpcs = this.rpcs.filter(item => item !== rpc) } } class RPCService extends Leaf.RPCComposer { } class RPCWSAdapter extends Leaf.RPCConnectionBase { constructor(private con: ws) { super() this.con.once("close", () => { this.events.emit("close") }) } isOpen() { return true } close() { this.con.close() } send(message: string) { this.con.send(message) } }