broccoli-eyeglass
Version:
Sass compiler for Broccoli with Eyeglass Integration
970 lines • 40.6 kB
JavaScript
"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