Files
meteor/packages/less/plugin/compile-less.js
David Glasser cf737183ca Change import subdirectory name to 'imports'
@dgreensp:

    The more I think about it, the more I don't like naming a directory
    after a singular noun like `import`.  We have `packages/` already,
    and I want to put my modules in a directory called `modules/`, not
    `module/`, just like I don't want to put my assets in `asset/`, or
    my stylesheets in `stylesheet/`, etc.  It's ok to have a singular
    suffix and a plural directory name: to have `.import` files go in
    `imports` or `.template` files go in `templates`.  Directory names
    like `doc` and `lib` follow an old Unix convention of three-letter
    names.  And if the `import` directory name is actually a verb, not a
    noun, that is confusing because putting files in the `import`
    directory does not import them.  It just causes them to be
    considered imports (noun).
2015-07-16 12:48:53 -07:00

269 lines
9.2 KiB
JavaScript

var fs = Npm.require('fs');
var path = Npm.require('path');
var less = Npm.require('less');
var util = Npm.require('util');
var Future = Npm.require('fibers/future');
var LRU = Npm.require('lru-cache');
Plugin.registerCompiler({
// *.lessimport has been deprecated since 0.7.1, but it still works. We
// *recommend *.import.less or the imports subdirectory instead.
extensions: ['less', 'lessimport'],
archMatching: 'web'
}, function () {
return new LessCompiler();
});
var CACHE_SIZE = process.env.METEOR_LESS_CACHE_SIZE || 1024*1024*10;
var CACHE_DEBUG = !! process.env.METEOR_TEST_PRINT_CACHE_DEBUG;
var LessCompiler = function () {
var self = this;
// absoluteImportPath -> { hashes, css, sourceMap }
// where hashes is a map from absoluteImportPath -> hash of all
// paths used by it (including it itself)
self._cache = new LRU({
max: CACHE_SIZE,
// Cache is measured in bytes (not counting the hashes).
length: function (value) {
return value.css.length + sourceMapLength(value.sourceMap);
}
});
self._diskCache = null;
// For testing.
self._callCount = 0;
};
_.extend(LessCompiler.prototype, {
processFilesForTarget: function (inputFiles) {
var self = this;
var filesByAbsoluteImportPath = {};
var roots = [];
var cacheMisses = [];
function decodeFilePath (filePath) {
var match = filePath.match(/^{(.*)}\/(.*)$/);
if (! match)
throw new Error('Failed to decode Less path: ' + filePath);
if (match[1] === '') {
// app
return match[2];
}
return 'packages/' + match[1] + '/' + match[2];
}
inputFiles.forEach(function (inputFile) {
var packageName = inputFile.getPackageName();
var pathInPackage = inputFile.getPathInPackage();
var fileOptions = inputFile.getFileOptions();
var absoluteImportPath = packageName === null
? ('{}/' + pathInPackage)
: ('{' + packageName + '}/' + pathInPackage);
filesByAbsoluteImportPath[absoluteImportPath] = inputFile;
// The heuristic is that a file is an import (ie, is not itself processed
// as a root) if it is in a subdirectory named 'imports' or if it matches
// *.import.less. This can be overridden in either direction via an
// explicit `isImport` file option in apiaddFiles.
var filenameSaysImport =
/\.import\.less$/.test(pathInPackage) ||
/\.lessimport$/.test(pathInPackage) ||
/(?:^|\/)imports\//.test(pathInPackage);
var isImport = fileOptions.hasOwnProperty('isImport')
? fileOptions.isImport : filenameSaysImport;
// Match files named `main.less` or with a `.main.less` extension
if (! isImport) {
roots.push({inputFile: inputFile,
absoluteImportPath: absoluteImportPath});
}
});
var importPlugin = new MeteorImportLessPlugin(filesByAbsoluteImportPath);
roots.forEach(function (main) {
var inputFile = main.inputFile;
var absoluteImportPath = main.absoluteImportPath;
var cacheEntry = self._cache.get(absoluteImportPath);
if (! (cacheEntry &&
self._cacheEntryValid(cacheEntry, filesByAbsoluteImportPath))) {
cacheMisses.push(inputFile.getDisplayPath());
var f = new Future;
less.render(inputFile.getContentsAsBuffer().toString('utf8'), {
filename: absoluteImportPath,
plugins: [importPlugin],
// Generate a source map, and include the source files in the
// sourcesContent field. (Note that source files which don't
// themselves produce text (eg, are entirely variable definitions)
// won't end up in the source map!)
sourceMap: { outputSourceFiles: true }
}, f.resolver());
try {
var output = f.wait();
} catch (e) {
inputFile.error({
message: e.message,
sourcePath: decodeFilePath(e.filename),
line: e.line,
column: e.column
});
return; // go on to next file
}
if (output.map) {
var map = JSON.parse(output.map);
map.sources = map.sources.map(decodeFilePath);
output.map = map;
}
cacheEntry = {
hashes: {},
css: output.css,
sourceMap: output.map
};
// Make this cache entry depend on the hash of the file itself...
cacheEntry.hashes[absoluteImportPath] = inputFile.getSourceHash();
// ... and of all files it (transitively) imports, helpfully provided
// to us by less.render.
output.imports.forEach(function (path) {
if (! filesByAbsoluteImportPath.hasOwnProperty(path)) {
throw Error("Imported an unknown file?");
}
var importedInputFile = filesByAbsoluteImportPath[path];
cacheEntry.hashes[path] = importedInputFile.getSourceHash();
});
// Override existing cache entry, if any.
self._cache.set(absoluteImportPath, cacheEntry);
}
inputFile.addStylesheet({
data: cacheEntry.css,
path: inputFile.getPathInPackage() + '.css',
sourceMap: cacheEntry.sourceMap
});
});
// Rewrite the cache to disk.
// XXX #BBPBetterCache we should just write individual entries separately.
self._writeCache();
if (CACHE_DEBUG) {
cacheMisses.sort();
console.log("Ran less.render (#%s) on: %s",
++self._callCount, JSON.stringify(cacheMisses));
}
},
_cacheEntryValid: function (cacheEntry, filesByAbsoluteImportPath) {
var self = this;
return _.all(cacheEntry.hashes, function (hash, path) {
return _.has(filesByAbsoluteImportPath, path) &&
filesByAbsoluteImportPath[path].getSourceHash() === hash;
});
},
setDiskCacheDirectory: function (diskCache) {
var self = this;
if (self._diskCache)
throw Error("setDiskCacheDirectory called twice?");
self._diskCache = diskCache;
self._readCache();
},
// XXX #BBPBetterCache this is an inefficiently designed cache that will cause
// quadratic behavior due to writing the whole cache on each write, and has no
// error handling, and uses sync, and has an exists/read race condition, and
// might not work on Windows
_cacheFile: function () {
var self = this;
return path.join(self._diskCache, 'cache.json');
},
_readCache: function () {
var self = this;
var cacheFile = self._cacheFile();
if (! fs.existsSync(cacheFile))
return;
var cacheJSON = JSON.parse(fs.readFileSync(cacheFile));
_.each(cacheJSON, function (value, cacheKey) {
self._cache.set(cacheKey, value);
});
if (CACHE_DEBUG) {
console.log("Loaded less cache");
}
},
_writeCache: function () {
var self = this;
if (! self._diskCache)
return;
var cacheJSON = {};
self._cache.forEach(function (value, cacheKey) {
cacheJSON[cacheKey] = value;
});
fs.writeFileSync(self._cacheFile(), JSON.stringify(cacheJSON));
}
});
var MeteorImportLessPlugin = function (filesByAbsoluteImportPath) {
var self = this;
self.filesByAbsoluteImportPath = filesByAbsoluteImportPath;
};
_.extend(MeteorImportLessPlugin.prototype, {
install: function (less, pluginManager) {
var self = this;
pluginManager.addFileManager(
new MeteorImportLessFileManager(self.filesByAbsoluteImportPath));
},
minVersion: [2, 5, 0]
});
var MeteorImportLessFileManager = function (filesByAbsoluteImportPath) {
var self = this;
self.filesByAbsoluteImportPath = filesByAbsoluteImportPath;
};
util.inherits(MeteorImportLessFileManager, less.AbstractFileManager);
_.extend(MeteorImportLessFileManager.prototype, {
// We want to be the only active FileManager, so claim to support everything.
supports: function () {
return true;
},
loadFile: function (filename, currentDirectory, options, environment, cb) {
var self = this;
var packageMatch = currentDirectory.match(/^(\{[^}]*\})/);
if (! packageMatch) {
// shouldn't happen. all filenames less ever sees should involve this {}
// thing!
throw new Error("file without Meteor context? " + currentDirectory);
}
var currentPackagePrefix = packageMatch[1];
var resolvedFilename;
if (filename[0] === '/') {
// Map `/foo/bar.less` onto `{thispackage}/foo/bar.less`
resolvedFilename = currentPackagePrefix + filename;
} else if (filename[0] === '{') {
resolvedFilename = filename;
} else {
resolvedFilename = path.join(currentDirectory, filename);
}
if (! _.has(self.filesByAbsoluteImportPath, resolvedFilename)) {
cb({type: "File", message: "Unknown import: " + filename});
return;
}
cb(null, {
contents: self.filesByAbsoluteImportPath[resolvedFilename]
.getContentsAsBuffer().toString('utf8'),
filename: resolvedFilename
});
return;
}
});
function sourceMapLength(sm) {
if (! sm) return 0;
// sum the length of sources and the mappings, the size of
// metadata is ignored, but it is not a big deal
return sm.mappings.length
+ (sm.sourcesContent || []).reduce(function (soFar, current) {
return soFar + (current ? current.length : 0);
}, 0);
};