UNPKG

@coursebook/change-tracker

Version:

A minimal, type-safe change tracker for JavaScript/TypeScript. You can use it to track changes to files for example.

184 lines (180 loc) 6.05 kB
import { createHash } from 'crypto'; import { readFile, mkdir, writeFile, rm } from 'fs/promises'; import { dirname } from 'path'; import { LogManagerImpl } from '@madooei/simple-logger'; // src/index.ts // src/types.ts var ChangeTrackerErrorType = /* @__PURE__ */ ((ChangeTrackerErrorType2) => { ChangeTrackerErrorType2["HISTORY_PATH_NOT_SET"] = "HISTORY_PATH_NOT_SET"; ChangeTrackerErrorType2["HISTORY_READ_ERROR"] = "HISTORY_READ_ERROR"; ChangeTrackerErrorType2["HISTORY_WRITE_ERROR"] = "HISTORY_WRITE_ERROR"; return ChangeTrackerErrorType2; })(ChangeTrackerErrorType || {}); var ChangeTrackerError = class extends Error { constructor(type, message, cause) { super(message); this.type = type; this.cause = cause; this.name = "ChangeTrackerError"; } }; // src/index.ts var ChangeTrackerImpl = class { constructor(config) { this.config = config; this.logger = LogManagerImpl.getInstance().getLogger("change-tracker"); this.config.enabled = this.config.enabled ?? true; } fileStates = /* @__PURE__ */ new Map(); logger; /** * Creates an MD5 hash of file contents */ createFingerprint(file) { return createHash("md5").update(file.contents).digest("hex"); } /** * Load previous fingerprints from history file */ async loadHistory() { this.logger.trace("Loading change history"); if (!this.config.historyPath) { this.logger.info("History path not set"); throw new ChangeTrackerError( "HISTORY_PATH_NOT_SET" /* HISTORY_PATH_NOT_SET */, "History path must be set to track changes" ); } try { const content = await readFile(this.config.historyPath, "utf8"); const history = JSON.parse(content); this.logger.trace("Loaded change history:", history); return history; } catch (err) { this.logger.trace("No previous change history found:", err); return {}; } } /** * Save current fingerprints to history file */ async saveHistory(fingerprints) { this.logger.trace("Saving change history"); if (!this.config.historyPath) { this.logger.info("History path not set"); throw new ChangeTrackerError( "HISTORY_PATH_NOT_SET" /* HISTORY_PATH_NOT_SET */, "History path must be set to track changes" ); } try { await mkdir(dirname(this.config.historyPath), { recursive: true }); await writeFile( this.config.historyPath, JSON.stringify(fingerprints, null, 2) ); this.logger.trace("Change history saved"); } catch (err) { this.logger.info("Failed to save change history:", err); throw new ChangeTrackerError( "HISTORY_WRITE_ERROR" /* HISTORY_WRITE_ERROR */, "Failed to save change history", err instanceof Error ? err : void 0 ); } } async trackChanges(files) { this.logger.trace("Starting change tracking"); if (!this.config.enabled) { this.logger.info( "Change tracking is disabled, marking files as untracked" ); return new Map( Object.keys(files).map((filepath) => [ filepath, { status: "untracked", previousFingerprint: void 0 } ]) ); } const loadedHistory = await this.loadHistory(); this.logger.trace("Calculating new fingerprints"); const newFingerprints = Object.entries(files).reduce( (acc, [filepath, file]) => { acc[filepath] = this.createFingerprint(file); return acc; }, {} ); this.logger.trace("New fingerprints:", newFingerprints); const currentFiles = new Set(Object.keys(files)); for (const [filepath] of this.fileStates) { if (!currentFiles.has(filepath)) { this.logger.trace("Removing state for deleted file:", filepath); this.fileStates.delete(filepath); } } for (const [filepath, file] of Object.entries(files)) { const previousFingerprint = loadedHistory[filepath]; const currentFingerprint = newFingerprints[filepath]; const isNew = !previousFingerprint; this.logger.trace( "Processing file:", filepath, "previous:", previousFingerprint || "none", "current:", currentFingerprint, "isNew:", isNew ); const state = { status: isNew ? "new" : previousFingerprint !== currentFingerprint ? "modified" : "unchanged", previousFingerprint: isNew ? void 0 : previousFingerprint }; this.logger.trace("Setting state for file:", filepath, state); this.fileStates.set(filepath, state); } await this.saveHistory(newFingerprints); this.logger.info("Change tracking completed"); return this.fileStates; } getFileState(filepath) { return this.fileStates.get(filepath); } async clearHistory() { this.logger.trace("Clearing change history"); if (!this.config.historyPath) { this.logger.info("History path not set"); throw new ChangeTrackerError( "HISTORY_PATH_NOT_SET" /* HISTORY_PATH_NOT_SET */, "History path must be set to clear history" ); } try { await rm(this.config.historyPath, { force: true }); this.fileStates.clear(); this.logger.info("Change history cleared"); } catch (err) { this.logger.info("Failed to clear change history:", err); throw new ChangeTrackerError( "HISTORY_WRITE_ERROR" /* HISTORY_WRITE_ERROR */, "Failed to clear change history", err instanceof Error ? err : void 0 ); } } enable(options) { this.logger.trace("Updating change tracker configuration"); if (typeof options === "boolean") { this.config.enabled = options; } else { this.config = { ...this.config, ...options }; } this.logger.info( `Change tracking ${this.config.enabled ? "enabled" : "disabled"}` ); } }; export { ChangeTrackerError, ChangeTrackerErrorType, ChangeTrackerImpl }; //# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map