UNPKG

grunt-ps-sass

Version:
199 lines (164 loc) 6.18 kB
const fs = require('fs'); const path = require('path'); const sass = require('sass'); const log = require('grunt-ps-log'); const FileChangeDetector = require('./file-change-detector'); const ScssDependencyResolver = require('./scss-dependency-resolver'); class SassOptimizedSvc { constructor(options = {}) { this.maxConcurrency = options.maxConcurrency || 4; this.skipUnchanged = options.skipUnchanged !== false; // default true this.sassOptions = options.sassOptions || {}; // Initialize separated services this.dependencyResolver = new ScssDependencyResolver({ verbose: options.verbose || false, includeExtensions: options.includeExtensions, resolveNodeModules: options.resolveNodeModules }); this.changeDetector = new FileChangeDetector({ verbose: options.verbose || false }); } /** * Check if a file needs compilation by comparing modification times * of the main file and all its dependencies using separated services */ #needsCompilation(file) { if (!this.skipUnchanged) { return true; } try { // Check if destination file exists const destStat = fs.statSync(file.dest); const destTime = destStat.mtime; // Get all dependencies for the source file const dependencyResult = this.dependencyResolver.getDependencies(file.src); const allFiles = [file.src, ...dependencyResult.dependencies]; // Check if any file (source or dependencies) has changed since destination was created const changeResult = this.changeDetector.checkForChanges(allFiles, destTime); if (changeResult.hasChanges && changeResult.changedFiles.length > 0) { log.verbose(`File needs compilation due to changes in: ${changeResult.changedFiles.map(f => f.path).join(', ')}`); return true; } return false; } catch(err) { // If dest doesn't exist or other error, needs compilation log.verbose(`File needs compilation: ${err.message}`); return true; } } /** * Compile a single Sass file asynchronously */ async #compileFile(file) { log.verbose(`Compiling ${file.src}`); try { // Use async compilation for better performance const out = await sass.compileAsync(file.src, { style: 'compressed', // Optimize output by default ...this.sassOptions }); const dir = path.dirname(file.dest); // Create directory if it doesn't exist if (!fs.existsSync(dir)) { await fs.promises.mkdir(dir, { recursive: true }); } // Write the compiled CSS await fs.promises.writeFile(file.dest, out.css); return { success: true, file: file.src, dest: file.dest, size: out.css.length }; } catch(err) { log.error(`Error compiling ${file.src}: ${err.message}`); return { success: false, file: file.src, error: err }; } } /** * Process files in controlled batches to manage concurrency */ async #compileBatch(files) { const results = []; for (let i = 0; i < files.length; i += this.maxConcurrency) { const batch = files.slice(i, i + this.maxConcurrency); log.verbose(`Processing batch ${Math.floor(i / this.maxConcurrency) + 1} (${batch.length} files)`); const batchResults = await Promise.allSettled( batch.map(file => this.#compileFile(file)) ); // Convert PromiseSettledResult to our format const processedResults = batchResults.map((result, index) => { if (result.status === 'fulfilled') { return result.value; } else { return { success: false, file: batch[index].src, error: result.reason }; } }); results.push(...processedResults); } return results; } /** * Main method to compile multiple files with improved performance */ async compileFiles(files, done) { try { // Filter files that need compilation const filesToCompile = files.filter(file => this.#needsCompilation(file)); if (filesToCompile.length === 0) { log.ok('All files are up to date'); done(); return; } if (filesToCompile.length < files.length) { log.verbose(`${filesToCompile.length} of ${files.length} files need compilation`); } const startTime = Date.now(); log.verbose(`Starting compilation of ${filesToCompile.length} file(s) with concurrency: ${this.maxConcurrency}`); // Compile files in controlled batches const results = await this.#compileBatch(filesToCompile); // Analyze results const successful = results.filter(r => r.success); const failed = results.filter(r => !r.success); const endTime = Date.now(); const duration = endTime - startTime; // Log results if (failed.length > 0) { log.error(`${failed.length} file(s) failed to compile:`); failed.forEach(f => log.error(` - ${f.file}: ${f.error?.message || 'Unknown error'}`)); } if (successful.length > 0) { const totalSize = successful.reduce((sum, r) => sum + (r.size || 0), 0); log.ok(`Successfully compiled ${successful.length} file(s) in ${duration}ms (${Math.round(totalSize / 1024)}KB total)`); } // Call done regardless, but Grunt will know about failures from the logs done(); } catch(err) { log.error(`Compilation failed: ${err.message}`); log.fail('Sass compile failed!'); done(); } } /** * Fallback method for backward compatibility with original sync API */ compileFilesSync(files, done) { // Convert to promise-based and handle in next tick to maintain async behavior setImmediate(() => { this.compileFiles(files, done).catch(err => { log.error(err); done(); }); }); } } module.exports = SassOptimizedSvc;