-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathraptor-modules-client.js
726 lines (592 loc) · 26.2 KB
/
raptor-modules-client.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
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
/*
GOAL: This module should mirror the NodeJS module system according the documented behavior.
The module transport will generate code that is used for resolving
real paths for a given logical path. This information is used to
resolve dependencies on client-side (in the browser).
Inspired by:
https://github.com/joyent/node/blob/master/lib/module.js
*/
(function() {
var win = typeof window === 'undefined' ? null : window;
if (win && win.$rmod) {
return;
}
/** the module runtime */
var $rmod;
// this object stores the module factories with the keys being real paths of module (e.g. "/[email protected]/lib/index" --> Function)
var definitions = {};
// Search path that will be checked when looking for modules
var searchPaths = [];
// The _ready flag is used to determine if "run" modules can
// be executed or if they should be deferred until all dependencies
// have been loaded
var _ready = false;
// If $rmod.run() is called when the page is not ready then
// we queue up the run modules to be executed later
var runQueue = [];
// this object stores the Module instance cache with the keys being logical paths of modules (e.g., "/$/foo/$/baz" --> Module)
var instanceCache = {};
// this object maps dependency logical path to a specific version (for example, "/$/foo/$/baz" --> ["3.0.0"])
// Each entry in the object is an array. The first item of the array is the version number of the dependency.
// The second item of the array (if present), is the real dependency ID if the entry belongs to a remapping rule.
// For example, with a remapping, an entry might look like:
// "/$/streams" => ["3.0.0", "streams-browser"]
// An example with no remapping:
// "/$/streams" => ["3.0.0"]
var dependencies = {};
// this object maps relative paths to a specific real path
var mains = {};
// used to remap a real path to a new path (keys are real paths and values are relative paths)
var remapped = {};
var cacheByDirname = {};
// When a module is mapped to a global varialble we add a reference
// that maps the real path of the module to the loaded global instance.
// We use this mapping to ensure that global modules are only loaded
// once if they map to the same real path.
//
// See issue #5 - Ensure modules mapped to globals only load once
// https://github.com/raptorjs/raptor-modules/issues/5
var loadedGlobalsByRealPath = {};
// temporary variable for referencing a prototype
var proto;
function moduleNotFoundError(target, from) {
var err = new Error('Cannot find module "' + target + '"' + (from ? ' from "' + from + '"' : ''));
err.code = 'MODULE_NOT_FOUND';
return err;
}
function Module(resolved) {
/*
A Node module has these properties:
- filename: The logical path of the module
- id: The logical path of the module (same as filename)
- exports: The exports provided during load
- loaded: Has module been fully loaded (set to false until factory function returns)
NOT SUPPORTED BY RAPTOR:
- parent: parent Module
- paths: The search path used by this module (NOTE: not documented in Node.js module system so we don't need support)
- children: The modules that were required by this module
*/
this.id = this.filename = resolved[0];
this.loaded = false;
}
Module.cache = instanceCache;
proto = Module.prototype;
proto.load = function(factoryOrObject) {
var logicalPath = this.id;
if (factoryOrObject && factoryOrObject.constructor === Function) {
// factoryOrObject is definitely a function
var lastSlashPos = logicalPath.lastIndexOf('/');
// find the value for the __dirname parameter to factory
var dirname = logicalPath.substring(0, lastSlashPos);
// find the value for the __filename paramter to factory
var filename = logicalPath;
// local cache for requires initiated from this module/dirname
var localCache = cacheByDirname[dirname] || (cacheByDirname[dirname] = {});
// this is the require used by the module
var instanceRequire = function(target) {
return localCache[target] || (localCache[target] = require(target, dirname));
};
// The require method should have a resolve method that will return logical
// path but not actually instantiate the module.
// This resolve function will make sure a definition exists for the corresponding
// real path of the target but it will not instantiate a new instance of the target.
instanceRequire.resolve = function(target) {
if (!target) {
throw moduleNotFoundError('');
}
var resolved = resolve(target, dirname);
if (!resolved) {
throw moduleNotFoundError(target, dirname);
}
// Return logical path
// NOTE: resolved[0] is logical path
return resolved[0];
};
// NodeJS provides access to the cache as a property of the "require" function
instanceRequire.cache = instanceCache;
// Expose the module system runtime via the `runtime` property
instanceRequire.runtime = $rmod;
// $rmod.def("/[email protected]/lib/index", function(require, exports, module, __filename, __dirname) {
this.exports = {};
// call the factory function
factoryOrObject.call(this, instanceRequire, this.exports, this, filename, dirname);
} else {
// factoryOrObject is not a function so have exports reference factoryOrObject
this.exports = factoryOrObject;
}
this.loaded = true;
};
/**
* Defines a packages whose metadata is used by raptor-loader to load the package.
*/
function define(realPath, factoryOrObject, options) {
/*
$rmod.def('/[email protected]/lib/index', function(require, exports, module, __filename, __dirname) {
// module source code goes here
});
*/
var globals = options && options.globals;
definitions[realPath] = factoryOrObject;
if (globals) {
var target = win || global;
for (var i=0;i<globals.length; i++) {
var globalVarName = globals[i];
loadedGlobalsByRealPath[realPath] = target[globalVarName] = require(realPath, realPath);
}
}
}
function registerMain(realPath, relativePath) {
mains[realPath] = relativePath;
}
function remap(oldRealPath, relativePath) {
remapped[oldRealPath] = relativePath;
}
function registerDependency(logicalParentPath, dependencyId, dependencyVersion, dependencyAlsoKnownAs) {
if (dependencyId === false) {
// This module has been remapped to a "void" module (empty object) for the browser.
// Add an entry in the dependencies, but use `null` as the value (handled differently from undefined)
dependencies[logicalParentPath + '/$/' + dependencyAlsoKnownAs] = null;
return;
}
var logicalPath = dependencyId.charAt(0) === '.' ?
logicalParentPath + dependencyId.substring(1) : // Remove '.' at the beginning
logicalParentPath + '/$/' + dependencyId;
dependencies[logicalPath] = [dependencyVersion];
if (dependencyAlsoKnownAs !== undefined) {
dependencies[logicalParentPath + '/$/' + dependencyAlsoKnownAs] = [dependencyVersion, dependencyId, logicalPath];
}
}
/**
* This function will take an array of path parts and normalize them by handling handle ".." and "."
* and then joining the resultant string.
*
* @param {Array} parts an array of parts that presumedly was split on the "/" character.
*/
function normalizePathParts(parts) {
// IMPORTANT: It is assumed that parts[0] === "" because this method is used to
// join an absolute path to a relative path
var i;
var len = 0;
var numParts = parts.length;
for (i = 0; i < numParts; i++) {
var part = parts[i];
if (part === '.') {
// ignore parts with just "."
/*
// if the "." is at end of parts (e.g. ["a", "b", "."]) then trim it off
if (i === numParts - 1) {
//len--;
}
*/
} else if (part === '..') {
// overwrite the previous item by decrementing length
len--;
} else {
// add this part to result and increment length
parts[len] = part;
len++;
}
}
if (len === 1) {
// if we end up with just one part that is empty string
// (which can happen if input is ["", "."]) then return
// string with just the leading slash
return '/';
} else if (len > 2) {
// parts i s
// ["", "a", ""]
// ["", "a", "b", ""]
if (parts[len - 1].length === 0) {
// last part is an empty string which would result in trailing slash
len--;
}
}
// truncate parts to remove unused
parts.length = len;
return parts.join('/');
}
function join(from, target) {
var targetParts = target.split('/');
var fromParts = from == '/' ? [''] : from.split('/');
return normalizePathParts(fromParts.concat(targetParts));
}
function withoutExtension(path) {
var lastDotPos = path.lastIndexOf('.');
var lastSlashPos;
/* jshint laxbreak:true */
return ((lastDotPos === -1) || ((lastSlashPos = path.lastIndexOf('/')) !== -1) && (lastSlashPos > lastDotPos))
? null // use null to indicate that returned path is same as given path
: path.substring(0, lastDotPos);
}
function truncate(str, length) {
return str.substring(0, str.length - length);
}
/**
* @param {String} logicalParentPath the path from which given dependencyId is required
* @param {String} dependencyId the name of the module (e.g. "async") (NOTE: should not contain slashes)
* @param {String} full version of the dependency that is required from given logical parent path
*/
function versionedDependencyInfo(logicalPath, dependencyId, subpath, dependencyVersion) {
// Our internal module resolver will return an array with the following properties:
// - logicalPath: The logical path of the module (used for caching instances)
// - realPath: The real path of the module (used for instantiating new instances via factory)
var realPath = dependencyVersion && ('/' + dependencyId + '@' + dependencyVersion + subpath);
logicalPath = logicalPath + subpath;
// return [logicalPath, realPath, factoryOrObject]
return [logicalPath, realPath, undefined];
}
function resolveAbsolute(target, origTarget) {
var start = target.lastIndexOf('$');
if (start === -1) {
// return [logicalPath, realPath, factoryOrObject]
return [target, target, undefined];
}
// target is something like "/$/foo/$/baz/lib/index"
// In this example we need to find what version of "baz" foo requires
// "start" is currently pointing to the last "$". We want to find the dependencyId
// which will start after after the substring "$/" (so we increment by two)
start += 2;
// the "end" needs to point to the slash that follows the "$" (if there is one)
var end = target.indexOf('/', start + 3);
var logicalPath;
var subpath;
var dependencyId;
if (end === -1) {
// target is something like "/$/foo/$/baz" so there is no subpath after the dependencyId
logicalPath = target;
subpath = '';
dependencyId = target.substring(start);
} else {
// Fixes https://github.com/raptorjs/raptor-modules/issues/15
// Handle scoped packages where scope and package name are separated by a
// forward slash (e.g. '@scope/package-name')
//
// In the case of scoped packages the dependencyId should be the combination of the scope
// and the package name. Therefore if the target module begins with an '@' symbol then
// skip past the first slash
if (target.charAt(start) === '@') {
end = target.indexOf('/', end+1);
}
// target is something like "/$/foo/$/baz/lib/index" so we need to separate subpath
// from the dependencyId
// logical path should not include the subpath
logicalPath = target.substring(0, end);
// subpath will be something like "/lib/index"
subpath = target.substring(end);
// dependencyId will be something like "baz" (will not contain slashes)
dependencyId = target.substring(start, end);
}
// lookup the version
var dependencyInfo = dependencies[logicalPath];
if (dependencyInfo === undefined) {
return undefined;
}
if (dependencyInfo === null) {
// This dependency has been mapped to a void module (empty object). Return an empty
// array as an indicator
return [];
}
return versionedDependencyInfo(
// dependencyInfo[2] is the logicalPath that the module should actually use
// if it has been remapped. If dependencyInfo[2] is undefined then we haven't
// found a remapped module and simply use the logicalPath that we checked
dependencyInfo[2] || logicalPath,
// realPath:
// dependencyInfo[1] is the optional remapped dependency ID
// (use the actual dependencyID from target if remapped dependency ID is undefined)
dependencyInfo[1] || dependencyId,
subpath,
// first item is version number
dependencyInfo[0]);
}
function resolveModule(target, from) {
if (target.charAt(target.length-1) === '/') {
// This is a hack because I found require('util/') in the wild and
// it did not work because of the trailing slash
target = target.slice(0, -1);
}
var len = searchPaths.length;
for (var i = 0; i < len; i++) {
// search path entries always end in "/";
var candidate = searchPaths[i] + target;
var resolved = resolve(candidate, from);
if (resolved) {
return resolved;
}
}
var dependencyId;
var subpath;
var lastSlashPos = target.indexOf('/');
// Fixes https://github.com/raptorjs/raptor-modules/issues/15
// Handle scoped packages where scope and package name are separated by a
// forward slash (e.g. '@scope/package-name')
//
// In the case of scoped packages the dependencyId should be the combination of the scope
// and the package name. Therefore if the target module begins with an '@' symbol then
// skip past the first slash
if (lastSlashPos !== -1 && target.charAt(0) === '@') {
lastSlashPos = target.indexOf('/', lastSlashPos+1);
}
if (lastSlashPos === -1) {
dependencyId = target;
subpath = '';
} else {
// When we're resolving a module, we don't care about the subpath at first
dependencyId = target.substring(0, lastSlashPos);
subpath = target.substring(lastSlashPos);
}
/*
Consider when the module "baz" (which is a dependency of "foo") requires module "async":
resolve('async', '/$/foo/$/baz');
// TRY
/$/foo/$/baz/$/async
/$/foo/$/async
/$/async
// SKIP
/$/foo/$/$/async
/$/$/async
*/
// First check to see if there is a sibling "$" with the given target
// by adding "/$/<target>" to the given "from" path.
// If the given from is "/$/foo/$/baz" then we will try "/$/foo/$/baz/$/async"
var logicalPath = from + '/$/' + dependencyId;
var dependencyInfo = dependencies[logicalPath];
if (dependencyInfo !== undefined) {
if (dependencyInfo === null) {
// This dependency has been mapped to a void module (empty object). Return an empty
// array as an indicator
return [];
}
return versionedDependencyInfo(
// dependencyInfo[2] is the logicalPath that the module should actually use
// if it has been remapped. If dependencyInfo[2] is undefined then we haven't
// found a remapped module and simply use the logicalPath that we checked
dependencyInfo[2] || logicalPath,
// dependencyInfo[1] is the optional remapped dependency ID
// (use the actual dependencyID from target if remapped dependency ID is undefined)
dependencyInfo[1] || dependencyId,
subpath,
// dependencyVersion
dependencyInfo[0]);
}
var end = from.lastIndexOf('/');
// if there is no "/" in the from path then this path is technically invalid (right?)
while(end !== -1) {
var start = -1;
// make sure we don't check a logical path that would end with "/$/$/dependencyId"
if (end > 0) {
start = from.lastIndexOf('/', end - 1);
if ((start !== -1) && (end - start === 2) && (from.charAt(start + 1) === '$')) {
// check to see if the substring from [start:end] is '/$/'
// skip look at this subpath because it ends with "/$/"
end = start;
continue;
}
}
logicalPath = from.substring(0, end) + '/$/' + dependencyId;
dependencyInfo = dependencies[logicalPath];
if (dependencyInfo !== undefined) {
if (dependencyInfo === null) {
return [];
}
return versionedDependencyInfo(
// dependencyInfo[2] is the logicalPath that the module should actually use
// if it has been remapped. If dependencyInfo[2] is undefined then we haven't
// found a remapped module and simply use the logicalPath that we checked
dependencyInfo[2] || logicalPath,
// dependencyInfo[1] is the optional remapped dependency ID
// (use the actual dependencyID from target if remapped dependency ID is undefined)
dependencyInfo[1] || dependencyId,
subpath,
// version number
dependencyInfo[0]);
} else if (start === -1) {
break;
}
// move end to the last slash that precedes it
end = start;
}
// not found
return undefined;
}
function resolve(target, from) {
var resolved;
var remappedPath;
if (target.charAt(0) === '.') {
// turn relative path into absolute path
resolved = resolveAbsolute(join(from, target), target);
} else if (target.charAt(0) === '/') {
// handle targets such as "/my/file" or "/$/foo/$/baz"
resolved = resolveAbsolute(normalizePathParts(target.split('/')));
} else {
remappedPath = remapped[target];
if (remappedPath) {
// The remapped path should be a complete logical path
return resolve(remappedPath);
} else {
// handle targets such as "foo/lib/index"
resolved = resolveModule(target, from);
}
}
if (!resolved) {
return undefined;
}
var logicalPath = resolved[0];
var realPath = resolved[1];
if (logicalPath === undefined) {
// This dependency has been mapped to a void module (empty object).
// Use a special '$' for logicalPath and realPath and an empty object for the factoryOrObject
return ['$', '$', {}];
}
if (!realPath) {
return resolve(logicalPath);
}
// target is something like "/foo/baz"
// There is no installed module in the path
var relativePath;
// check to see if "target" is a "directory" which has a registered main file
if ((relativePath = mains[realPath]) !== undefined) {
// there is a main file corresponding to the given target so add the relative path
logicalPath = join(logicalPath, relativePath);
realPath = join(realPath, relativePath);
}
remappedPath = remapped[realPath];
if (remappedPath !== undefined) {
// remappedPath should be treated as a relative path
logicalPath = join(logicalPath + '/..', remappedPath);
realPath = join(realPath + '/..', remappedPath);
}
var factoryOrObject = definitions[realPath];
if (factoryOrObject === undefined) {
// check for definition for given realPath but without extension
var realPathWithoutExtension;
if (((realPathWithoutExtension = withoutExtension(realPath)) === null) ||
((factoryOrObject = definitions[realPathWithoutExtension]) === undefined)) {
return undefined;
}
// we found the definition based on real path without extension so
// update logical path and real path
logicalPath = truncate(logicalPath, realPath.length - realPathWithoutExtension.length);
realPath = realPathWithoutExtension;
}
// since we had to make sure a definition existed don't throw this away
resolved[0] = logicalPath;
resolved[1] = realPath;
resolved[2] = factoryOrObject;
return resolved;
}
function require(target, from) {
if (!target) {
throw moduleNotFoundError('');
}
var resolved = resolve(target, from);
if (!resolved) {
throw moduleNotFoundError(target, from);
}
var logicalPath = resolved[0];
var module = instanceCache[logicalPath];
if (module !== undefined) {
// found cached entry based on the logical path
return module.exports;
}
// Fixes issue #5 - Ensure modules mapped to globals only load once
// https://github.com/raptorjs/raptor-modules/issues/5
//
// If a module is mapped to a global variable then we want to always
// return that global instance of the module when it is being required
// to avoid duplicate modules being loaded. For modules that are mapped
// to global variables we also add an entry that maps the real path
// of the module to the global instance of the loaded module.
var realPath = resolved[1];
if (loadedGlobalsByRealPath.hasOwnProperty(realPath)) {
return loadedGlobalsByRealPath[realPath];
}
var factoryOrObject = resolved[2];
module = new Module(resolved);
// cache the instance before loading (allows support for circular dependency with partial loading)
instanceCache[logicalPath] = module;
module.load(factoryOrObject);
return module.exports;
}
/*
$rmod.run('/$/installed-module', '/src/foo');
*/
function run(logicalPath, options) {
var wait = !options || (options.wait !== false);
if (wait && !_ready) {
return runQueue.push([logicalPath, options]);
}
require(logicalPath, '/');
}
/*
* Mark the page as being ready and execute any of the
* run modules that were deferred
*/
function ready() {
_ready = true;
var len;
while((len = runQueue.length)) {
// store a reference to the queue before we reset it
var queue = runQueue;
// clear out the queue
runQueue = [];
// run all of the current jobs
for (var i = 0; i < len; i++) {
var args = queue[i];
run(args[0], args[1]);
}
// stop running jobs in the queue if we change to not ready
if (!_ready) {
break;
}
}
}
function addSearchPath(prefix) {
searchPaths.push(prefix);
}
var pendingCount = 0;
var onPendingComplete = function() {
pendingCount--;
if (!pendingCount) {
// Trigger any "require-run" modules in the queue to run
ready();
}
};
/*
* $rmod is the short-hand version that that the transport layer expects
* to be in the browser window object
*/
$rmod = {
// "def" is used to define a module
def: define,
// "dep" is used to register a dependency (e.g. "/$/foo" depends on "baz")
dep: registerDependency,
run: run,
main: registerMain,
remap: remap,
require: require,
resolve: resolve,
join: join,
ready: ready,
addSearchPath: addSearchPath,
/**
* Asynchronous bundle loaders should call `pending()` to instantiate
* a new job. The object we return here has a `done` method that
* should be called when the job completes. When the number of
* pending jobs drops to 0, we invoke any of the require-run modules
* that have been declared.
*/
pending: function() {
_ready = false;
pendingCount++;
return {
done: onPendingComplete
};
}
};
if (win) {
win.$rmod = $rmod;
} else {
module.exports = $rmod;
}
})();