You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

191 lines
6.1 KiB

4 years ago
/*
* MIT License http://opensource.org/licenses/MIT
* Author: Ben Holloway @bholloway
*/
'use strict';
var path = require('path'),
fs = require('fs'),
compose = require('compose-function'),
Iterator = require('es6-iterator');
var PACKAGE_NAME = require('../package.json').name;
var simpleJoin = compose(path.normalize, path.join);
/**
* The default join function iterates over possible base paths until a suitable join is found.
*
* The first base path is used as fallback for the case where none of the base paths can locate the actual file.
*
* @type {function}
*/
exports.defaultJoin = createJoinForPredicate(
function predicate(_, uri, base, i, next) {
var absolute = simpleJoin(base, uri);
return fs.existsSync(absolute) ? absolute : next((i === 0) ? absolute : null);
},
'defaultJoin'
);
/**
* Define a join function by a predicate that tests possible base paths from an iterator.
*
* The `predicate` is of the form:
*
* ```
* function(filename, uri, base, i, next):string|null
* ```
*
* Given the uri and base it should either return:
* - an absolute path success
* - a call to `next(null)` as failure
* - a call to `next(absolute)` where absolute is placeholder and the iterator continues
*
* The value given to `next(...)` is only used if success does not eventually occur.
*
* The `file` value is typically unused but useful if you would like to differentiate behaviour.
*
* You can write a much simpler function than this if you have specific requirements.
*
* @param {function} predicate A function that tests values
* @param {string} [name] Optional name for the resulting join function
*/
function createJoinForPredicate(predicate, name) {
/**
* A factory for a join function with logging.
*
* @param {string} filename The current file being processed
* @param {{debug:function|boolean,root:string}} options An options hash
*/
function join(filename, options) {
var log = createDebugLogger(options.debug);
/**
* Join function proper.
*
* For absolute uri only `uri` will be provided. In this case we substitute any `root` given in options.
*
* @param {string} uri A uri path, relative or absolute
* @param {string|Iterator.<string>} [baseOrIteratorOrAbsent] Optional absolute base path or iterator thereof
* @return {string} Just the uri where base is empty or the uri appended to the base
*/
return function joinProper(uri, baseOrIteratorOrAbsent) {
var iterator =
(typeof baseOrIteratorOrAbsent === 'undefined') && new Iterator([options.root ]) ||
(typeof baseOrIteratorOrAbsent === 'string' ) && new Iterator([baseOrIteratorOrAbsent]) ||
baseOrIteratorOrAbsent;
var result = runIterator([]);
log(createJoinMsg, [filename, uri, result, result.isFound]);
return (typeof result.absolute === 'string') ? result.absolute : uri;
function runIterator(accumulator) {
var nextItem = iterator.next();
var base = !nextItem.done && nextItem.value;
if (typeof base === 'string') {
var element = predicate(filename, uri, base, accumulator.length, next);
if ((typeof element === 'string') && path.isAbsolute(element)) {
return Object.assign(
accumulator.concat(base),
{isFound: true, absolute: element}
);
} else if (Array.isArray(element)) {
return element;
} else {
throw new Error('predicate must return an absolute path or the result of calling next()');
}
} else {
return accumulator;
}
function next(fallback) {
return runIterator(Object.assign(
accumulator.concat(base),
(typeof fallback === 'string') && {absolute: fallback}
));
}
}
};
}
function toString() {
return '[Function: ' + name + ']';
}
return Object.assign(join, name && {
valueOf : toString,
toString: toString
});
}
exports.createJoinForPredicate = createJoinForPredicate;
/**
* Format a debug message.
*
* @param {string} file The file being processed by webpack
* @param {string} uri A uri path, relative or absolute
* @param {Array.<string>} bases Absolute base paths up to and including the found one
* @param {boolean} isFound Indicates the last base was correct
* @return {string} Formatted message
*/
function createJoinMsg(file, uri, bases, isFound) {
return [PACKAGE_NAME + ': ' + pathToString(file) + ': ' + uri]
.concat(bases.map(pathToString).filter(Boolean))
.concat(isFound ? 'FOUND' : 'NOT FOUND')
.join('\n ');
/**
* If given path is within `process.cwd()` then show relative posix path, otherwise show absolute posix path.
*
* @param {string} absolute An absolute path
* @return {string} A relative or absolute path
*/
function pathToString(absolute) {
if (!absolute) {
return null;
} else {
var relative = path.relative(process.cwd(), absolute)
.split(path.sep);
return ((relative[0] === '..') ? absolute.split(path.sep) : ['.'].concat(relative).filter(Boolean))
.join('/');
}
}
}
exports.createJoinMsg = createJoinMsg;
/**
* A factory for a log function predicated on the given debug parameter.
*
* The logging function created accepts a function that formats a message and parameters that the function utilises.
* Presuming the message function may be expensive we only call it if logging is enabled.
*
* The log messages are de-duplicated based on the parameters, so it is assumed they are simple types that stringify
* well.
*
* @param {function|boolean} debug A boolean or debug function
* @return {function(function, array)} A logging function possibly degenerate
*/
function createDebugLogger(debug) {
var log = !!debug && ((typeof debug === 'function') ? debug : console.log);
var cache = {};
return log ? actuallyLog : noop;
function noop() {}
function actuallyLog(msgFn, params) {
var key = JSON.stringify(params);
if (!cache[key]) {
cache[key] = true;
log(msgFn.apply(null, params));
}
}
}
exports.createDebugLogger = createDebugLogger;