grunt-ps-sass
Version:
GruntJS plugin to compile Sass files
199 lines (164 loc) • 6.18 kB
JavaScript
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;