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
JavaScript
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 };