UNPKG

writepool

Version:

Collects files before writing to disk, allowing change tracking, output management, and dry-runs for controlled file handling.

195 lines (188 loc) 4.92 kB
import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { reset, red, green } from 'colorette'; import { calcSlices } from 'fast-myers-diff'; import { existsSync, mkdirSync, writeFileSync } from 'node:fs'; import EventEmitter from 'node:events'; const flattenDiff = (diff) => { return Array.from(diff).flatMap(([type, contents]) => { return contents.map((line) => [type, line]); }); }; class FileChanges { constructor(path) { this.path = path; } log = []; list() { return this.log.map((change, index) => ({ index, timestamp: change.timestamp, origin: change.origin })); } stackChange(contents, origin) { const record = { timestamp: Date.now(), origin, contents }; this.log.push(record); } get(index) { return this.log.at(index); } diff(prevIndex, nextIndex) { const prev = this.log.at(prevIndex); const next = this.log.at(nextIndex); const diff = calcSlices( prev.contents.split("\n"), next.contents.split("\n") ); const flatDiff = flattenDiff(diff); const flat = flatDiff.map(([type, line]) => { let icon = " "; if (type === -1) { icon = " - "; } if (type === 1) { icon = " + "; } return `${icon}${line}`; }); const formatted = flatDiff.map(([type, line]) => { let color = reset; let icon = reset(" "); if (type === -1) { color = red; icon = red(" - "); } if (type === 1) { color = green; icon = green(" + "); } return `${icon}${color(line)}`; }); return { diff: flatDiff, flat, formatted, log() { console.log("\n", formatted.join("\n")); } }; } } class ChangeLog { changes = /* @__PURE__ */ new Map(); add(path, contents, origin) { if (!this.changes.has(path)) { this.changes.set(path, new FileChanges(path)); } this.changes.get(path).stackChange(contents, origin); } get(path) { return this.changes.get(path); } } const outputFileSync = (file, data, options) => { const dir = dirname(file); if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }); } writeFileSync(file, data, options); }; class TypedEventEmitter { emitter = new EventEmitter(); on(name, listener) { this.emitter.on(name, listener); } off(name, listener) { this.emitter.off(name, listener); } emit(name, payload) { return this.emitter.emit(name, payload); } once(name, listener) { this.emitter.once(name, listener); } } class Writepool { static instance; emitter = new TypedEventEmitter(); changeLog = new ChangeLog(); files = /* @__PURE__ */ new Map(); origin; rootCollection = this; options = { outDir: dirname(fileURLToPath(import.meta.url)), dry: false, logChanges: false }; on = this.rootCollection.emitter.on; once = this.rootCollection.emitter.once; constructor(options) { this.options = { ...this.options, ...options }; this.rootCollection.on = this.rootCollection.emitter.on.bind(this); this.rootCollection.once = this.rootCollection.emitter.once.bind(this); } get size() { return this.rootCollection.files.size; } static getInstance(options) { if (!Writepool.instance) { Writepool.instance = new Writepool(options); } return Writepool.instance; } withOrigin(name) { const collection = new Writepool(this.options); collection.origin = name; collection.rootCollection = this.rootCollection; return collection; } get(pathOrPredicate) { if (typeof pathOrPredicate === "function") { for (const file of this.rootCollection.files) { if (pathOrPredicate(...file)) { return file; } } return; } if (this.rootCollection.files.has(pathOrPredicate)) { return [pathOrPredicate, this.rootCollection.files.get(pathOrPredicate)]; } } write(path, contents, origin = this.origin) { this.rootCollection.files.set(path, contents); if (this.options.logChanges) { this.rootCollection.changeLog.add(path, contents, origin); } this.rootCollection.emitter.emit("file:stacked", { path, origin, contents }); } getChanges(path) { if (!this.options.logChanges) return; return this.rootCollection.changeLog.get(path); } *writeFilesToDisk(options) { const finalOptions = { ...this.rootCollection.options, ...options }; let index = 1; for (const [file, contents] of this.rootCollection.files) { const path = resolve(finalOptions.outDir, file); if (!this.rootCollection.options.dry) { outputFileSync(path, contents); } yield { path, index, total: this.rootCollection.files.size }; index++; } } } export { Writepool };