304 lines
9.3 KiB
JavaScript
304 lines
9.3 KiB
JavaScript
|
var path = require('path');
|
||
|
var fs = require('fs');
|
||
|
|
||
|
var Evaluator = require('stylus/lib/visitor/evaluator');
|
||
|
var loaderUtils = require('loader-utils');
|
||
|
var nodes = require('stylus/lib/nodes');
|
||
|
var utils = require('stylus/lib/utils');
|
||
|
var when = require('when');
|
||
|
var whenNodefn = require('when/node/function');
|
||
|
|
||
|
var listImports = require('./listimports');
|
||
|
|
||
|
module.exports = PathCache;
|
||
|
|
||
|
var readFile = whenNodefn.lift(fs.readFile);
|
||
|
|
||
|
// A cache of import paths of a stylus file resolved to their location on disk
|
||
|
// before the stylus file is rendered. With a special evaluator this lets us
|
||
|
// webpack's resolver.
|
||
|
function PathCache(contexts, sources, imports) {
|
||
|
this.contexts = contexts;
|
||
|
this.sources = sources;
|
||
|
this.imports = imports;
|
||
|
|
||
|
// Non relative paths are simpler and looked up in this as a fallback
|
||
|
// to this.context.
|
||
|
this.simpleContext = {};
|
||
|
for (var dirname in this.contexts) {
|
||
|
for (var path in this.contexts[dirname]) {
|
||
|
this.simpleContext[path] = this.contexts[dirname][path];
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Return a promise for a PathCache.
|
||
|
PathCache.create = function(contexts, sources, imports) {
|
||
|
return when(new PathCache(contexts, sources, imports));
|
||
|
};
|
||
|
PathCache.createFromFile = resolveFileDeep;
|
||
|
|
||
|
// Create a list of ways to resolve paths.
|
||
|
PathCache.resolvers = resolvers;
|
||
|
|
||
|
PathCache.resolvers.reduce = reduceResolvers;
|
||
|
|
||
|
// Lookup the path in this cache.
|
||
|
PathCache.prototype.find = function(path, dirname) {
|
||
|
if (this.contexts[dirname] && this.contexts[dirname][path]) {
|
||
|
return this.contexts[dirname][path].path;
|
||
|
} else if (this.simpleContext[path]) {
|
||
|
return this.simpleContext[path].path;
|
||
|
} else if (/.styl$/.test(path)) {
|
||
|
// A user can specify @import 'something.styl' but if they specify
|
||
|
// @import 'something' stylus adds .styl, we drop that here to see if we
|
||
|
// looked for it without .styl.
|
||
|
return this.find(path.replace(/.styl$/, ''), dirname);
|
||
|
} else {
|
||
|
return undefined;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// Return if the path in this cache is an index file.
|
||
|
PathCache.prototype.isIndex = function(path, dirname) {
|
||
|
if (this.contexts[dirname] && this.contexts[dirname][path]) {
|
||
|
return this.contexts[dirname][path].index;
|
||
|
} else {
|
||
|
return undefined;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// Return an array of all imports the original file depends on.
|
||
|
PathCache.prototype.allDeps = function() {
|
||
|
var deps = [];
|
||
|
for (var dirname in this.contexts) {
|
||
|
for (var path in this.contexts[dirname]) {
|
||
|
if (this.contexts[dirname][path]) {
|
||
|
deps = deps.concat(this.contexts[dirname][path].path);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return deps;
|
||
|
};
|
||
|
|
||
|
// Create an array of ways to resolve a path.
|
||
|
//
|
||
|
// The resolved paths may be a path or an object specifying path and index
|
||
|
// members. The index member is used later by stylus, we store it at this point.
|
||
|
function resolvers(options, webpackResolver) {
|
||
|
var evaluator = new Evaluator(nodes.null, options);
|
||
|
var whenWebpackResolver = whenNodefn.lift(webpackResolver);
|
||
|
|
||
|
// Stylus's normal resolver for single files.
|
||
|
var stylusFile = function(context, path) {
|
||
|
// Stylus adds .styl to paths for normal "paths" lookup if it isn't there.
|
||
|
if (!/.styl$/.test(path)) {
|
||
|
path += '.styl';
|
||
|
}
|
||
|
|
||
|
var paths = options.paths.concat(context);
|
||
|
var found = utils.find(path, paths, options.filename)
|
||
|
if (found) {
|
||
|
return normalizePaths(found);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// Stylus's normal resolver for node_modules packages. Cannot locate paths
|
||
|
// inside a package.
|
||
|
var stylusIndex = function(context, path) {
|
||
|
// Stylus calls the argument name. If it exists it should match the name
|
||
|
// of a module in node_modules.
|
||
|
if (!path) {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
var paths = options.paths.concat(context);
|
||
|
var found = utils.lookupIndex(path, paths, options.filename);
|
||
|
if (found) {
|
||
|
return {path: normalizePaths(found), index: true};
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// Fallback to resolving with webpack's configured resovler.
|
||
|
var webpackResolve = function(context, path) {
|
||
|
// Follow the webpack stylesheet idiom of '~path' meaning a path in a
|
||
|
// modules folder and a unprefixed 'path' meaning a relative path like
|
||
|
// './path'.
|
||
|
path = loaderUtils.urlToRequest(path, options.root);
|
||
|
// First try with a '.styl' extension.
|
||
|
return whenWebpackResolver(context, path + '.styl')
|
||
|
// If the user adds ".styl" to resolve.extensions, webpack can find
|
||
|
// index files like stylus but it uses all of webpack's configuration,
|
||
|
// by default for example the module could be web_modules.
|
||
|
.catch(function() { return whenWebpackResolver(context, path); })
|
||
|
.catch(function() { return null; })
|
||
|
.then(function(result) {
|
||
|
return Array.isArray(result) && result[1] && result[1].path || result
|
||
|
});
|
||
|
};
|
||
|
|
||
|
if (options.preferPathResolver === 'webpack') {
|
||
|
return [
|
||
|
webpackResolve,
|
||
|
stylusFile,
|
||
|
stylusIndex
|
||
|
];
|
||
|
}
|
||
|
else {
|
||
|
return [
|
||
|
stylusFile,
|
||
|
stylusIndex,
|
||
|
webpackResolve
|
||
|
];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function reduceResolvers(resolvers, context, path) {
|
||
|
return when
|
||
|
.reduce(resolvers, function(result, resolver) {
|
||
|
return result ? result : resolver(context, path);
|
||
|
}, undefined);
|
||
|
}
|
||
|
|
||
|
// Run resolvers on one path and return an object with the found path under a
|
||
|
// key of the original path.
|
||
|
//
|
||
|
// Example:
|
||
|
// resolving the path
|
||
|
// 'a/file'
|
||
|
// returns an object
|
||
|
// {'a/file': {path: ['node_modules/a/file'], index: true}}
|
||
|
function resolveOne(resolvers, context, path) {
|
||
|
return reduceResolvers(resolvers, context, path)
|
||
|
.then(function(result) {
|
||
|
result = typeof result === 'string' ? [result] : result;
|
||
|
result = Array.isArray(result) ? {path: result, index: false} : result;
|
||
|
var map = {};
|
||
|
map[path] = result;
|
||
|
return map;
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// Run the resolvers on an array of paths and return an object like resolveOne.
|
||
|
function resolveMany(resolvers, context, paths) {
|
||
|
return when
|
||
|
.map(paths, resolveOne.bind(null, resolvers, context))
|
||
|
.then(function(maps) {
|
||
|
return maps.reduce(function(map, resolvedPaths) {
|
||
|
Object.keys(resolvedPaths).forEach(function(path) {
|
||
|
map[path] = resolvedPaths[path];
|
||
|
});
|
||
|
return map;
|
||
|
}, {});
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// Load a file at fullPath, resolve all of it's imports and report for those.
|
||
|
function resolveFileDeep(helpers, parentCache, source, fullPath) {
|
||
|
var resolvers = helpers.resolvers;
|
||
|
var readFile = helpers.readFile;
|
||
|
|
||
|
var contexts = parentCache.contexts;
|
||
|
var sources = parentCache.sources;
|
||
|
|
||
|
contexts = contexts || {};
|
||
|
var nestResolve = resolveFileDeep.bind(null, helpers, parentCache, null);
|
||
|
var context = path.dirname(fullPath);
|
||
|
readFile = whenNodefn.lift(readFile);
|
||
|
|
||
|
return when
|
||
|
.resolve(source || sources[fullPath] || readFile(fullPath))
|
||
|
// Cast the buffer from the cached input file system to a string.
|
||
|
.then(String)
|
||
|
// Store the source so that the evaluator doesn't need to touch the
|
||
|
// file system.
|
||
|
.then(function(_source) {
|
||
|
sources[fullPath] = _source;
|
||
|
return _source;
|
||
|
})
|
||
|
// Make sure the stylus functions/index.styl source is stored.
|
||
|
.then(partial(ensureFunctionsSource, sources))
|
||
|
// List imports and use its cache. The source file is translated into a
|
||
|
// list of imports. Where the source file came from isn't important for the
|
||
|
// list. The where is added by resolveMany with the context and resolvers.
|
||
|
.then(partialRight(listImports, { cache: parentCache.imports }))
|
||
|
.then(resolveMany.bind(null, resolvers, context))
|
||
|
.then(function(newPaths) {
|
||
|
// Contexts are the full path since multiple could be in the same folder
|
||
|
// but different deps.
|
||
|
contexts[context] = merge(contexts[context] || {}, newPaths);
|
||
|
return when.map(Object.keys(newPaths), function(key) {
|
||
|
var found = newPaths[key] && newPaths[key].path;
|
||
|
if (found) {
|
||
|
return when.map(found, nestResolve);
|
||
|
}
|
||
|
});
|
||
|
})
|
||
|
.then(function() {
|
||
|
return PathCache.create(contexts, sources, parentCache.imports);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// Resolve functions in a promise wrapper to catch any errors from resolving.
|
||
|
var functionsPath =
|
||
|
new when.Promise(function(resolve) {
|
||
|
resolve(require.resolve('stylus/lib/functions/index.styl'));
|
||
|
})
|
||
|
.catch(function() { return ''; });
|
||
|
|
||
|
var functionsSource = functionsPath
|
||
|
.then(readFile)
|
||
|
.catch(function(error) {
|
||
|
// Ignore error if functions/index.styl doesn't exist.
|
||
|
if (error.code !== 'ENOENT') {
|
||
|
throw error;
|
||
|
}
|
||
|
return '';
|
||
|
})
|
||
|
.then(String);
|
||
|
|
||
|
function ensureFunctionsSource(sources, source) {
|
||
|
if (!sources[functionsPath]) {
|
||
|
return functionsSource
|
||
|
.then(function(functionsSource) {
|
||
|
if (functionsSource) {
|
||
|
sources[functionsPath] = functionsSource;
|
||
|
}
|
||
|
})
|
||
|
// Pass through the source given to this function.
|
||
|
.yield(source);
|
||
|
}
|
||
|
// Pass through the source given to this function.
|
||
|
return source;
|
||
|
}
|
||
|
|
||
|
var slice = Array.prototype.slice.call.bind(Array.prototype.slice);
|
||
|
|
||
|
function merge(a, b) {
|
||
|
var key;
|
||
|
for (key in b) {
|
||
|
a[key] = b[key];
|
||
|
}
|
||
|
return a;
|
||
|
}
|
||
|
|
||
|
function partial(fn) {
|
||
|
var args = slice(arguments, 1);
|
||
|
return function() {
|
||
|
return fn.apply(this, args.concat(slice(arguments)));
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function partialRight(fn) {
|
||
|
var args = slice(arguments, 1);
|
||
|
return function() {
|
||
|
return fn.apply(this, slice(arguments).concat(args));
|
||
|
};
|
||
|
}
|
||
|
|
||
|
function normalizePaths(paths) {
|
||
|
for(var i in paths) {
|
||
|
paths[i] = path.normalize(paths[i]);
|
||
|
}
|
||
|
return paths;
|
||
|
}
|