1 | /*
|
2 | * MIT License http://opensource.org/licenses/MIT
|
3 | * Author: Ben Holloway @bholloway
|
4 | */
|
5 | ;
|
6 |
|
7 | var path = require('path'),
|
8 | fs = require('fs'),
|
9 | compose = require('compose-function'),
|
10 | Iterator = require('es6-iterator');
|
11 |
|
12 | var PACKAGE_NAME = require('../package.json').name;
|
13 |
|
14 | var simpleJoin = compose(path.normalize, path.join);
|
15 |
|
16 | /**
|
17 | * The default join function iterates over possible base paths until a suitable join is found.
|
18 | *
|
19 | * The first base path is used as fallback for the case where none of the base paths can locate the actual file.
|
20 | *
|
21 | * @type {function}
|
22 | */
|
23 | exports.defaultJoin = createJoinForPredicate(
|
24 | function predicate(_, uri, base, i, next) {
|
25 | var absolute = simpleJoin(base, uri);
|
26 | return fs.existsSync(absolute) ? absolute : next((i === 0) ? absolute : null);
|
27 | },
|
28 | 'defaultJoin'
|
29 | );
|
30 |
|
31 | /**
|
32 | * Define a join function by a predicate that tests possible base paths from an iterator.
|
33 | *
|
34 | * The `predicate` is of the form:
|
35 | *
|
36 | * ```
|
37 | * function(filename, uri, base, i, next):string|null
|
38 | * ```
|
39 | *
|
40 | * Given the uri and base it should either return:
|
41 | * - an absolute path success
|
42 | * - a call to `next(null)` as failure
|
43 | * - a call to `next(absolute)` where absolute is placeholder and the iterator continues
|
44 | *
|
45 | * The value given to `next(...)` is only used if success does not eventually occur.
|
46 | *
|
47 | * The `file` value is typically unused but useful if you would like to differentiate behaviour.
|
48 | *
|
49 | * You can write a much simpler function than this if you have specific requirements.
|
50 | *
|
51 | * @param {function} predicate A function that tests values
|
52 | * @param {string} [name] Optional name for the resulting join function
|
53 | */
|
54 | function createJoinForPredicate(predicate, name) {
|
55 | /**
|
56 | * A factory for a join function with logging.
|
57 | *
|
58 | * @param {string} filename The current file being processed
|
59 | * @param {{debug:function|boolean,root:string}} options An options hash
|
60 | */
|
61 | function join(filename, options) {
|
62 | var log = createDebugLogger(options.debug);
|
63 |
|
64 | /**
|
65 | * Join function proper.
|
66 | *
|
67 | * For absolute uri only `uri` will be provided. In this case we substitute any `root` given in options.
|
68 | *
|
69 | * @param {string} uri A uri path, relative or absolute
|
70 | * @param {string|Iterator.<string>} [baseOrIteratorOrAbsent] Optional absolute base path or iterator thereof
|
71 | * @return {string} Just the uri where base is empty or the uri appended to the base
|
72 | */
|
73 | return function joinProper(uri, baseOrIteratorOrAbsent) {
|
74 | var iterator =
|
75 | (typeof baseOrIteratorOrAbsent === 'undefined') && new Iterator([options.root ]) ||
|
76 | (typeof baseOrIteratorOrAbsent === 'string' ) && new Iterator([baseOrIteratorOrAbsent]) ||
|
77 | baseOrIteratorOrAbsent;
|
78 |
|
79 | var result = runIterator([]);
|
80 | log(createJoinMsg, [filename, uri, result, result.isFound]);
|
81 |
|
82 | return (typeof result.absolute === 'string') ? result.absolute : uri;
|
83 |
|
84 | function runIterator(accumulator) {
|
85 | var nextItem = iterator.next();
|
86 | var base = !nextItem.done && nextItem.value;
|
87 | if (typeof base === 'string') {
|
88 | var element = predicate(filename, uri, base, accumulator.length, next);
|
89 |
|
90 | if ((typeof element === 'string') && path.isAbsolute(element)) {
|
91 | return Object.assign(
|
92 | accumulator.concat(base),
|
93 | {isFound: true, absolute: element}
|
94 | );
|
95 | } else if (Array.isArray(element)) {
|
96 | return element;
|
97 | } else {
|
98 | throw new Error('predicate must return an absolute path or the result of calling next()');
|
99 | }
|
100 | } else {
|
101 | return accumulator;
|
102 | }
|
103 |
|
104 | function next(fallback) {
|
105 | return runIterator(Object.assign(
|
106 | accumulator.concat(base),
|
107 | (typeof fallback === 'string') && {absolute: fallback}
|
108 | ));
|
109 | }
|
110 | }
|
111 | };
|
112 | }
|
113 |
|
114 | function toString() {
|
115 | return '[Function: ' + name + ']';
|
116 | }
|
117 |
|
118 | return Object.assign(join, name && {
|
119 | valueOf : toString,
|
120 | toString: toString
|
121 | });
|
122 | }
|
123 |
|
124 | exports.createJoinForPredicate = createJoinForPredicate;
|
125 |
|
126 | /**
|
127 | * Format a debug message.
|
128 | *
|
129 | * @param {string} file The file being processed by webpack
|
130 | * @param {string} uri A uri path, relative or absolute
|
131 | * @param {Array.<string>} bases Absolute base paths up to and including the found one
|
132 | * @param {boolean} isFound Indicates the last base was correct
|
133 | * @return {string} Formatted message
|
134 | */
|
135 | function createJoinMsg(file, uri, bases, isFound) {
|
136 | return [PACKAGE_NAME + ': ' + pathToString(file) + ': ' + uri]
|
137 | .concat(bases.map(pathToString).filter(Boolean))
|
138 | .concat(isFound ? 'FOUND' : 'NOT FOUND')
|
139 | .join('\n ');
|
140 |
|
141 | /**
|
142 | * If given path is within `process.cwd()` then show relative posix path, otherwise show absolute posix path.
|
143 | *
|
144 | * @param {string} absolute An absolute path
|
145 | * @return {string} A relative or absolute path
|
146 | */
|
147 | function pathToString(absolute) {
|
148 | if (!absolute) {
|
149 | return null;
|
150 | } else {
|
151 | var relative = path.relative(process.cwd(), absolute)
|
152 | .split(path.sep);
|
153 |
|
154 | return ((relative[0] === '..') ? absolute.split(path.sep) : ['.'].concat(relative).filter(Boolean))
|
155 | .join('/');
|
156 | }
|
157 | }
|
158 | }
|
159 |
|
160 | exports.createJoinMsg = createJoinMsg;
|
161 |
|
162 | /**
|
163 | * A factory for a log function predicated on the given debug parameter.
|
164 | *
|
165 | * The logging function created accepts a function that formats a message and parameters that the function utilises.
|
166 | * Presuming the message function may be expensive we only call it if logging is enabled.
|
167 | *
|
168 | * The log messages are de-duplicated based on the parameters, so it is assumed they are simple types that stringify
|
169 | * well.
|
170 | *
|
171 | * @param {function|boolean} debug A boolean or debug function
|
172 | * @return {function(function, array)} A logging function possibly degenerate
|
173 | */
|
174 | function createDebugLogger(debug) {
|
175 | var log = !!debug && ((typeof debug === 'function') ? debug : console.log);
|
176 | var cache = {};
|
177 | return log ? actuallyLog : noop;
|
178 |
|
179 | function noop() {}
|
180 |
|
181 | function actuallyLog(msgFn, params) {
|
182 | var key = JSON.stringify(params);
|
183 | if (!cache[key]) {
|
184 | cache[key] = true;
|
185 | log(msgFn.apply(null, params));
|
186 | }
|
187 | }
|
188 | }
|
189 |
|
190 | exports.createDebugLogger = createDebugLogger;
|