UNPKG

broccoli-eyeglass

Version:
970 lines 40.6 kB
"use strict"; var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; result["default"] = mod; return result; }; Object.defineProperty(exports, "__esModule", { value: true }); const debugGenerator = require("debug"); const path = __importStar(require("path")); const fs = require("fs-extra"); const mkdirp = require("mkdirp"); const BroccoliPlugin = require("broccoli-plugin"); const glob = require("glob"); const FSTree = require("fs-tree-diff"); const walkSync = require("walk-sync"); const queue = require("async-promise-queue"); const ensureSymlink = require("ensure-symlink"); const copyObject = require("lodash.clonedeep"); const MergeTrees = require("broccoli-merge-trees"); const chained_emitter_1 = require("chained-emitter"); const DiskCache = require("sync-disk-cache"); const heimdall = require("heimdalljs"); const fs_1 = require("fs"); const concurrency_1 = require("./concurrency"); const concurrency = Promise.resolve(concurrency_1.determineOptimalConcurrency()); const FSTreeFromEntries = FSTree.fromEntries; const debug = debugGenerator("broccoli-eyeglass"); const hotCacheDebug = debugGenerator("broccoli-eyeglass:hot-cache"); const concurrencyDebug = debug.extend("concurrency"); function findSass(sass) { if (sass) return sass; try { return require("node-sass"); } catch (e) { try { return require("sass"); } catch (e) { throw new Error("A sass engine was not provided and neither `sass` nor `node-sass` were found in the current project."); } } } function absolutizeEntries(entries) { // We make everything absolute because relative path comparisons don't work for us. entries.forEach(entry => { // TODO support windows paths entry.relativePath = path.join(entry.basePath, entry.relativePath); entry.basePath = "/"; }); } function shouldPersist(env, persist) { let result; if (env.CI) { result = env.FORCE_PERSISTENCE_IN_CI; } else { result = persist; } return !!result; } class Entry { constructor(path) { let stats = fs.statSync(path); this.relativePath = path; this.basePath = "/"; this.mode = stats.mode; this.size = stats.size; this.mtime = stats.mtime; } isDirectory() { return false; } } function unique(array) { return new Array(...new Set(array)); } function removePathPrefix(prefix, fileNames) { if (prefix[prefix.length - 1] !== path.sep) { prefix = prefix + path.sep; } let newFileNames = new Array(); for (let i = 0; i < fileNames.length; i++) { if (fileNames[i].indexOf(prefix) === 0) { newFileNames[i] = fileNames[i].substring(prefix.length); } else { newFileNames[i] = fileNames[i]; } } return newFileNames; } // sassFile: The sass file being compiled // cssFile: The default css file location. // cb: This callback must be invoked once for each time you want to compile the // sass file. It must be called synchronously. You can change the output // filename and options passed to it. const defaultOptionsGenerator = (_sassFile, cssFile, options, cb) => cb(cssFile, options); // Support for older versions of node. function parsePath(pathname) { if (path.parse) { return path.parse(pathname); } else { let parsed = { root: "", name: "", dir: path.dirname(pathname), base: path.basename(pathname), ext: path.extname(pathname), }; parsed.name = parsed.base.substring(0, parsed.base.length - parsed.ext.length); return parsed; } } function formatPath(parsed) { if (path.format) { return path.format(parsed); } else { return path.join(parsed.dir, parsed.name + parsed.ext); } } function forbidNodeSassOption(options, property) { if (options[property]) { throw new Error(`The node-sass option '${property}' cannot be set explicitly.`); } } /* write data to cachedFile, and symlink outputFile to that * * @argument cachedFile - the file to write the data to * @argument outputFile - where to write the symlink * @argument data - the data to write */ function writeDataToFile(cachedFile, outputFile, data) { mkdirp.sync(path.dirname(cachedFile)); fs.writeFileSync(cachedFile, data); mkdirp.sync(path.dirname(outputFile)); ensureSymlink(cachedFile, outputFile); } // This Sass compiler has a different philosophy than the default one // that comes with broccoli. It is directory based instead of being file // based and can merge several input trees into a single output tree // instead of using the trees as a proxy for includePaths. It has error // handling, verbose logging and will pass through any options that // it doesn't own. It uses the node-sass async api via promises. // It allows several css files to be compiled from a single sass file // by customizing the options and output file names. // // You can emit the following events from custom functions that are invoked during compilation: // * compiler.events.emit("dependency", absolutePath); // marks the file as a dependency for the Sass file being compiled so that future // compiles will invalidate the cache if that file changes. // * compiler.events.emit("additional-output", absolutePathToOutput, httpPathToOutput, absolutePathToSource); // marks the file as an additional output for the Sass file being compiled so that future // cached compiles will be able to install or remove them as needed in conjunction with the // sass file. Note: the source will not be considered a dependency unless the "dependency" event // is also emitted. // // You can subscribe to the following events: // // * compiler.events.on("compiling", function(details) { }); // prepare for a compilation to occur with these options. The options // are a unique copy for this compilation. E.g. This can be used to // prime a cache in the options for the compilation. // * compiler.events.on("compiled", (details, result) => { }); // receive notification of a successful compilation. // * compiler.events.on("failed", (details, error) => { }); // receive notification of a compilation failure. // * compiler.events.on("stale-external-output", (outputFile) => {}) // receive a notification that a file outside of this broccoli output // tree might need to be removed because the only known sass file // in this tree to output it has been deleted. // * compiler.events.on("cached-asset", (absolutePathToSource, httpPathToOutput) => {}) // receive a notification that an additional asset that was created // when the caller fired "additional-output" (see above) needs to be // restored because the sass file that produced it was retrieved from // cache. This is only invoked when the asset was created outside of the // broccoli tree for this addon. If the asset was in the tree, it will // be automatically recreated from the cache. // * compiler.events.on("build", (buildCount) => {}) // Receive an event that a new build is about to start. The number of // builds done prior to this build is passed as an argument. // // For all these events, a compilation details object is passed of the // following form: // // { // srcPath: /* Source directory containing the sassFilename */, // sassFilename: /* Sass file relative to the srcPath */, // fullSassFilename: /* Fully expanded and resolved sass filename. */, // destDir: /* The location css files are being written (a tmp dir) */, // cssFilename: /* The css filename relation to the output directory */, // fullCssFilename: /* Fully expanded and resolved css filename */, // options: /* The options used for this compile */ // }; // class BroccoliSassCompiler extends BroccoliPlugin { constructor(inputTree, options) { var _a; if (Array.isArray(inputTree)) { if (inputTree.length > 1) { // eslint-disable-next-line no-console console.warn("Support for passing several trees to BroccoliSassCompiler has been removed.\n" + "Passing the trees to broccoli-merge-trees with the overwrite option set,\n" + "but you should do this yourself if you need to compile CSS files from them\n" + "or use the node-sass includePaths option if you need to import from them."); inputTree = new MergeTrees(inputTree, { overwrite: true, annotation: "Sass Trees" }); } else { inputTree = inputTree[0]; } } options = options || {}; options.persistentOutput = true; super([inputTree], options); this.sass = findSass((_a = options.engines) === null || _a === void 0 ? void 0 : _a.sass); this.buildCount = 0; this.events = new chained_emitter_1.EventEmitter(); this.currentTree = null; this.dependencies = {}; this.outputs = {}; this.outputURLs = {}; this.sessionCache = options.sessionCache; delete options.sessionCache; this.buildCache = this.sessionCache || new Map(); if (shouldPersist(process.env, !!options.persistentCache)) { this.persistentCache = new DiskCache(options.persistentCache); } this.persistentCacheDebug = debugGenerator(`broccoli-eyeglass:persistent-cache:${options.persistentCache || 'disabled'}`); this.treeName = options.annotation; delete options.annotation; this.cssDir = options.cssDir; delete options.cssDir; this.sassDir = options.sassDir; delete options.sassDir; this.optionsGenerator = options.optionsGenerator || defaultOptionsGenerator; delete options.optionsGenerator; this.fullException = options.fullException || false; delete options.fullException; this.verbose = options.verbose || debugGenerator.enabled("broccoli-eyeglass:results") || debugGenerator.enabled("eyeglass:results"); delete options.verbose; this.renderSync = options.renderSync || false; delete options.renderSync; this.discover = options.discover; delete options.discover; if (!options.sourceFiles) { this.sourceFiles = []; if (this.discover === false) { throw new Error("sourceFiles are required when discovery is disabled."); } else { // Default to discovery mode if no sourcefiles are provided. this.discover = true; } } else { this.sourceFiles = options.sourceFiles; } delete options.sourceFiles; this.maxListeners = options.maxListeners || 10; delete options.maxListeners; this.options = copyObject(options); if (!this.cssDir) { throw new Error("Expected cssDir option."); } forbidNodeSassOption(this.options, "file"); forbidNodeSassOption(this.options, "data"); forbidNodeSassOption(this.options, "outFile"); if (this.verbose) { this.colors = require("colors/safe"); this.events.on("compiled", this.logCompilationSuccess.bind(this)); this.events.on("failed", this.logCompilationFailure.bind(this)); } this.events.addListener("compiled", (details, result) => { this.addOutput(details.fullSassFilename, details.fullCssFilename); let depFiles = result.stats.includedFiles; this.addDependency(details.fullSassFilename, details.fullSassFilename); for (let i = 0; i < depFiles.length; i++) { this.addDependency(details.fullSassFilename, depFiles[i]); } }); } /** * Wraps the node-style async render method with a promise. */ renderSassAsync(options) { return new Promise((resolve, reject) => { this.sass.render(options, (err, result) => { if (err) { reject(err); } else { resolve(result); } }); }); } /** * Wraps the sync render method with a promise that is either immediately * resolved or rejected. */ renderSassSync(options) { try { return Promise.resolve(this.sass.renderSync(options)); } catch (e) { return Promise.reject(e); } } logCompilationSuccess(details, result) { let timeInSeconds = result.stats.duration / 1000.0; if (timeInSeconds === 0) { timeInSeconds = 0.001; // nothing takes zero seconds. } let action = this.colors.inverse.green(`compile (${timeInSeconds}s)`); let message = this.scopedFileName(details.sassFilename) + " => " + details.cssFilename; // eslint-disable-next-line no-console console.log(action + " " + message); } logCompilationFailure(details, error) { let sassFilename = details.sassFilename; let action = this.colors.bgRed.white("error"); let message = this.colors.red(error.message); let location = `Line ${error.line}, Column ${error.column}`; if (error.file.substring(error.file.length - sassFilename.length) !== sassFilename) { location = location + " of " + error.file; } // eslint-disable-next-line no-console console.log(action + " " + sassFilename + " (" + location + "): " + message); } compileTree(srcPath, files, destDir, compilationTimer) { switch (files.length) { case 0: return Promise.resolve(); case 1: return Promise.all(this.compileSassFile(srcPath, files[0], destDir, compilationTimer)); default: { let worker = queue.async.asyncify((file) => { return Promise.all(this.compileSassFile(srcPath, file, destDir, compilationTimer)); }); return concurrency.then(numConcurrentCalls => { concurrencyDebug("Compiling files with a worker queue of size %d", numConcurrentCalls); return Promise.resolve(queue(worker, files, numConcurrentCalls)); }); } } } compileSassFile(srcPath, sassFilename, destDir, compilationTimer) { let sassOptions = copyObject(this.options); let fullSassFilename = path.join(srcPath, sassFilename); sassOptions.file = fullSassFilename; let parsedName = parsePath(sassFilename); if (this.sassDir && parsedName.dir.slice(0, this.sassDir.length) === this.sassDir) { parsedName.dir = parsedName.dir.slice(this.sassDir.length + 1); } parsedName.ext = ".css"; parsedName.base = parsedName.name + ".css"; let cssFileName = path.join(this.cssDir, formatPath(parsedName)); let promises = []; this.optionsGenerator(sassFilename, cssFileName, sassOptions, (resolvedCssFileName, resolvedOptions) => { let details = { srcPath: srcPath, sassFilename: sassFilename, fullSassFilename: resolvedOptions.file || fullSassFilename, destDir: destDir, cssFilename: resolvedCssFileName, fullCssFilename: path.join(destDir, resolvedCssFileName), options: copyObject(resolvedOptions), }; details.options.outFile = details.cssFilename; promises.push(this.compileCssFileMaybe(details, compilationTimer)); }); return promises; } render(options) { if (this.renderSync) { return this.renderSassSync(options); // XXX This cast sucks } else { return this.renderSassAsync(options); // This cast isn't strictly necessary } } /* Check if a dependency's hash has changed. * * @argument srcDir The directory in which to resolve relative paths against. * @argument dep An array of two elements, the first is the file and * the second is the last known hash of that file. * * @return Boolean true if the file hasn't changed from the input hash, otherwise false **/ dependencyChanged(srcDir, dep) { let file = path.isAbsolute(dep[0]) ? dep[0] : path.join(srcDir, dep[0]); let hexDigest = dep[1]; return hexDigest !== this.hashForFile(file); } /* get the cached output for a source file, or compile the file if not in cache * * @argument details The compilation details object. * * @return Promise that resolves to the cached output of the file or the output * of compiling the file **/ getFromCacheOrCompile(details, compilationTimer) { let key = this.keyForSourceFile(details.srcPath, details.sassFilename, details.options); try { let cachedDependencies = this.persistentCache.get(this.dependenciesKey(key)); if (!cachedDependencies.isCached) { let reason = { message: "no dependency data for " + details.sassFilename }; return this.handleCacheMiss(details, reason, key, compilationTimer); } let dependencies = JSON.parse(cachedDependencies.value); // check dependency caches if (dependencies.some(dep => this.dependencyChanged(details.srcPath, dep))) { let reason = { message: "dependency changed" }; return this.handleCacheMiss(details, reason, key, compilationTimer); } let cachedOutput = this.persistentCache.get(this.outputKey(key)); if (!cachedOutput.isCached) { let reason = { message: "no output data for " + details.sassFilename }; return this.handleCacheMiss(details, reason, key, compilationTimer); } let depFiles = dependencies.map(depAndHash => depAndHash[0]); let value = [depFiles, JSON.parse(cachedOutput.value)]; compilationTimer.stats.cacheHitCount++; return Promise.resolve(this.handleCacheHit(details, value).then(() => { })); } catch (error) { return this.handleCacheMiss(details, error, key, compilationTimer); } } /* compute the hash for a file. * * @argument absolutePath The absolute path to the file. * @return hash object of the file data **/ hashForFile(absolutePath) { return this.fileKey(absolutePath); } /* compute a key for a file that will change if the file has changed. */ fileKey(file) { let cachedKeyKey = `fileKey(${file})`; let cachedKey = this.buildCache.get(cachedKeyKey); if (cachedKey) { return cachedKey; } let key; try { let stat = fs_1.statSync(file); key = `${mtimeMs(stat)}:${stat.size}:${stat.mode}`; this.buildCache.set(cachedKeyKey, key); } catch (_) { key = `0:0:0`; } this.buildCache.set(cachedKeyKey, key); return key; } /* construct a base cache key for a file to be compiled. * * @argument srcDir The directory in which to resolve relative paths against. * @argument relativeFilename The filename relative to the srcDir that is being compiled. * @argument options The compilation options. * * @return The cache key for the file **/ keyForSourceFile(srcDir, relativeFilename, _options) { let absolutePath = path.join(srcDir, relativeFilename); let hash = this.hashForFile(absolutePath); return relativeFilename + "@" + hash; } /* construct a cache key for storing dependency hashes. * * @argument key The base cache key * @return String */ dependenciesKey(key) { return "[[[dependencies of " + key + "]]]"; } /* construct a cache key for storing output. * * @argument key The base cache key * @return String */ outputKey(key) { return "[[[output of " + key + "] v2]]"; } /* retrieve the files from cache, write them, and populate the hot cache information * for rebuilds. */ handleCacheHit(details, inputAndOutputFiles) { let [inputFiles, outputFiles] = inputAndOutputFiles; this.persistentCacheDebug("Persistent cache hit for %s. Writing to: %s", details.sassFilename, details.fullCssFilename); if (this.verbose) { let action = this.colors.inverse.green("cached"); let message = this.scopedFileName(details.sassFilename) + " => " + details.cssFilename; // eslint-disable-next-line no-console console.log(action + " " + message); } inputFiles.forEach(dep => { // populate the dependencies cache for rebuilds this.addDependency(details.fullSassFilename, path.resolve(details.srcPath, dep)); }); let { contents, urls } = outputFiles; let files = Object.keys(contents); this.persistentCacheDebug("cached output files for %s are: %s", details.sassFilename, files.join(", ")); for (let file of files) { let data = contents[file]; let cachedFile = path.join(this.cachePath, file); let outputFile = path.join(this.outputPath, file); // populate the output cache for rebuilds this.addOutput(details.fullSassFilename, outputFile); writeDataToFile(cachedFile, outputFile, Buffer.from(data, "base64")); } let eventPromises = []; let allUrls = Object.keys(urls); if (allUrls.length > 0) { this.persistentCacheDebug("firing 'cached-asset' for each asset url for %s: %s", details.sassFilename, allUrls.join(", ")); } for (let url of allUrls) { let sourceFile = urls[url]; eventPromises.push(this.events.emit("cached-asset", sourceFile, url)); } return Promise.all(eventPromises); } scopedFileName(file) { file = this.relativize(file); if (this.treeName) { return this.treeName + "/" + file; } else { return file; } } relativize(file) { return removePathPrefix(this.inputPaths[0], [file])[0]; } isOutputInTree(file) { if (path.isAbsolute(file)) { return file.startsWith(this.outputPath); } else { return true; } } relativizeOutput(file) { return removePathPrefix(this.outputPath, [file])[0]; } relativizeAll(files) { return removePathPrefix(this.inputPaths[0], files); } hasDependenciesSet(file) { return this.dependencies[this.relativize(file)] !== undefined; } dependenciesOf(file) { return this.dependencies[this.relativize(file)] || new Set(); } outputsFrom(file) { return this.outputs[this.relativize(file)] || new Set(); } outputURLsFrom(file) { return this.outputURLs[this.relativize(file)] || new Map(); } /** * Some filenames returned from importers are not really files * on disk. These three prefixes are used in eyeglass. * Skipping a read on these files avoids a more expensive fs call * and exception handling. * @param filename a filename returned from an importer */ isNotFile(filename) { return filename.startsWith("already-imported:") || filename.startsWith("autoGenerated:") || filename.startsWith("fs:"); } /* hash all dependencies synchronously and return the files that exist * as an array of pairs (filename, hash). */ hashDependencies(details) { let depsWithHashes = new Array(); this.dependenciesOf(details.fullSassFilename).forEach(f => { try { if (this.isNotFile(f)) { this.persistentCacheDebug("Ignoring non-file dependency: %s", f); } else { let h = this.hashForFile(f); if (f.startsWith(details.srcPath)) { f = f.substring(details.srcPath.length + 1); } depsWithHashes.push([f, h]); } } catch (e) { if (typeof e === "object" && e !== null && e.code === "ENOENT") { this.persistentCacheDebug("Ignoring non-existent file: %s", f); } else { throw e; } } }); // prune out the dependencies that weren't files. return depsWithHashes; } /* read all output files asynchronously and return the contents * as a hash of relative filenames to base64 encoded strings. */ readOutputs(details) { let contents = {}; let urls = {}; let outputs = this.outputsFrom(details.fullSassFilename); for (let output of outputs) { if (this.isOutputInTree(output)) { contents[this.relativizeOutput(output)] = fs.readFileSync(output, "base64"); } else { this.persistentCacheDebug("refusing to cache output file found outside the output tree: %s", output); } } let outputURLs = this.outputURLsFrom(details.fullSassFilename); for (let url of outputURLs.keys()) { urls[url] = outputURLs.get(url); } return { contents, urls }; } /* Writes the dependencies and output contents to the persistent cache */ populateCache(key, details, _result) { this.persistentCacheDebug("Populating cache for " + key); let cache = this.persistentCache; let depsWithHashes = this.hashDependencies(details); let outputContents = this.readOutputs(details); cache.set(this.dependenciesKey(key), JSON.stringify(depsWithHashes)); cache.set(this.outputKey(key), JSON.stringify(outputContents)); } /* When the cache misses, we need to compile the file and then populate the cache */ handleCacheMiss(details, reason, key, compilationTimer) { compilationTimer.stats.cacheMissCount++; this.persistentCacheDebug("Persistent cache miss for %s. Reason: %s", details.sassFilename, reason.message); // for errors if (reason.stack) { this.persistentCacheDebug("Stacktrace:", reason.stack); } return this.compileCssFile(details, compilationTimer).then(result => { return this.populateCache(key, details, result); }); } /* Compile the file if it's not in the cache. * Reuse cached output if it is. * * @argument details The compilation details object. * * @return A promise that resolves when the output files are written * either from cache or by compiling. Rejects on error. */ compileCssFileMaybe(details, compilationTimer) { if (this.persistentCache) { return this.getFromCacheOrCompile(details, compilationTimer); } else { return this.compileCssFile(details, compilationTimer); } } compileCssFile(details, compilationTimer) { let success = this.handleSuccess.bind(this, details); let failure = this.handleFailure.bind(this, details); return this.events.emit("compiling", details).then(() => { let dependencyListener = (absolutePath) => { this.addDependency(details.fullSassFilename, absolutePath); }; let additionalOutputListener = (absolutePathToOutput, httpPathToOutput, absolutePathToSource) => { this.persistentCacheDebug("additional-output %s -> %s -> %s", absolutePathToSource, httpPathToOutput, absolutePathToOutput); if (!this.isOutputInTree(absolutePathToOutput)) { // it's outside this tree, don't cache the output. if (absolutePathToSource && httpPathToOutput) { this.persistentCacheDebug("additional-output is outside tree will cache source & url"); // something outside this tree is putting it there, so we need to // let that same thing deal with it again when the warm cache is accessed. // we will track this file from its source location and target url this.addSource(details.fullSassFilename, absolutePathToSource, httpPathToOutput); } } else { this.persistentCacheDebug("additional-output is in tree will cache contents"); this.addOutput(details.fullSassFilename, absolutePathToOutput); } }; this.events.addListener("additional-output", additionalOutputListener); this.events.addListener("dependency", dependencyListener); return this.render(details.options) .finally(() => { this.events.removeListener("dependency", dependencyListener); this.events.removeListener("additional-output", additionalOutputListener); }) .then(result => { compilationTimer.stats.nodeSassTime += result.stats.duration; compilationTimer.stats.importCount += result.stats.includedFiles.length; for (let f of result.stats.includedFiles) { if (!f.startsWith("already-imported:")) { compilationTimer.stats.uniqueImportCount++; } } debug(`render of ${result.stats.entry} took ${result.stats.duration}`); return success(result).then(() => result); }, failure); }); } handleSuccess(details, result) { let cachedFile = path.join(this.cachePath, details.cssFilename); let outputFile = details.fullCssFilename; writeDataToFile(cachedFile, outputFile, result.css); return this.events.emit("compiled", details, result); } handleFailure(details, error) { let failed = this.events.emit("failed", details, error); return failed.then(() => { if (typeof error === "object" && error !== null) { error.message = `${error.message}\n at ${error.file}:${error.line}:${error.column}`; } throw error; }); } filesInTree(srcPath) { let sassDir = this.sassDir || ""; let files = new Array(); if (this.discover) { let pattern = path.join(srcPath, sassDir, "**", "[^_]*.scss"); files = glob.sync(pattern); } this.sourceFiles.forEach(sourceFile => { let pattern = path.join(srcPath, sassDir, sourceFile); files = files.concat(glob.sync(pattern)); }); return unique(files); } addSource(sassFilename, sourceFilename, httpPathToOutput) { sassFilename = this.relativize(sassFilename); this.outputURLs[sassFilename] = this.outputURLs[sassFilename] || new Map(); let urlMap = this.outputURLs[sassFilename]; urlMap.set(httpPathToOutput, sourceFilename); } addOutput(sassFilename, outputFilename) { sassFilename = this.relativize(sassFilename); this.outputs[sassFilename] = this.outputs[sassFilename] || new Set(); this.outputs[sassFilename].add(outputFilename); } clearOutputs(files) { this.relativizeAll(files).forEach(f => { if (this.outputs[f]) { delete this.outputs[f]; } if (this.outputURLs[f]) { delete this.outputURLs[f]; } }); } /* This method computes the output files that are only output for at least one given inputs * and never for an input that isn't provided. * * This is important because the same assets might be output from compiling several * different inputs for tools like eyeglass assets. * * @return Set<String> The full paths output files. */ outputsFromOnly(inputs) { inputs = this.relativizeAll(inputs); let otherOutputs = new Set(); let onlyOutputs = new Set(); let allInputs = Object.keys(this.outputs); for (let i = 0; i < allInputs.length; i++) { let outputs = this.outputs[allInputs[i]]; if (inputs.indexOf(allInputs[i]) < 0) { outputs.forEach(output => otherOutputs.add(output)); } else { outputs.forEach(output => onlyOutputs.add(output)); } } onlyOutputs.forEach(only => { if (otherOutputs.has(only)) { onlyOutputs.delete(only); } }); return onlyOutputs; } addDependency(sassFilename, dependencyFilename) { sassFilename = this.relativize(sassFilename); this.dependencies[sassFilename] = this.dependencies[sassFilename] || new Set(); this.dependencies[sassFilename].add(dependencyFilename); } clearDependencies(files) { this.relativizeAll(files).forEach(f => { delete this.dependencies[f]; }); } knownDependencies() { let deps = new Set(); let sassFiles = Object.keys(this.dependencies); for (let i = 0; i < sassFiles.length; i++) { let sassFile = sassFiles[i]; deps.add(sassFile); this.dependencies[sassFile].forEach(dep => deps.add(dep)); } let entries = new Array(); deps.forEach(d => { try { entries.push(new Entry(d)); } catch (e) { // Lots of things aren't files that are dependencies, ignore them. } }); return entries; } hasKnownDependencies() { return Object.keys(this.dependencies).length > 0; } knownDependenciesTree(inputPath) { let entries = walkSync.entries(inputPath); absolutizeEntries(entries); let tree = FSTreeFromEntries(entries); tree.addEntries(this.knownDependencies(), { sortAndExpand: true }); return tree; } _reset() { this.currentTree = null; this.dependencies = {}; this.outputs = {}; this.outputURLs = {}; } _build() { let inputPath = this.inputPaths[0]; let outputPath = this.outputPath; let currentTree = this.currentTree; let nextTree = null; let patches = new Array(); let compilationAvoidanceTimer = heimdall.start("eyeglass:broccoli:build:invalidation"); if (this.hasKnownDependencies()) { hotCacheDebug("Has known dependencies"); nextTree = this.knownDependenciesTree(inputPath); this.currentTree = nextTree; currentTree = currentTree || new FSTree(); patches = currentTree.calculatePatch(nextTree); hotCacheDebug("currentTree = ", currentTree); hotCacheDebug("nextTree = ", nextTree); hotCacheDebug("patches = ", patches); } else { hotCacheDebug("No known dependencies"); } // TODO: handle indented syntax files. let treeFiles = removePathPrefix(inputPath, this.filesInTree(inputPath)); treeFiles = treeFiles.filter(f => { f = path.join(inputPath, f); if (!this.hasDependenciesSet(f)) { hotCacheDebug("no deps for", this.scopedFileName(f)); return true; } let deps = this.dependenciesOf(f); hotCacheDebug("dependencies are", deps); for (var p = 0; p < patches.length; p++) { let entry = patches[p][2]; hotCacheDebug("looking for", entry.relativePath); if (deps.has(entry.relativePath)) { hotCacheDebug("building because", entry.relativePath, "is used by", f); return true; } } if (this.verbose) { let action = this.colors.inverse.green("unchanged"); let message = this.scopedFileName(f); // eslint-disable-next-line no-console console.log(action + " " + message); } return false; }); compilationAvoidanceTimer.stop(); // Cleanup any unneeded output files let removed = []; for (var p = 0; p < patches.length; p++) { if (patches[p][0] === "unlink") { let entry = patches[p][2]; if (entry.relativePath.indexOf(inputPath) === 0) { removed.push(entry.relativePath); } } } if (removed.length > 0) { let outputs = this.outputsFromOnly(removed); // TODO: outputURLsFromOnly(removed) outputs.forEach(output => { if (output.indexOf(outputPath) === 0) { fs.unlinkSync(output); } else { hotCacheDebug("not removing because outside the outputTree", output); this.events.emit("stale-external-output", output); } }); this.clearOutputs(removed); } hotCacheDebug("building files:", treeFiles); let absoluteTreeFiles = treeFiles.map(f => path.join(inputPath, f)); this.clearDependencies(absoluteTreeFiles); this.clearOutputs(absoluteTreeFiles); let internalListeners = absoluteTreeFiles.length * 2 + // 1 dep & 1 output listeners each 1 + // one compilation listener (this.verbose ? 2 : 0); // 2 logging listeners if in verbose mode debug("There are %d internal event listeners.", internalListeners); debug("Setting max external listeners to %d via the maxListeners option (default: 10).", this.maxListeners); this.events.setMaxListeners(internalListeners + this.maxListeners); let compilationTimer = heimdall.start(`eyeglass:broccoli:build:compileTree:${inputPath}`, SassRenderSchema); compilationTimer.stats.numSassFiles = treeFiles.length; return this.compileTree(inputPath, treeFiles, outputPath, compilationTimer).finally(() => { compilationTimer.stop(); if (!this.currentTree) { this.currentTree = this.knownDependenciesTree(inputPath); } }); } async build() { await this.events.emit("build", this.buildCount); this.buildCount++; if (this.buildCount > 1) { this.buildCache = this.sessionCache || new Map(); } else { if (process.env.BROCCOLI_EYEGLASS === "forceInvalidateCache") { this.persistentCacheDebug("clearing cache because forceInvalidateCache was set."); this.persistentCache && this.persistentCache.clear(); } } try { await this._build(); } catch (e) { this._reset(); fs.removeSync(this.outputPath); fs.mkdirpSync(this.outputPath); throw e; } return; } } exports.default = BroccoliSassCompiler; class SassRenderSchema { constructor() { this.cacheMissCount = 0; this.cacheHitCount = 0; this.numSassFiles = 0; this.nodeSassTime = 0; this.importCount = 0; this.uniqueImportCount = 0; } } module.exports.shouldPersist = shouldPersist; /* shim for fs.Stats.mtimeMS which was introduced in node 8. */ function mtimeMs(stat) { if (stat.mtimeMs) { return stat.mtimeMs; } else { return stat.mtime.valueOf(); } } //# sourceMappingURL=broccoli_sass_compiler.js.map