UNPKG

@fwdslsh/unify

Version:

A lightweight, framework-free static site generator with Bun native APIs

348 lines (298 loc) 11.4 kB
/** * Dependency Tracking System for unify * Tracks include relationships for selective rebuilds */ import path from 'path'; import { logger } from '../utils/logger.js'; import { extractIncludeDependencies } from './include-processor.js'; /** * Dependency tracker for managing include relationships */ export class DependencyTracker { constructor() { // Maps page file path to array of include file paths it depends on this.includesInPage = new Map(); // Maps include file path to array of page file paths that depend on it this.pagesByInclude = new Map(); // Cache of all known files for efficient lookups this.knownFiles = new Set(); } /** * Record dependencies for a page * @param {string} pagePath - Path to the page file * @param {string[]} includePaths - Array of include file paths * @param {string[]} layoutPaths - Array of layout file paths (optional) */ recordDependencies(pagePath, includePaths, layoutPaths = []) { // Clear existing dependencies for this page this.clearPageDependencies(pagePath); // Combine include and layout dependencies const allDependencies = [...includePaths, ...layoutPaths]; // Record new dependencies if (allDependencies.length > 0) { this.includesInPage.set(pagePath, [...allDependencies]); // Update reverse mapping for (const dependencyPath of allDependencies) { if (!this.pagesByInclude.has(dependencyPath)) { this.pagesByInclude.set(dependencyPath, []); } this.pagesByInclude.get(dependencyPath).push(pagePath); } logger.debug(`Recorded ${allDependencies.length} dependencies (${includePaths.length} includes, ${layoutPaths.length} layouts) for ${pagePath}`); } // Track all known files this.knownFiles.add(pagePath); allDependencies.forEach(path => this.knownFiles.add(path)); } /** * Clear dependencies for a specific page * @param {string} pagePath - Path to the page file */ clearPageDependencies(pagePath) { const existingIncludes = this.includesInPage.get(pagePath); if (existingIncludes) { // Remove from reverse mapping for (const includePath of existingIncludes) { const pages = this.pagesByInclude.get(includePath); if (pages) { const index = pages.indexOf(pagePath); if (index > -1) { pages.splice(index, 1); } // Clean up empty arrays if (pages.length === 0) { this.pagesByInclude.delete(includePath); } } } this.includesInPage.delete(pagePath); } } /** * Get all pages that depend on a specific include file (alias for getAffectedPages) * @param {string} includePath - Path to the include file * @returns {string[]} Array of page paths that depend on the include */ getDependentPages(includePath) { return this.getAffectedPages(includePath); } /** * Optimized Get all pages that depend on a specific include file * @param {string} includePath - Path to the include file * @returns {string[]} Array of page paths that depend on the include */ getAffectedPages(includePath, cache = new Map()) { if (cache.has(includePath)) { return cache.get(includePath); } const directlyAffected = this.pagesByInclude.get(includePath) || []; const allAffected = new Set(directlyAffected); // Check for nested dependencies - if this include is included by other includes const includesUsingThis = []; for (const [page, includes] of this.includesInPage.entries()) { if (includes.includes(includePath) && this.isIncludeFile(page)) { includesUsingThis.push(page); } } // Recursively find pages affected by nested includes for (const nestedInclude of includesUsingThis) { const nestedAffected = this.getAffectedPages(nestedInclude, cache); nestedAffected.forEach(page => allAffected.add(page)); } const result = Array.from(allAffected); cache.set(includePath, result); logger.debug(`Include ${includePath} affects ${result.length} pages: ${result.join(', ')}`); return result; } /** * Get all includes used by a specific page * @param {string} pagePath - Path to the page file * @returns {string[]} Array of include paths used by the page */ getPageDependencies(pagePath) { return this.includesInPage.get(pagePath) || []; } /** * Check if a file is an include (used by other files but not a main page) * @param {string} filePath - Path to check * @returns {boolean} True if file is used as an include */ isIncludeFile(filePath) { return this.pagesByInclude.has(filePath); } /** * Check if a file is a main page (not used as an include by others) * @param {string} filePath - Path to check * @returns {boolean} True if file is a main page */ isMainPage(filePath) { return this.includesInPage.has(filePath) && !this.pagesByInclude.has(filePath); } /** * Get all known files * @returns {string[]} Array of all known file paths */ getAllFiles() { return Array.from(this.knownFiles); } /** * Get all main pages (files that are not includes) * @returns {string[]} Array of main page paths */ getMainPages() { return this.getAllFiles().filter(file => !this.isIncludeFile(file) || this.includesInPage.has(file)); } /** * Get all include files (files used by other files) * @returns {string[]} Array of include file paths */ getIncludeFiles() { return Array.from(this.pagesByInclude.keys()); } /** * Analyze and record dependencies from HTML content * @param {string} pagePath - Path to the page file * @param {string} htmlContent - HTML content to analyze * @param {string} sourceRoot - Source root directory */ analyzePage(pagePath, htmlContent, sourceRoot) { const includeDependencies = extractIncludeDependencies(htmlContent, pagePath, sourceRoot); const layoutDependencies = this.extractLayoutDependencies(htmlContent, pagePath, sourceRoot); this.recordDependencies(pagePath, includeDependencies, layoutDependencies); // Also analyze nested dependencies for deeper tracking this.analyzeNestedDependencies(pagePath, sourceRoot); } /** * Extract layout dependencies from HTML content * @param {string} htmlContent - HTML content to analyze * @param {string} pagePath - Path to the current page * @param {string} sourceRoot - Source root directory * @returns {string[]} Array of resolved layout file paths */ extractLayoutDependencies(htmlContent, pagePath, sourceRoot) { const dependencies = []; // Look for data-layout attribute const layoutMatch = htmlContent.match(/data-layout=["']([^"']+)["']/i); if (layoutMatch) { const layoutPath = layoutMatch[1]; try { // Resolve layout path (similar to unified-html-processor logic) let resolvedLayoutPath; if (layoutPath.startsWith("/")) { // Absolute path from source root resolvedLayoutPath = path.join(sourceRoot, layoutPath.substring(1)); } else if (layoutPath.includes('/')) { // Path with directory structure, relative to source root resolvedLayoutPath = path.join(sourceRoot, layoutPath); } else { // Bare filename, relative to current page directory const pageDir = path.dirname(pagePath); resolvedLayoutPath = path.join(pageDir, layoutPath); } dependencies.push(resolvedLayoutPath); logger.debug(`Extracted layout dependency: ${layoutPath} -> ${resolvedLayoutPath}`); } catch (error) { // Log warning but continue - dependency tracking shouldn't break builds logger.warn(`Could not resolve layout dependency: ${layoutPath} in ${pagePath}`); } } return dependencies; } /** * Analyze nested dependencies by reading include files * @param {string} pagePath - Path to the page file * @param {string} sourceRoot - Source root directory */ async analyzeNestedDependencies(pagePath, sourceRoot) { const directDependencies = this.getPageDependencies(pagePath); for (const includePath of directDependencies) { try { // Read the include file to find its dependencies const fs = await import('fs/promises'); const includeContent = await fs.readFile(includePath, 'utf-8'); const nestedDependencies = extractIncludeDependencies(includeContent, includePath, sourceRoot); if (nestedDependencies.length > 0) { this.recordDependencies(includePath, nestedDependencies); logger.debug(`Found ${nestedDependencies.length} nested dependencies in ${includePath}`); } } catch (error) { // Include file might not exist or be readable - log but continue logger.debug(`Could not analyze nested dependencies for ${includePath}: ${error.message}`); } } } /** * Remove all records of a file (when file is deleted) * @param {string} filePath - Path to the deleted file */ removeFile(filePath) { // Clear if it's a page this.clearPageDependencies(filePath); // Clear if it's an include if (this.pagesByInclude.has(filePath)) { const affectedPages = this.pagesByInclude.get(filePath); this.pagesByInclude.delete(filePath); // Update affected pages for (const pagePath of affectedPages) { const includes = this.includesInPage.get(pagePath); if (includes) { const index = includes.indexOf(filePath); if (index > -1) { includes.splice(index, 1); } } } } this.knownFiles.delete(filePath); logger.debug(`Removed file from dependency tracking: ${filePath}`); } /** * Get dependency statistics for debugging * @returns {Object} Statistics about tracked dependencies */ getStats() { return { totalFiles: this.knownFiles.size, pagesWithDependencies: this.includesInPage.size, includeFiles: this.pagesByInclude.size, totalDependencyRelationships: Array.from(this.includesInPage.values()) .reduce((sum, deps) => sum + deps.length, 0) }; } /** * Clear all dependency data */ clear() { this.includesInPage.clear(); this.pagesByInclude.clear(); this.knownFiles.clear(); logger.debug('Cleared all dependency data'); } /** * Export dependency data for debugging or persistence * @returns {Object} Serializable dependency data */ export() { return { includesInPage: Object.fromEntries(this.includesInPage), pagesByInclude: Object.fromEntries(this.pagesByInclude), knownFiles: Array.from(this.knownFiles) }; } /** * Import dependency data * @param {Object} data - Dependency data to import */ import(data) { this.clear(); if (data.includesInPage) { this.includesInPage = new Map(Object.entries(data.includesInPage)); } if (data.pagesByInclude) { this.pagesByInclude = new Map(Object.entries(data.pagesByInclude)); } if (data.knownFiles) { this.knownFiles = new Set(data.knownFiles); } } }