1 | makeInstaller = function (options) {
|
2 | ;
|
3 |
|
4 | options = options || {};
|
5 |
|
6 | // These file extensions will be appended to required module identifiers
|
7 | // if they do not exactly match an installed module.
|
8 | var defaultExtensions = options.extensions || [".js", ".json"];
|
9 |
|
10 | // If defined, the options.onInstall function will be called any time
|
11 | // new modules are installed.
|
12 | var onInstall = options.onInstall;
|
13 |
|
14 | // If defined, each module-specific require function will be passed to
|
15 | // this function, along with the module object of the parent module, and
|
16 | // the result will be used in place of the original require function.
|
17 | var wrapRequire = options.wrapRequire;
|
18 |
|
19 | // If defined, the options.override function will be called before
|
20 | // looking up any top-level package identifiers in node_modules
|
21 | // directories. It can either return a string to provide an alternate
|
22 | // package identifier, or a non-string value to prevent the lookup from
|
23 | // proceeding.
|
24 | var override = options.override;
|
25 |
|
26 | // If defined, the options.fallback function will be called when no
|
27 | // installed module is found for a required module identifier. Often
|
28 | // options.fallback will be implemented in terms of the native Node
|
29 | // require function, which has the ability to load binary modules.
|
30 | var fallback = options.fallback;
|
31 |
|
32 | // List of fields to look for in package.json files to determine the
|
33 | // main entry module of the package. The first field listed here whose
|
34 | // value is a string will be used to resolve the entry module.
|
35 | var mainFields = options.mainFields ||
|
36 | // If options.mainFields is absent and options.browser is truthy,
|
37 | // package resolution will prefer the "browser" field of package.json
|
38 | // files to the "main" field. Note that this only supports
|
39 | // string-valued "browser" fields for now, though in the future it
|
40 | // might make sense to support the object version, a la browserify.
|
41 | (options.browser ? ["browser", "main"] : ["main"]);
|
42 |
|
43 | // Called below as hasOwn.call(obj, key).
|
44 | var hasOwn = {}.hasOwnProperty;
|
45 |
|
46 | // Cache for looking up File objects given absolute module identifiers.
|
47 | // Invariants:
|
48 | // filesByModuleId[module.id] === fileAppendId(root, module.id)
|
49 | // filesByModuleId[module.id].module === module
|
50 | var filesByModuleId = {};
|
51 |
|
52 | // The file object representing the root directory of the installed
|
53 | // module tree.
|
54 | var root = new File("/", new File("/.."));
|
55 | var rootRequire = makeRequire(root);
|
56 |
|
57 | // Merges the given tree of directories and module factory functions
|
58 | // into the tree of installed modules and returns a require function
|
59 | // that behaves as if called from a module in the root directory.
|
60 | function install(tree, options) {
|
61 | if (isObject(tree)) {
|
62 | fileMergeContents(root, tree, options);
|
63 | if (isFunction(onInstall)) {
|
64 | onInstall(rootRequire);
|
65 | }
|
66 | }
|
67 | return rootRequire;
|
68 | }
|
69 |
|
70 | // Replace this function to enable Module.prototype.prefetch.
|
71 | install.fetch = function (ids) {
|
72 | throw new Error("fetch not implemented");
|
73 | };
|
74 |
|
75 | // This constructor will be used to instantiate the module objects
|
76 | // passed to module factory functions (i.e. the third argument after
|
77 | // require and exports), and is exposed as install.Module in case the
|
78 | // caller of makeInstaller wishes to modify Module.prototype.
|
79 | function Module(id) {
|
80 | this.id = id;
|
81 |
|
82 | // The Node implementation of module.children unfortunately includes
|
83 | // only those child modules that were imported for the first time by
|
84 | // this parent module (i.e., child.parent === this).
|
85 | this.children = [];
|
86 |
|
87 | // This object is an install.js extension that includes all child
|
88 | // modules imported by this module, even if this module is not the
|
89 | // first to import them.
|
90 | this.childrenById = {};
|
91 | }
|
92 |
|
93 | Module.prototype.resolve = function (id) {
|
94 | return this.require.resolve(id);
|
95 | };
|
96 |
|
97 | var resolvedPromise;
|
98 | var lastPrefetchPromise;
|
99 |
|
100 | Module.prototype.prefetch = function (id) {
|
101 | var module = this;
|
102 | var parentFile = getOwn(filesByModuleId, module.id);
|
103 | var missing; // Initialized to {} only if necessary.
|
104 |
|
105 | resolvedPromise = resolvedPromise || Promise.resolve();
|
106 | lastPrefetchPromise = lastPrefetchPromise || resolvedPromise;
|
107 | var previousPromise = lastPrefetchPromise;
|
108 |
|
109 | function walk(module) {
|
110 | var file = getOwn(filesByModuleId, module.id);
|
111 | if (fileIsDynamic(file) && ! file.pending) {
|
112 | file.pending = true;
|
113 | missing = missing || {};
|
114 |
|
115 | // These are the data that will be exposed to the install.fetch
|
116 | // callback, so it's worth documenting each item with a comment.
|
117 | missing[module.id] = {
|
118 | // The CommonJS module object that will be exposed to this
|
119 | // dynamic module when it is evaluated. Note that install.fetch
|
120 | // could decide to populate module.exports directly, instead of
|
121 | // fetching anything. In that case, install.fetch should omit
|
122 | // this module from the tree that it produces.
|
123 | module: file.module,
|
124 | // List of module identifier strings imported by this module.
|
125 | // Note that the missing object already contains all available
|
126 | // dependencies (including transitive dependencies), so
|
127 | // install.fetch should not need to traverse these dependencies
|
128 | // in most cases; however, they may be useful for other reasons.
|
129 | // Though the strings are unique, note that two different
|
130 | // strings could resolve to the same module.
|
131 | deps: Object.keys(file.deps),
|
132 | // The options (if any) that were passed as the second argument
|
133 | // to the install(tree, options) function when this stub was
|
134 | // first registered. Typically contains options.extensions, but
|
135 | // could contain any information appropriate for the entire tree
|
136 | // as originally installed. These options will be automatically
|
137 | // inherited by the newly fetched modules, so install.fetch
|
138 | // should not need to modify them.
|
139 | options: file.options,
|
140 | // Any stub data included in the array notation from the
|
141 | // original entry for this dynamic module. Typically contains
|
142 | // "main" and/or "browser" fields for package.json files, and is
|
143 | // otherwise undefined.
|
144 | stub: file.stub
|
145 | };
|
146 |
|
147 | each(file.deps, function (parentId, id) {
|
148 | fileResolve(file, id);
|
149 | });
|
150 |
|
151 | each(module.childrenById, walk);
|
152 | }
|
153 | }
|
154 |
|
155 | return lastPrefetchPromise = resolvedPromise.then(function () {
|
156 | var absChildId = module.resolve(id);
|
157 | each(module.childrenById, walk);
|
158 |
|
159 | return Promise.resolve(
|
160 | // The install.fetch function takes an object mapping missing
|
161 | // dynamic module identifiers to options objects, and should
|
162 | // return a Promise that resolves to a module tree that can be
|
163 | // installed. As an optimization, if there were no missing dynamic
|
164 | // modules, then we can skip calling install.fetch entirely.
|
165 | missing && install.fetch(missing)
|
166 |
|
167 | ).then(function (tree) {
|
168 | function both() {
|
169 | if (tree) install(tree);
|
170 | return absChildId;
|
171 | }
|
172 |
|
173 | // Although we want multiple install.fetch calls to run in
|
174 | // parallel, it is important that the promises returned by
|
175 | // module.prefetch are resolved in the same order as the original
|
176 | // calls to module.prefetch, because previous fetches may include
|
177 | // modules assumed to exist by more recent module.prefetch calls.
|
178 | // Whether previousPromise was resolved or rejected, carry on with
|
179 | // the installation regardless.
|
180 | return previousPromise.then(both, both);
|
181 | });
|
182 | });
|
183 | };
|
184 |
|
185 | install.Module = Module;
|
186 |
|
187 | function getOwn(obj, key) {
|
188 | return hasOwn.call(obj, key) && obj[key];
|
189 | }
|
190 |
|
191 | function isObject(value) {
|
192 | return typeof value === "object" && value !== null;
|
193 | }
|
194 |
|
195 | function isFunction(value) {
|
196 | return typeof value === "function";
|
197 | }
|
198 |
|
199 | function isString(value) {
|
200 | return typeof value === "string";
|
201 | }
|
202 |
|
203 | function makeMissingError(id) {
|
204 | return new Error("Cannot find module '" + id + "'");
|
205 | }
|
206 |
|
207 | function makeRequire(file) {
|
208 | function require(id) {
|
209 | var result = fileResolve(file, id);
|
210 | if (result) {
|
211 | return fileEvaluate(result, file.module);
|
212 | }
|
213 |
|
214 | var error = makeMissingError(id);
|
215 |
|
216 | if (isFunction(fallback)) {
|
217 | return fallback(
|
218 | id, // The missing module identifier.
|
219 | file.module.id, // The path of the requiring file.
|
220 | error // The error we would have thrown.
|
221 | );
|
222 | }
|
223 |
|
224 | throw error;
|
225 | }
|
226 |
|
227 | if (isFunction(wrapRequire)) {
|
228 | require = wrapRequire(require, file.module);
|
229 | }
|
230 |
|
231 | require.extensions = fileGetExtensions(file).slice(0);
|
232 |
|
233 | require.resolve = function (id) {
|
234 | var f = fileResolve(file, id);
|
235 | if (f) return f.module.id;
|
236 | var error = makeMissingError(id);
|
237 | if (fallback && isFunction(fallback.resolve)) {
|
238 | return fallback.resolve(id, file.module.id, error);
|
239 | }
|
240 | throw error;
|
241 | };
|
242 |
|
243 | return require;
|
244 | }
|
245 |
|
246 | // File objects represent either directories or modules that have been
|
247 | // installed. When a `File` respresents a directory, its `.contents`
|
248 | // property is an object containing the names of the files (or
|
249 | // directories) that it contains. When a `File` represents a module, its
|
250 | // `.contents` property is a function that can be invoked with the
|
251 | // appropriate `(require, exports, module)` arguments to evaluate the
|
252 | // module. If the `.contents` property is a string, that string will be
|
253 | // resolved as a module identifier, and the exports of the resulting
|
254 | // module will provide the exports of the original file. The `.parent`
|
255 | // property of a File is either a directory `File` or `null`. Note that
|
256 | // a child may claim another `File` as its parent even if the parent
|
257 | // does not have an entry for that child in its `.contents` object.
|
258 | // This is important for implementing anonymous files, and preventing
|
259 | // child modules from using `../relative/identifier` syntax to examine
|
260 | // unrelated modules.
|
261 | function File(moduleId, parent) {
|
262 | var file = this;
|
263 |
|
264 | // Link to the parent file.
|
265 | file.parent = parent = parent || null;
|
266 |
|
267 | // The module object for this File, which will eventually boast an
|
268 | // .exports property when/if the file is evaluated.
|
269 | file.module = new Module(moduleId);
|
270 | filesByModuleId[moduleId] = file;
|
271 |
|
272 | // The .contents of the file can be either (1) an object, if the file
|
273 | // represents a directory containing other files; (2) a factory
|
274 | // function, if the file represents a module that can be imported; (3)
|
275 | // a string, if the file is an alias for another file; or (4) null, if
|
276 | // the file's contents are not (yet) available.
|
277 | file.contents = null;
|
278 |
|
279 | // Set of module identifiers imported by this module. Note that this
|
280 | // set is not necessarily complete, so don't rely on it unless you
|
281 | // know what you're doing.
|
282 | file.deps = {};
|
283 | }
|
284 |
|
285 | function fileEvaluate(file, parentModule) {
|
286 | var module = file.module;
|
287 | if (! hasOwn.call(module, "exports")) {
|
288 | var contents = file.contents;
|
289 | if (! contents) {
|
290 | // If this file was installed with array notation, and the array
|
291 | // contained one or more objects but no functions, then the combined
|
292 | // properties of the objects are treated as a temporary stub for
|
293 | // file.module.exports. This is particularly important for partial
|
294 | // package.json modules, so that the resolution logic can know the
|
295 | // value of the "main" and/or "browser" fields, at least, even if
|
296 | // the rest of the package.json file is not (yet) available.
|
297 | if (file.stub) {
|
298 | return file.stub;
|
299 | }
|
300 |
|
301 | throw makeMissingError(module.id);
|
302 | }
|
303 |
|
304 | if (parentModule) {
|
305 | module.parent = parentModule;
|
306 | var children = parentModule.children;
|
307 | if (Array.isArray(children)) {
|
308 | children.push(module);
|
309 | }
|
310 | }
|
311 |
|
312 | // If a Module.prototype.useNode method is defined, give it a chance
|
313 | // to define module.exports based on module.id using Node.
|
314 | if (! isFunction(module.useNode) ||
|
315 | ! module.useNode()) {
|
316 | contents(
|
317 | module.require = module.require || makeRequire(file),
|
318 | // If the file had a .stub, reuse the same object for exports.
|
319 | module.exports = file.stub || {},
|
320 | module,
|
321 | file.module.id,
|
322 | file.parent.module.id
|
323 | );
|
324 | }
|
325 |
|
326 | module.loaded = true;
|
327 | }
|
328 |
|
329 | if (isFunction(module.runModuleSetters)) {
|
330 | module.runModuleSetters();
|
331 | }
|
332 |
|
333 | return module.exports;
|
334 | }
|
335 |
|
336 | function fileIsDirectory(file) {
|
337 | return file && isObject(file.contents);
|
338 | }
|
339 |
|
340 | function fileIsDynamic(file) {
|
341 | return file && file.contents === null;
|
342 | }
|
343 |
|
344 | function fileMergeContents(file, contents, options) {
|
345 | if (Array.isArray(contents)) {
|
346 | contents.forEach(function (item) {
|
347 | if (isString(item)) {
|
348 | file.deps[item] = file.module.id;
|
349 | } else if (isFunction(item)) {
|
350 | contents = item;
|
351 | } else if (isObject(item)) {
|
352 | file.stub = file.stub || {};
|
353 | each(item, function (value, key) {
|
354 | file.stub[key] = value;
|
355 | });
|
356 | }
|
357 | });
|
358 |
|
359 | if (! isFunction(contents)) {
|
360 | // If the array did not contain a function, merge nothing.
|
361 | contents = null;
|
362 | }
|
363 |
|
364 | } else if (! isFunction(contents) &&
|
365 | ! isString(contents) &&
|
366 | ! isObject(contents)) {
|
367 | // If contents is neither an array nor a function nor a string nor
|
368 | // an object, just give up and merge nothing.
|
369 | contents = null;
|
370 | }
|
371 |
|
372 | if (contents) {
|
373 | file.contents = file.contents || (isObject(contents) ? {} : contents);
|
374 | if (isObject(contents) && fileIsDirectory(file)) {
|
375 | each(contents, function (value, key) {
|
376 | if (key === "..") {
|
377 | child = file.parent;
|
378 |
|
379 | } else {
|
380 | var child = getOwn(file.contents, key);
|
381 |
|
382 | if (! child) {
|
383 | child = file.contents[key] = new File(
|
384 | file.module.id.replace(/\/*$/, "/") + key,
|
385 | file
|
386 | );
|
387 |
|
388 | child.options = options;
|
389 | }
|
390 | }
|
391 |
|
392 | fileMergeContents(child, value, options);
|
393 | });
|
394 | }
|
395 | }
|
396 | }
|
397 |
|
398 | function each(obj, callback, context) {
|
399 | Object.keys(obj).forEach(function (key) {
|
400 | callback.call(this, obj[key], key);
|
401 | }, context);
|
402 | }
|
403 |
|
404 | function fileGetExtensions(file) {
|
405 | return file.options
|
406 | && file.options.extensions
|
407 | || defaultExtensions;
|
408 | }
|
409 |
|
410 | function fileAppendIdPart(file, part, extensions) {
|
411 | // Always append relative to a directory.
|
412 | while (file && ! fileIsDirectory(file)) {
|
413 | file = file.parent;
|
414 | }
|
415 |
|
416 | if (! file || ! part || part === ".") {
|
417 | return file;
|
418 | }
|
419 |
|
420 | if (part === "..") {
|
421 | return file.parent;
|
422 | }
|
423 |
|
424 | var exactChild = getOwn(file.contents, part);
|
425 |
|
426 | // Only consider multiple file extensions if this part is the last
|
427 | // part of a module identifier and not equal to `.` or `..`, and there
|
428 | // was no exact match or the exact match was a directory.
|
429 | if (extensions && (! exactChild || fileIsDirectory(exactChild))) {
|
430 | for (var e = 0; e < extensions.length; ++e) {
|
431 | var child = getOwn(file.contents, part + extensions[e]);
|
432 | if (child && ! fileIsDirectory(child)) {
|
433 | return child;
|
434 | }
|
435 | }
|
436 | }
|
437 |
|
438 | return exactChild;
|
439 | }
|
440 |
|
441 | function fileAppendId(file, id, extensions) {
|
442 | var parts = id.split("/");
|
443 |
|
444 | // Use `Array.prototype.every` to terminate iteration early if
|
445 | // `fileAppendIdPart` returns a falsy value.
|
446 | parts.every(function (part, i) {
|
447 | return file = i < parts.length - 1
|
448 | ? fileAppendIdPart(file, part)
|
449 | : fileAppendIdPart(file, part, extensions);
|
450 | });
|
451 |
|
452 | return file;
|
453 | }
|
454 |
|
455 | function recordChild(parentModule, childFile) {
|
456 | var childModule = childFile && childFile.module;
|
457 | if (parentModule && childModule) {
|
458 | parentModule.childrenById[childModule.id] = childModule;
|
459 | }
|
460 | }
|
461 |
|
462 | function fileResolve(file, id, parentModule, seenDirFiles) {
|
463 | var parentModule = parentModule || file.module;
|
464 | var extensions = fileGetExtensions(file);
|
465 |
|
466 | file =
|
467 | // Absolute module identifiers (i.e. those that begin with a `/`
|
468 | // character) are interpreted relative to the root directory, which
|
469 | // is a slight deviation from Node, which has access to the entire
|
470 | // file system.
|
471 | id.charAt(0) === "/" ? fileAppendId(root, id, extensions) :
|
472 | // Relative module identifiers are interpreted relative to the
|
473 | // current file, naturally.
|
474 | id.charAt(0) === "." ? fileAppendId(file, id, extensions) :
|
475 | // Top-level module identifiers are interpreted as referring to
|
476 | // packages in `node_modules` directories.
|
477 | nodeModulesLookup(file, id, extensions);
|
478 |
|
479 | // If the identifier resolves to a directory, we use the same logic as
|
480 | // Node to find an `index.js` or `package.json` file to evaluate.
|
481 | while (fileIsDirectory(file)) {
|
482 | seenDirFiles = seenDirFiles || [];
|
483 |
|
484 | // If the "main" field of a `package.json` file resolves to a
|
485 | // directory we've already considered, then we should not attempt to
|
486 | // read the same `package.json` file again. Using an array as a set
|
487 | // is acceptable here because the number of directories to consider
|
488 | // is rarely greater than 1 or 2. Also, using indexOf allows us to
|
489 | // store File objects instead of strings.
|
490 | if (seenDirFiles.indexOf(file) < 0) {
|
491 | seenDirFiles.push(file);
|
492 |
|
493 | var pkgJsonFile = fileAppendIdPart(file, "package.json"), main;
|
494 | var pkg = pkgJsonFile && fileEvaluate(pkgJsonFile, parentModule);
|
495 | if (pkg &&
|
496 | mainFields.some(function (name) {
|
497 | return isString(main = pkg[name]);
|
498 | })) {
|
499 | recordChild(parentModule, pkgJsonFile);
|
500 |
|
501 | // The "main" field of package.json does not have to begin with
|
502 | // ./ to be considered relative, so first we try simply
|
503 | // appending it to the directory path before falling back to a
|
504 | // full fileResolve, which might return a package from a
|
505 | // node_modules directory.
|
506 | file = fileAppendId(file, main, extensions) ||
|
507 | fileResolve(file, main, parentModule, seenDirFiles);
|
508 |
|
509 | if (file) {
|
510 | // The fileAppendId call above may have returned a directory,
|
511 | // so continue the loop to make sure we resolve it to a
|
512 | // non-directory file.
|
513 | continue;
|
514 | }
|
515 | }
|
516 | }
|
517 |
|
518 | // If we didn't find a `package.json` file, or it didn't have a
|
519 | // resolvable `.main` property, the only possibility left to
|
520 | // consider is that this directory contains an `index.js` module.
|
521 | // This assignment almost always terminates the while loop, because
|
522 | // there's very little chance `fileIsDirectory(file)` will be true
|
523 | // for the result of `fileAppendIdPart(file, "index.js")`. However,
|
524 | // in principle it is remotely possible that a file called
|
525 | // `index.js` could be a directory instead of a file.
|
526 | file = fileAppendIdPart(file, "index.js");
|
527 | }
|
528 |
|
529 | if (file && isString(file.contents)) {
|
530 | file = fileResolve(file, file.contents, parentModule, seenDirFiles);
|
531 | }
|
532 |
|
533 | recordChild(parentModule, file);
|
534 |
|
535 | return file;
|
536 | };
|
537 |
|
538 | function nodeModulesLookup(file, id, extensions) {
|
539 | if (isFunction(override)) {
|
540 | id = override(id, file.module.id);
|
541 | }
|
542 |
|
543 | if (isString(id)) {
|
544 | for (var resolved; file && ! resolved; file = file.parent) {
|
545 | resolved = fileIsDirectory(file) &&
|
546 | fileAppendId(file, "node_modules/" + id, extensions);
|
547 | }
|
548 |
|
549 | return resolved;
|
550 | }
|
551 | }
|
552 |
|
553 | return install;
|
554 | };
|
555 |
|
556 | if (typeof exports === "object") {
|
557 | exports.makeInstaller = makeInstaller;
|
558 | }
|