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.

1 lines 15.1 kB
{"version":3,"sources":["../src/types.ts","../src/index.ts"],"names":["ChangeTrackerErrorType","LogManagerImpl","createHash","readFile","mkdir","dirname","writeFile","rm"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Collection of files, keyed by relative path\n *\n * @template T - The type of the file data\n */\ninterface HasContents {\n contents: Buffer;\n}\n\ntype FileDataCollection<T extends HasContents> = Record<string, T>;\n\n/**\n * Configuration options for change tracking\n */\ninterface ChangeTrackerConfig {\n /**\n * Path to store the change history file\n */\n historyPath: string;\n\n /**\n * Enable or disable change tracking\n * When disabled, all files will be treated as unchanged\n * @default true\n */\n enabled?: boolean;\n}\n\n/**\n * Status of a file's changes\n */\ntype FileChangeStatus = \"new\" | \"modified\" | \"unchanged\" | \"untracked\";\n\n/**\n * State of a file's changes\n */\ninterface FileChangeState {\n /**\n * Current status of the file\n */\n status: FileChangeStatus;\n\n /**\n * Previous MD5 hash of the file, if any\n */\n previousFingerprint?: string;\n}\n\n/**\n * Map of file paths to their fingerprints\n */\ntype FileFingerprints = Record<string, string>;\n\n/**\n * Map of file paths to their change states\n */\ntype FileStates = Map<string, FileChangeState>;\n\n/**\n * Error types specific to change tracker\n */\nenum ChangeTrackerErrorType {\n HISTORY_PATH_NOT_SET = \"HISTORY_PATH_NOT_SET\",\n HISTORY_READ_ERROR = \"HISTORY_READ_ERROR\",\n HISTORY_WRITE_ERROR = \"HISTORY_WRITE_ERROR\",\n}\n\n/**\n * Custom error class for change tracker errors\n */\nclass ChangeTrackerError extends Error {\n constructor(\n public type: ChangeTrackerErrorType,\n message: string,\n public cause?: Error,\n ) {\n super(message);\n this.name = \"ChangeTrackerError\";\n }\n}\n\n/**\n * Interface for the change tracker component\n *\n * @template T - The type of the file data\n */\ninterface ChangeTracker<T extends HasContents> {\n /**\n * Track changes in files\n * @param files Files to track changes for\n * @returns Map of file paths to their change states\n */\n trackChanges(files: FileDataCollection<T>): Promise<FileStates>;\n\n /**\n * Get the change state of a file\n * @param filepath Path of the file\n * @returns Change state of the file, or undefined if not tracked\n */\n getFileState(filepath: string): FileChangeState | undefined;\n\n /**\n * Clear change history\n * This will cause all files to be treated as new in the next build\n */\n clearHistory(): Promise<void>;\n\n /**\n * Enable or disable change tracking with optional configuration update\n * @param options Boolean to enable/disable tracking, or a complete configuration object\n */\n enable(options: boolean | ChangeTrackerConfig): void;\n}\n\nexport {\n type ChangeTracker,\n type ChangeTrackerConfig,\n type FileChangeState,\n type FileChangeStatus,\n type FileDataCollection,\n type FileFingerprints,\n type FileStates,\n type HasContents,\n ChangeTrackerError,\n ChangeTrackerErrorType,\n};\n","import { createHash } from \"crypto\";\nimport { readFile, writeFile, mkdir, rm } from \"fs/promises\";\nimport { dirname } from \"path\";\nimport { LogManagerImpl, type Logger } from \"@madooei/simple-logger\";\nimport {\n type ChangeTracker,\n type ChangeTrackerConfig,\n type FileChangeState,\n type FileChangeStatus,\n type FileDataCollection,\n type FileFingerprints,\n type FileStates,\n type HasContents,\n ChangeTrackerError,\n ChangeTrackerErrorType,\n} from \"./types\";\n\n/**\n * Implementation of the change tracker component\n */\nclass ChangeTrackerImpl<T extends HasContents> implements ChangeTracker<T> {\n private fileStates: FileStates = new Map();\n private logger: Logger;\n\n constructor(private config: ChangeTrackerConfig) {\n this.logger = LogManagerImpl.getInstance().getLogger(\"change-tracker\");\n this.config.enabled = this.config.enabled ?? true;\n }\n\n /**\n * Creates an MD5 hash of file contents\n */\n private createFingerprint(file: { contents: Buffer }): string {\n return createHash(\"md5\").update(file.contents).digest(\"hex\");\n }\n\n /**\n * Load previous fingerprints from history file\n */\n private async loadHistory(): Promise<FileFingerprints> {\n this.logger.trace(\"Loading change history\");\n if (!this.config.historyPath) {\n this.logger.info(\"History path not set\");\n throw new ChangeTrackerError(\n ChangeTrackerErrorType.HISTORY_PATH_NOT_SET,\n \"History path must be set to track changes\",\n );\n }\n\n try {\n const content = await readFile(this.config.historyPath, \"utf8\");\n const history = JSON.parse(content);\n this.logger.trace(\"Loaded change history:\", history);\n return history;\n } catch (err) {\n this.logger.trace(\"No previous change history found:\", err);\n return {};\n }\n }\n\n /**\n * Save current fingerprints to history file\n */\n private async saveHistory(fingerprints: FileFingerprints): Promise<void> {\n this.logger.trace(\"Saving change history\");\n if (!this.config.historyPath) {\n this.logger.info(\"History path not set\");\n throw new ChangeTrackerError(\n ChangeTrackerErrorType.HISTORY_PATH_NOT_SET,\n \"History path must be set to track changes\",\n );\n }\n\n try {\n await mkdir(dirname(this.config.historyPath), { recursive: true });\n await writeFile(\n this.config.historyPath,\n JSON.stringify(fingerprints, null, 2),\n );\n this.logger.trace(\"Change history saved\");\n } catch (err) {\n this.logger.info(\"Failed to save change history:\", err);\n throw new ChangeTrackerError(\n ChangeTrackerErrorType.HISTORY_WRITE_ERROR,\n \"Failed to save change history\",\n err instanceof Error ? err : undefined,\n );\n }\n }\n\n async trackChanges(files: FileDataCollection<T>): Promise<FileStates> {\n this.logger.trace(\"Starting change tracking\");\n\n if (!this.config.enabled) {\n this.logger.info(\n \"Change tracking is disabled, marking files as untracked\",\n );\n return new Map(\n Object.keys(files).map((filepath) => [\n filepath,\n { status: \"untracked\" as const, previousFingerprint: undefined },\n ]),\n );\n }\n\n // Load previous fingerprints\n const loadedHistory = await this.loadHistory();\n\n // Calculate new fingerprints\n this.logger.trace(\"Calculating new fingerprints\");\n const newFingerprints = Object.entries(files).reduce(\n (acc, [filepath, file]) => {\n acc[filepath] = this.createFingerprint(file);\n return acc;\n },\n {} as FileFingerprints,\n );\n this.logger.trace(\"New fingerprints:\", newFingerprints);\n\n // Clear states for deleted files\n const currentFiles = new Set(Object.keys(files));\n for (const [filepath] of this.fileStates) {\n if (!currentFiles.has(filepath)) {\n this.logger.trace(\"Removing state for deleted file:\", filepath);\n this.fileStates.delete(filepath);\n }\n }\n\n // Update file states\n for (const [filepath, file] of Object.entries(files)) {\n const previousFingerprint = loadedHistory[filepath];\n const currentFingerprint = newFingerprints[filepath];\n const isNew = !previousFingerprint;\n\n this.logger.trace(\n \"Processing file:\",\n filepath,\n \"previous:\",\n previousFingerprint || \"none\",\n \"current:\",\n currentFingerprint,\n \"isNew:\",\n isNew,\n );\n\n const state: FileChangeState = {\n status: isNew\n ? \"new\"\n : previousFingerprint !== currentFingerprint\n ? \"modified\"\n : \"unchanged\",\n previousFingerprint: isNew ? undefined : previousFingerprint,\n };\n\n this.logger.trace(\"Setting state for file:\", filepath, state);\n this.fileStates.set(filepath, state);\n }\n\n // Save new fingerprints\n await this.saveHistory(newFingerprints);\n\n this.logger.info(\"Change tracking completed\");\n return this.fileStates;\n }\n\n getFileState(filepath: string): FileChangeState | undefined {\n return this.fileStates.get(filepath);\n }\n\n async clearHistory(): Promise<void> {\n this.logger.trace(\"Clearing change history\");\n if (!this.config.historyPath) {\n this.logger.info(\"History path not set\");\n throw new ChangeTrackerError(\n ChangeTrackerErrorType.HISTORY_PATH_NOT_SET,\n \"History path must be set to clear history\",\n );\n }\n\n try {\n await rm(this.config.historyPath, { force: true });\n this.fileStates.clear();\n this.logger.info(\"Change history cleared\");\n } catch (err) {\n this.logger.info(\"Failed to clear change history:\", err);\n throw new ChangeTrackerError(\n ChangeTrackerErrorType.HISTORY_WRITE_ERROR,\n \"Failed to clear change history\",\n err instanceof Error ? err : undefined,\n );\n }\n }\n\n enable(options: boolean | ChangeTrackerConfig): void {\n this.logger.trace(\"Updating change tracker configuration\");\n\n if (typeof options === \"boolean\") {\n this.config.enabled = options;\n } else {\n this.config = { ...this.config, ...options };\n }\n\n this.logger.info(\n `Change tracking ${this.config.enabled ? \"enabled\" : \"disabled\"}`,\n );\n }\n}\n\n// Export types\nexport type {\n ChangeTracker,\n ChangeTrackerConfig,\n FileChangeState,\n FileChangeStatus,\n FileDataCollection,\n FileFingerprints,\n FileStates,\n};\n\n// Export implementation\nexport { ChangeTrackerError, ChangeTrackerErrorType, ChangeTrackerImpl };\n"]}