UNPKG

20.1 kBJavaScriptView Raw
1makeInstaller = function (options) {
2 "use strict";
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
556if (typeof exports === "object") {
557 exports.makeInstaller = makeInstaller;
558}