-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
209 lines (178 loc) · 5.58 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
'use strict';
var empty = require('is-empty')
, path = require('path')
, fs = require('fs');
/**
* Expose small fabrication helper.
*
* Possible options:
*
* - source: {String} Absolute path to be used to resolve file paths.
* - recursive: {Boolean} Should file paths be recursively discovered.
* - name: {String} Force a name for a given constructor.
*
* @param {Mixed} stack String, array or object that contains constructible entities
* @param {Object} options Optional options.
* @returns {Array} collection of constructors.
* @api public
*/
function fabricator(stack, options) {
options = options || {};
//
// Empty strings, arrays or objects should not be processed, return early.
//
if (empty(stack)) return [];
switch (is(stack)) {
case 'string':
stack = read(stack, options);
break;
case 'object':
stack = Object.keys(stack).reduce(iterator(read, stack, options), []);
break;
case 'array':
stack = stack.reduce(iterator(read, null, options), []);
break;
default:
if ('function' !== typeof stack) {
throw new Error('Unsupported type, cannot fabricate an: '+ is(stack));
}
stack = [init(stack, undefined, options)];
}
return (stack || []).filter(Boolean);
}
/**
* Read directory and initialize JavaScript files.
*
* @param {String} filepath Full directory path.
* @param {Object} options Additional configuration.
* @return {Array} collection of constructor
* @api private
*/
function read(filepath, options) {
if ('string' !== is(filepath)) return fabricator(filepath, options);
if (options.source) filepath = path.resolve(options.source, filepath);
//
// Check if the provided string is a JS file or when recursion is not allowed.
//
if (js(filepath) || options.recursive === false) return [
init(filepath, path.basename(filepath, '.js'), options)
];
//
// Read the directory, only process files.
//
if (!fs.existsSync(filepath)) return false;
return fs.readdirSync(filepath).map(function locate(file) {
file = path.resolve(filepath, file);
var stat = fs.statSync(file);
if (stat.isDirectory() && fs.existsSync(path.join(file, 'index.js'))) {
//
// Use the directory name instead of `index` for name as it probably has
// more meaning then just `index` as a name.
//
return init(path.join(file, 'index.js'), path.basename(file, '.js'), options);
}
//
// Only allow JS files, init determines if it is a constructible instance.
//
if (!stat.isFile() || !js(file)) return;
return init(file, path.basename(file, '.js'), options);
});
}
/**
* Return iterator for array or object.
*
* @param {Function} traverse Recursive iterator, called on directories.
* @param {Object} obj Original object, if set values are fetched by entity.
* @param {Object} options Configuration.
* @return {Function} iterator
* @api private
*/
function iterator(traverse, obj, options) {
return function reduce(stack, entity) {
var base = obj ? obj[entity] : entity
, name = options.name || entity;
//
// Fabricated objects should provide each constructor with the name
// of its property on the original object.
//
if (obj) options.name = entity;
//
// Run the functions, traverse will handle init.
//
if (js(base)) {
return stack.concat(init(
base,
'string' === is(name) ? name : '',
options
));
}
//
// When we've been supplied with an array as base assume we want to keep it
// as array and do not want it to be merged.
//
if (Array.isArray(base)) {
options.name = name; // Force the name of the entry for all items in array.
stack.push(traverse(base, options));
return stack;
}
return stack.concat(traverse(base, options));
};
}
/**
* Make sure only valid JavaScript files are used as source. Ignore other files,
* like .log files. Also allow constructors.
*
* @param {String|Function} file Path or constructor function.
* @returns {Boolean} allow entity to be used or not.
* @api private
*/
function js(file) {
var type = is(file);
return 'function' === type
|| 'string' === type && path.extname(file) === '.js';
}
/**
* A better alternative to `typeof` checks by trying to figure out the root
* class of things. This eliminates the needs for Array.is checks when the type
* is an object etc.
*
* @param {Mixed} obj Unknown thing we need to know.
* @returns {String}
* @api private
*/
function is(obj) {
return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
}
/**
* It's not required to supply resolve with instances, we can just
* automatically require them if they are using the:
*
* module.exports = function Constructor() {};
*
* @param {String} constructor
* @param {String} name Optional identifier for the constructor.
* @returns {Object} initialized object
* @api private
*/
function init(constructor, name, options) {
constructor = ('string' === is(constructor)) ? require(constructor) : constructor;
//
// We really want to have a function/class here. Make sure that we can
// construct it using `new constructor`
//
if (!constructor.prototype) return;
name = constructor.prototype.name || name || constructor.name;
if (options.name) name = options.name;
//
// Sets the lowercase name on the prototype if required.
//
if ('name' in constructor.prototype) {
name = name.toString();
constructor.prototype.name = name[0].toLowerCase() + name.slice(1);
}
return constructor;
}
//
// Expose the module.
//
module.exports = fabricator;