UNPKG

gow

Version:

Watch your files on the gow.

217 lines (178 loc) 8.62 kB
import { Dirent, promises as fs } from "fs"; import { exec, spawn, ChildProcess } from "child_process"; export interface Config { command?: string; files?: string[]; silent?: boolean; delay?: number; } interface File { name: string; path: string; modified: Date | string; lastModified: Date | string; [key: string]: any; } export class Gow { private readonly path: string; private readonly command: string; private readonly delay: number; private readonly runtimePath: string; private readonly silent: boolean; private readonly fileExpression: RegExp; private ignoreRuntimeChanges: string[]; private runtimeProcess: ChildProcess; private readyForNextReload: boolean = true; private reloadInQueue: boolean = false; private waitingForProcess: number; private cache: { project, runtime } = { project: { last: [], current: [] }, runtime: { last: [], current: [] } }; constructor(path: string, config?: Config) { this.delay = config.delay || 1000; this.command = config.command || "node ."; this.silent = config.silent || false; this.fileExpression = config.files ? new RegExp(config.files.map((fileGlob: string) => this.getRegularExpressionByGlob(fileGlob)).join("|")) : new RegExp(this.getRegularExpressionByGlob("***/*")); this.path = this.normalizePath(path); this.runtimePath = this.path + ".gowing/"; this.createRuntime(); } async createProcesses(): Promise<void> { await this.copyIntoRuntime(); this.runtimeProcess = this.spawnCommandProcess(this.command); } async createRuntime(): Promise<void> { await this.createProcesses(); if (!this.silent) console.log("\x1b[97;42m Gow \x1b[0m Created process"); setInterval(async () => { const modifiedRuntimeFiles: boolean | File[] = await this.getModifiedRuntimeFiles(); const haveProjectFilesBeenModified: boolean = await this.haveProjectFilesBeenModified(); if (modifiedRuntimeFiles) { for (const modifiedRuntimeFile of (modifiedRuntimeFiles as File[])) { await fs.copyFile(modifiedRuntimeFile.path, this.normalizePath(modifiedRuntimeFile.path, false).replace(this.runtimePath, this.path)); this.ignoreRuntimeChanges.push(modifiedRuntimeFile.name); } } if (this.reloadInQueue && this.readyForNextReload) { this.readyForNextReload = false; this.reloadInQueue = false; this.createProcesses(); if (!this.silent) console.log("\x1b[97;42m Gow \x1b[0m Reloaded"); return; } if (!haveProjectFilesBeenModified) return; if (!this.readyForNextReload) { this.reloadInQueue = true; return; } this.readyForNextReload = false; this.createProcesses(); if (!this.silent) console.log("\x1b[97;42m Gow \x1b[0m Reloaded"); }, 250); } isWindows(): boolean { return /^win/.test(process.platform); } normalizePath(path: string, tailingSlash: boolean = true) { const normalizedPath = path.replace(/(\/|\\)/, "/"); return tailingSlash ? normalizedPath.replace(/\/$/, "") + "/" : normalizedPath.replace(/\/$/, ""); } getRegularExpressionByGlob(glob: string): string { return ((glob.includes("/") ? glob : "./" + glob) + "$") .replace(/\*/g, "ALL") .replace(/\//g, "SLASH") .replace(/\./g, "\.") .replace(/\[!/g, "[^") .replace(/ALLALLALL/g, "((.*)(?=\\/)(\\/)|)") .replace(/ALLALL/g, "(([a-zA-Z0-9-_]*)\\/|\.\/|)") .replace(/ALL/g, "([a-zA-Z0-9-_.]*)") .replace(/SLASH/g, ""); } spawnCommandProcess(command: string): any { process.chdir(this.runtimePath); if (this.runtimeProcess) { if (this.isWindows()) { spawn("taskkill", ["/pid", this.runtimeProcess.pid.toString(), "/f", "/t"]) }else { this.runtimeProcess.kill(); } } const processID: number = Math.round(Math.random() * 1000); this.waitingForProcess = processID; setTimeout(() => { if (this.waitingForProcess === processID) this.readyForNextReload = true; }, this.delay); return exec(command, (error: Error, stdout: string, stderr: string) => { (stdout || stderr).length > 2 && console.log((stdout || stderr).replace(/\n$/g, "")); }); } private async copyIntoRuntime() { process.chdir(this.path); try { await fs.readdir(this.runtimePath); }catch(e) { await fs.mkdir(this.runtimePath); } const files: File[] = await this.getFiles(this.path, { last: [], current: [] }); for (const { path } of files) { const relativePath: string = this.normalizePath(path, false).replace(this.path, ""); const folders: string[] = relativePath.match(/([a-zA-Z0-9\/]*)(?=\/)/g); if (folders) { for (const folder of folders) { try { await fs.readdir(this.runtimePath + folder); }catch(e) { await fs.mkdir(this.runtimePath + folder, { recursive: true }); } } } await fs.copyFile(relativePath, this.runtimePath + relativePath); } } private async getFiles(path: string = "./", cache: { last, current }): Promise<File[]> { const files: File[] = await Promise.all((await fs.readdir(path, { withFileTypes: true })) .filter((entry: Dirent): boolean => { return !entry.isDirectory() && !entry.name.endsWith(".swp") }) .map(async ({ name }: { name: string }): Promise<File> => { return { name, path: path + name, modified: (await fs.stat(path + name)).mtime, lastModified: cache.last.length === 0 ? "FIRST_RUN" : cache.last.find(file => file.path === path + name) && cache.last.find(file => file.path === path + name).modified } })); const folders: Dirent[] = (await fs.readdir(path, { withFileTypes: true })).filter((folder: Dirent): boolean => { return folder.isDirectory() && !folder.name.startsWith(".") && folder.name !== "node_modules"; }); for (const folder of folders) files.push(...await this.getFiles(`${path}${folder.name}/`, cache)); return files; } private async getModifiedRuntimeFiles(): Promise<boolean | File[]> { this.cache.runtime.last = this.cache.runtime.current; this.cache.runtime.current = await this.getFiles(this.runtimePath, this.cache.runtime); const changedFiles: File[] = this.cache.runtime.current.filter((file: File): boolean => { return (this.normalizePath(file.path, false).replace(this.runtimePath, "")).replace(this.fileExpression, "") === "" && (file.lastModified && file.lastModified.toString()) !== file.modified.toString() && file.lastModified !== "FIRST_RUN"; }); return changedFiles.length > 0 ? changedFiles : false; } private async haveProjectFilesBeenModified(): Promise<boolean> { this.cache.project.last = this.cache.project.current; this.cache.project.current = await this.getFiles(this.path, this.cache.project); const haveProjectFilesBeenModified: boolean = this.cache.project.current.filter((file: File): boolean => { return (this.normalizePath(file.path, false).replace(this.path, "")).replace(this.fileExpression, "") === "" && (file.lastModified && file.lastModified.toString()) !== file.modified.toString() && file.lastModified !== "FIRST_RUN" && !this.ignoreRuntimeChanges.includes(file.name); }).length > 0; this.ignoreRuntimeChanges = []; return haveProjectFilesBeenModified; } }