@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
JavaScript
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