-
Notifications
You must be signed in to change notification settings - Fork 5
/
modules.js
314 lines (279 loc) · 9.08 KB
/
modules.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
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
/**
* Functions related to the loading and manipulation of CommonJS Modules in
* Kanso apps.
*
* @module
*/
var utils = require('./utils'),
async = require('async'),
path = require('path'),
fs = require('fs'),
evals = require('vm'),
Script = evals.Script || evals.NodeScript;
// As modules are added, their original file paths are added to this object
// eg: {'lib': {'mymodule': '/home/user/project/lib/mymodule.js'}, ...}
//exports.originalPaths = {};
// REPLACED BY doc._module_paths - so it works across package processors
/**
* Add the module source to the document in the correct location for requiring
* server-side, then add the path to the _modules property for use by the
* modules plugin postprocessor (when creating the kanso.js attachment)
*
* The filename parameter is optional, and improves error reporting with
* line numbers etc.
*
* Returns the updated document.
*
* @param {Object} doc
* @param {String} path
* @param {String} src
* @param {String} filename
* @returns {Object}
*/
exports.add = function (doc, path, src, /*optional*/filename) {
if (!doc._module_paths) {
doc._module_paths = {};
}
if (filename) {
// Store location of module file for later reference should a
// syntax error occur when requiring it.
utils.setPropertyPath(doc._module_paths, path, filename);
}
var curr = utils.getPropertyPath(doc, path, true);
if (curr !== undefined) {
var paths = utils.getPropertyPaths(path, curr);
throw new Error(
'Adding ' + path + ' would overwrite:\n' +
' ' + paths.join('\n ') + '\n' +
'\n' +
'This error often occurs because there is a module with the\n' +
'same name as a directory containing modules. There is no way\n' +
'to map this structure in the design doc.\n'
);
}
utils.setPropertyPath(doc, path, src);
if (!doc._modules) {
doc._modules = {};
}
doc._modules[path] = null;
return doc;
};
/**
* Wraps module source code with useful comments and module cache boilerplate
* code.
*
* @param {Object} doc
* @param {String} path
* @param {String} src
* @returns {Object}
*/
exports.wrap = function (path, src) {
return '/********** ' + path + ' **********/\n\n' +
'kanso.moduleCache["' + path.replace('"', '\\"') + '"] = ' +
'{load: (function (module, exports, require) {\n\n' + src + '\n\n})};' +
'\n\n';
};
/**
* Searchs a path for commonjs modules, adding them to the document.
*
* @param {String} pkgdir - path to the source package
* @param {String} p - path to a module file or directory of modules
* @param {Object} doc - the document to extend
* @param {Function} callback
*/
exports.addPath = function (pkgdir, p, doc, callback) {
p = utils.abspath(p, pkgdir);
exports.find(p, function (err, files) {
if (err) {
return callback(err);
}
async.forEach(files, function (f, cb) {
exports.addFile(pkgdir, f, doc, cb);
}, callback);
});
};
/**
* Loads a module file and adds its contents to the document
*
* @param {String} pkgdir
* @param {String} p
* @param {Object} doc
* @param {Function} callback
*/
exports.addFile = function (pkgdir, p, doc, callback) {
fs.readFile(p, function (err, content) {
if (err) {
return callback(err);
}
var rel = utils.relpath(p, pkgdir);
var module_path = rel.replace(/\.js$/, '');
var src = content.toString();
exports.add(doc, module_path, src, p);
callback()
});
};
/**
* Find all modules below or at a given path, recursing through subdirectories
*
* @param {String} p - the path to search
* @param {Function} callback
*/
exports.find = async.memoize(function (p, callback) {
utils.find(p, exports.filenameFilter(p), callback);
});
/**
* Creates a filter used when searching for module files. This function tests
* for a .js extension and omits hidden dot-preceeded filenames.
*
* @param {String} p - the path to the directory being searched
* @returns {Function}
*/
exports.filenameFilter = function (p) {
return function (f) {
if (f === p) {
return true;
}
var relpath = utils.relpath(f, p);
// should not start with a '.'
if (/^\.[^\/]?/.test(relpath)) {
return false;
}
// should not contain a file or folder starting with a '.'
if (/\/\./.test(relpath)) {
return false;
}
// should have a .js extension
if (!/\.js$/.test(f)) {
return false;
}
return true;
};
};
/**
* When traversing the document looking for a module, but an object is found
* this function will list all sub-paths available under that object - used
* to report possible solutions to require path errors.
*/
exports.getNames = function (obj) {
var names = [];
for (var k in obj) {
if (typeof obj[k] === 'object') {
names.concat(exports.getNames(obj[k]).map(function (n) {
return k + '/' + n;
}));
}
else {
names.push(k);
}
}
return names;
};
/**
* Loads a commonjs module from the loaded design document, returning
* the exported properties. The current_dir and target parameters are not the
* path of the module on the filesystem, but rather the path of the module
* within couchdb, root ('/') being the design doc itself.
*
* @param {Object} module_cache
* @param {Object} doc
* @param {String} current_dir
* @param {String} target
* @param {Object} context - optional, extends sandbox object
*/
exports.require = function (module_cache, doc, current, target, context) {
var current_dir = path.dirname(current);
if (target.charAt(0) !== '.') {
current_dir = '/';
}
var p = path.normalize(path.join(current_dir, target));
if (module_cache[p]) {
return module_cache[p];
}
var nodes = p.split('/').slice(1);
var content = nodes.reduce(function (a, x) {
if (a[x] === undefined) {
throw new Error(
//'Could not require module: ' + target + ' ' +
'Could not require module: ' + p.replace(/^\//, '') + ' ' +
'(from: ' + current.replace(/^\//, '') + ') - ' +
'Module not found'
);
}
a = a[x];
return a;
}, doc);
if (doc._module_paths) {
var filename = utils.getPropertyPath(
doc._module_paths, p.substr(1), true
);
}
// if you attempt to require
if (typeof content === 'object') {
var names = exports.getNames(content);
throw new Error(
//'Could not require module: ' + target + ' ' +
'Could not require module: ' + p.replace(/^\//, '') + ' ' +
'(from: ' + current.replace(/^\//, '') + ')\n\n' +
'It\'s not possible to require directories of modules in CouchDB,\n' +
'you have to specify one of the following sub-modules directly:\n' +
names.map(function (f) {
return ' ' + p.replace(/^\//, '') + '/' + f;
}).join('\n') + '\n'
);
}
var module = {
id: p.substr(1),
exports: {},
// TODO: this property is not provided by couchdb, but is by node:
//filename: filename,
// TODO: this property is not provided by couchdb, but is by node:
//require: async.apply(exports.require, module_cache, doc, p)
// TODO: module properties provided by couchdb, but not by kanso
// * current
// * parent
};
var sandbox = {
module: module,
exports: module.exports,
//require: module.require,
require: async.apply(exports.require, module_cache, doc, p),
log: function () {
console.log.apply(console, arguments);
}
};
// copy context into sandbox
if (context) {
Object.keys(context).forEach(function (k) {
sandbox[k] = context[k];
});
}
// Create a placeholder for this module's exports so circular requires
// are possible. TODO: node.js uses a loaded = false attribute on the
// cached module object to mark this as a placeholder.
module_cache[p] = {};
try {
var s = new Script(content, filename).runInNewContext(sandbox);
}
catch (e) {
if (e instanceof SyntaxError && filename && !e.caught) {
e.caught = true;
// gives a better syntax error than runInNewContext
// with filename and line number
try {
require(filename);
}
catch (e2) {
e.message = 'Error loading: ' + filename + '\n' + e.message;
e.got_filename = true;
throw e;
}
}
if (!e.got_filename && filename) {
e.message = 'Error loading: ' + filename + '\n' + e.message;
e.got_filename = true;
}
throw e;
}
module_cache[p] = sandbox.module.exports;
return module_cache[p];
};