4 * Copyright 2011 Sibblingz, Inc.
9 // XXX Lines between comments with @{{{ and @}}} are removed when building
15 // MIT: http://trac.webkit.org/wiki/DetectingWebKit
16 var IS_WEBKIT
= typeof navigator
!== 'undefined' && navigator
&& / AppleWebKit
\//.test(navigator.userAgent);
18 var HAS_SETTIMEOUT
= typeof setTimeout
=== 'function';
25 // Show debug warnings
29 var ENABLE_ALIASES
= true;
32 var ENABLE_PACKAGES
= true;
34 // Web browser support
35 var ENABLE_BROWSER
= true;
37 // Web browser should try to make synchronous requests
38 var BROWSER_SYNC
= false;
41 var ENABLE_NODEJS
= true;
44 var ENABLE_SPACEPORT
= true;
46 // CommonJS compatibility
47 var COMMONJS_COMPAT
= true;
49 // Check for circular dependencies
50 var CHECK_CYCLES
= true;
54 // Utility functions {{{
55 var hasOwnProperty
= ({ }).hasOwnProperty
;
56 var toString
= ({ }).toString
;
62 function hasOwn(obj
, name
) {
63 return obj
&& hasOwnProperty
.call(obj
, name
);
67 return toString
.call(x
) === '[object Array]';
70 function isPlainOldObject(x
) {
71 return toString
.call(x
) === '[object Object]';
74 function map(array
, fn
, context
) {
75 // TODO Fallback if Function.prototype.map is missing
76 return array
.map(fn
, context
);
81 function extend(base
, extension
) {
84 for (key
in extension
) {
85 if (hasOwn(extension
, key
)) {
86 base
[key
] = extension
[key
];
93 function clone(object
, extension
) {
94 return extend(extend({ }, object
), extension
|| { });
96 // Utility functions }}}
99 function stringToPath(parts
) {
100 parts
= isArray(parts
) ? parts
: [ parts
];
102 var splitParts
= [ ];
105 for (i
= 0; i
< parts
.length
; ++i
) {
106 splitParts
= splitParts
.concat(parts
[i
].split(/\//g));
112 function pathToString(path
) {
115 .replace(/\/+/g, '/');
117 if (path
.length
=== 0 && path
[0] === '') {
124 function normalizePath(path
) {
128 for (i
= 0; i
< path
.length
; ++i
) {
132 } else if (path
[i
] === dotdot
) {
134 if (!newPath
.length
) {
135 newPath
= [ dotdot
];
136 } else if (newPath
.length
=== 1) {
137 if (newPath
[0] === dot
|| !newPath
[0]) {
138 newPath
= [ dotdot
];
145 } else if (path
[i
] === dot
) {
147 if (!newPath
.length
) {
152 newPath
.push(path
[i
]);
159 function resolveUrl(cwd
, baseUrl
, path
) {
160 var cwdPath
= normalizePath(stringToPath(cwd
));
161 var basePath
= normalizePath(stringToPath(baseUrl
|| dot
));
162 var npath
= normalizePath(stringToPath(path
));
164 if (npath
[0] === dotdot
|| npath
[0] === dot
) {
165 // Relative paths are based on cwd
166 return pathToString(normalizePath(cwdPath
.concat(npath
)));
167 } else if (npath
[0] === '') {
168 // Absolute path stays absolute
169 return pathToString(npath
);
171 // Implicit relative paths are based on baseUrl
172 return pathToString(basePath
.concat(npath
));
176 function resolveCwd(baseUrl
, cwd
) {
177 var basePath
= normalizePath(stringToPath(baseUrl
|| dot
));
178 var npath
= normalizePath(stringToPath(cwd
));
180 if (npath
[0] === dotdot
|| npath
[0] === dot
) {
181 // Relative paths are absolute
182 return pathToString(npath
);
183 } else if (npath
[0] === '') {
184 // Absolute path stays absolute
185 return pathToString(npath
);
187 // Implicit relative paths are based on baseUrl
188 return pathToString(basePath
.concat(npath
));
192 function dirname(url
) {
193 var path
= stringToPath(url
);
194 path
= path
.slice(0, path
.length
- 1);
195 return pathToString(path
);
197 // Path functions }}}
199 // Argument extraction functions {{{
200 function defArgs(name
, config
, deps
, callback
) {
201 if (typeof name
!== 'string') {
209 if (!isPlainOldObject(config
)) {
216 if (!isArray(deps
)) {
217 // Dependencies omitted
230 function reqArgs(config
, deps
, callback
) {
231 // TODO require(string)
232 if (typeof config
=== 'string') {
233 throw new Error('Not supported');
236 if (!isPlainOldObject(config
)) {
243 if (!isArray(deps
)) {
244 // Dependencies omitted
255 // Argument extraction functions }}}
257 function getScriptName(moduleName
, config
) {
258 if (ENABLE_ALIASES
) {
259 if (hasOwn(config
._aliases
, moduleName
)) {
260 return config
._aliases
[moduleName
];
264 var scriptName
= resolveUrl(config
.cwd
, config
.baseUrl
, moduleName
);
265 scriptName
= scriptName
+ (/\.js$/i.test(scriptName
) ? '' : '.js');
269 function mergeConfigInto(base
, augmentation
) {
270 // The order of these checks are important, because changes cascade
272 if (hasOwn(augmentation
, 'baseUrl')) {
273 base
.baseUrl
= resolveUrl(base
.cwd
, base
.baseUrl
, augmentation
.baseUrl
);
276 if (hasOwn(augmentation
, 'cwd')) {
277 base
.cwd
= augmentation
.cwd
;
278 //base.cwd = resolveCwd(base.baseUrl, augmentation.cwd);
281 if (ENABLE_ALIASES
) {
282 if (hasOwn(base
, '_aliases')) {
283 base
._aliases
= clone(base
._aliases
);
288 if (hasOwn(augmentation
, '_aliases')) {
289 extend(base
._aliases
, augmentation
._aliases
);
292 if (hasOwn(augmentation
, 'aliases')) {
294 for (aliasName
in augmentation
.aliases
) {
295 if (!hasOwn(augmentation
.aliases
, aliasName
)) {
299 var aliasTarget
= augmentation
.aliases
[aliasName
];
301 // Aliases are stored as their full script name
302 base
._aliases
[aliasName
] = getScriptName(aliasTarget
, base
);
307 if (ENABLE_PACKAGES
) {
308 if (hasOwn(base
, '_packageOwners')) {
309 base
._packageOwners
= clone(base
._packageOwners
);
311 base
._packageOwners
= { };
314 if (hasOwn(augmentation
, '_packageOwners')) {
315 extend(base
._packageOwners
, augmentation
._packageOwners
);
318 if (hasOwn(augmentation
, 'packages')) {
320 for (packageName
in augmentation
.packages
) {
321 if (!hasOwn(augmentation
.packages
, packageName
)) {
325 var packageOwner
= getScriptName(packageName
, base
);
326 forEach(augmentation
.packages
[packageName
], function (moduleName
) {
327 base
._packageOwners
[getScriptName(moduleName
, base
)] = packageOwner
;
334 function mergeConfigs(first
, second
) {
335 var base
= clone(first
);
336 mergeConfigInto(base
, second
);
340 function findCycles(graph
, vertices
) {
341 var vertexIndices
= { };
342 var vertexLowLinks
= { };
349 function strongConnect(v
) {
350 vertexIndices
[v
] = index
;
351 vertexLowLinks
[v
] = index
;
355 if (hasOwn(graph
, v
)) {
356 graph
[v
].forEach(function (w
) {
357 if (!hasOwn(vertexIndices
, w
)) {
359 vertexLowLinks
[v
] = Math
.min(vertexLowLinks
[v
], vertexLowLinks
[w
]);
360 } else if (stack
.indexOf(w
) >= 0) {
361 vertexLowLinks
[v
] = Math
.min(vertexLowLinks
[v
], vertexIndices
[w
]);
366 if (vertexLowLinks
[v
] === vertexIndices
[v
]) {
377 vertices
.forEach(function (vertex
) {
378 if (!hasOwn(vertexIndices
, vertex
)) {
379 strongConnect(vertex
);
386 // dependencyGraph :: Map String [String]
387 var dependencyGraph
= { };
389 // pulledScripts :: [String]
390 var pulledScripts
= [ ];
392 function addDependency(from, to
) {
393 if (hasOwn(dependencyGraph
, from)) {
394 dependencyGraph
[from].push(to
);
396 dependencyGraph
[from] = [ to
];
400 function checkCircularDependencies() {
401 var cycles
= findCycles(dependencyGraph
, pulledScripts
);
403 cycles
.forEach(function (cycle
) {
404 if (cycle
.length
> 1) {
405 throw new Error('Circular dependency detected between scripts: ' + cycle
.join(' '));
410 function getScriptsDependingUpon(scriptName
) {
413 for (var curScript
in dependencyGraph
) {
414 if (hasOwn(dependencyGraph
, curScript
)) {
415 if (dependencyGraph
[curScript
].indexOf(scriptName
) >= 0) {
416 scripts
.push(curScript
);
424 // requestedScripts :: Map String Bool
425 var requestedScripts
= { };
427 // requestingScriptCount :: Int
428 var requestingScriptCount
= 0;
430 // We have two queues here.
432 // The script complete queue is built up while executing scripts. A define
433 // call adds to this queue. The queue is flushed when the script completes
434 // execution. This allows us to determine which script was executed
435 // exactly for asynchronous loads.
437 // A load callback queue is built up after a define call knows its complete
438 // name configuration. It is executed when that defined module is
439 // requested. This allows for lazy loading of defiend modules, and also
440 // allows for asynchronous module definitions. There is a mapping of
441 // script name to load callback queue, thus this queue is a hash and not an
444 // scriptCompleteQueue :: [Maybe Error -> Configuration -> IO ()]
445 var scriptCompleteQueue
= [ ];
447 // loadCallbackQueues :: Map String [IO ()]
448 var loadCallbackQueues
= { };
450 // The push-pull mechanism decouples requesters of a module from definers
451 // of a module. When a module is defined, it is "pushed"; when a module is
452 // requested, it is "pulled". If a pull is made on an already-pushed
453 // module name, the pull callback is executed immediately. Else, the pull
454 // callback is executed immediately when the appropriate push is made.
456 // pushed :: Map String a
459 // pulling :: Map String [Maybe Error -> a -> IO ()]
462 function checkPullForLoadCallback(scriptName
) {
463 if (hasOwn(pulling
, scriptName
) && hasOwn(loadCallbackQueues
, scriptName
)) {
464 var callbacks
= loadCallbackQueues
[scriptName
];
465 delete loadCallbackQueues
[scriptName
];
467 forEach(callbacks
, function (callback
) {
473 function checkPullForPush(scriptName
, value
) {
474 if (hasOwn(pulling
, scriptName
) && hasOwn(pushed
, scriptName
)) {
475 var callbacks
= pulling
[scriptName
];
476 delete pulling
[scriptName
];
478 forEach(callbacks
, function (callback
) {
479 callback(null, pushed
[scriptName
]);
484 function enqueueLoadCallback(scriptName
, callback
) {
485 if (hasOwn(loadCallbackQueues
, scriptName
)) {
486 loadCallbackQueues
[scriptName
].push(callback
);
488 loadCallbackQueues
[scriptName
] = [ callback
];
491 checkPullForLoadCallback(scriptName
);
494 function enqueueScriptCompleteCallback(callback
) {
495 if (requestingScriptCount
> 0) {
496 scriptCompleteQueue
.push(callback
);
502 function push(scriptName
, value
) {
503 if (hasOwn(pushed
, scriptName
)) {
504 throw new Error('Should not push value for ' + scriptName
+ ' again');
507 pushed
[scriptName
] = value
;
509 checkPullForPush(scriptName
);
512 function pull(scriptName
, callback
) {
514 pulledScripts
.push(scriptName
);
517 if (hasOwn(pulling
, scriptName
)) {
518 pulling
[scriptName
].push(callback
);
520 pulling
[scriptName
] = [ callback
];
523 checkPullForLoadCallback(scriptName
);
524 checkPullForPush(scriptName
);
527 function needsRequest(scriptName
) {
528 return !hasOwn(requestedScripts
, scriptName
) && !hasOwn(pushed
, scriptName
) && !hasOwn(loadCallbackQueues
, scriptName
);
532 function create(configuration
) {
533 var context
= extend({
536 'reconfigure': reconfigure
,
537 'userCallback': defaultUserCallback
545 context
.configuration
= configuration
;
551 function config(config
) {
552 mergeConfigInto(baseConfig
, config
);
555 function defaultUserCallback(scriptName
, data
, moduleValues
, moduleScripts
, moduleNames
, callback
) {
557 console
.log('Executing', scriptName
);
561 if (typeof data
=== 'function') {
562 if (COMMONJS_COMPAT
&& data
.length
=== 3 && moduleNames
.length
=== 0) {
566 throw new Error('Not supported');
577 moduleValue
= data
.apply(null, moduleValues
);
579 if (COMMONJS_COMPAT
&& data
.length
=== 3 && moduleNames
.length
=== 0) {
580 if (typeof moduleValue
=== 'undefined') {
581 moduleValue
= moduleValues
[1]; // exports
588 callback(null, moduleValue
);
591 function reconfigure(configuration
) {
592 extend(context
, configuration
);
595 function getRequestScriptName(scriptName
, config
) {
596 if (ENABLE_PACKAGES
) {
597 if (hasOwn(config
._packageOwners
, scriptName
)) {
598 return config
._packageOwners
[scriptName
];
605 function request(scriptName
, config
, callback
) {
606 if (!needsRequest(scriptName
)) {
607 throw new Error('Should not request ' + scriptName
+ ' again');
611 console
.log('Requesting script ' + scriptName
);
614 requestedScripts
[scriptName
] = true;
615 ++requestingScriptCount
;
618 --requestingScriptCount
;
620 var scriptCompleteCallbacks
= scriptCompleteQueue
;
621 scriptCompleteQueue
= [ ];
624 if (scriptCompleteCallbacks
.length
=== 0) {
625 console
.warn('Possibly missing define for script ' + scriptName
);
629 callback(err
, scriptCompleteCallbacks
);
632 function tryAsync() {
635 // Try a sync load first
636 if (context
.loadScriptSync
) {
637 // We have this setTimeout logic to handle exceptions thrown by
638 // loadScriptSync. We do not catch exceptions (so debugging is
639 // easier for users), or deal with the 'finally' mess, but
640 // still call done().
642 if (HAS_SETTIMEOUT
) {
643 timer
= setTimeout(function () {
644 done(new Error('Script threw exception'));
648 var success
= context
.loadScriptSync(scriptName
, config
);
650 if (HAS_SETTIMEOUT
) {
660 if (context
.loadScriptAsync
) {
661 return context
.loadScriptAsync(scriptName
, done
, config
);
664 done(new Error('Failed to load script'));
667 function requestAndPullMany(scriptNames
, config
, callback
) {
673 function checkValues() {
678 for (i
= 0; i
< scriptNames
.length
; ++i
) {
679 if (!loaded
[i
]) return;
683 callback(null, values
, scriptNames
);
686 forEach(scriptNames
, function (scriptName
, i
) {
687 var requestScriptName
= getRequestScriptName(scriptName
, config
);
689 if (needsRequest(requestScriptName
)) {
690 request(requestScriptName
, config
, function (err
, callbacks
) {
691 var neoConfig
= mergeConfigs(config
, { });
692 neoConfig
.cwd
= dirname(requestScriptName
);
693 neoConfig
.scriptName
= scriptName
;
695 forEach(callbacks
, function (callback
) {
696 callback(err
, neoConfig
);
700 var errorString
= 'Failed to load ' + requestScriptName
;
702 var dependers
= getScriptsDependingUpon(requestScriptName
);
703 if (dependers
.length
) {
704 errorString
+= ' (depended upon by ' + dependers
.join(', ') + ')';
707 console
.error(errorString
, err
);
712 pull(scriptName
, function (err
, value
) {
721 // In case we have no scripts to load
726 // TODO require(string)
728 var args
= reqArgs
.apply(null, arguments
);
729 var config
= args
.config
;
730 var deps
= args
.deps
;
731 var callback
= args
.callback
;
734 console
.log('Requiring [ ' + (deps
|| [ ]).join(', ') + ' ]');
737 var effectiveConfig
= mergeConfigs(baseConfig
, config
);
739 enqueueScriptCompleteCallback(function (err
, config
) {
742 mergeConfigInto(effectiveConfig
, config
);
744 var scriptNames
= map(deps
, function (dep
) {
745 return getScriptName(dep
, effectiveConfig
);
748 requestAndPullMany(scriptNames
, effectiveConfig
, function (err
, values
) {
751 context
.userCallback(null, callback
, values
, scriptNames
, deps
.slice(), function (err
, value
) {
761 var args
= defArgs
.apply(null, arguments
);
762 var name
= args
.name
;
763 var config
= args
.config
;
764 var deps
= args
.deps
;
765 var callback
= args
.callback
;
768 console
.log('Defining ' + (name
|| 'unnamed package') + ' with dependencies [ ' + (deps
|| [ ]).join(', ') + ' ]');
771 var effectiveConfig
= mergeConfigs(baseConfig
, config
);
773 enqueueScriptCompleteCallback(function (err
, config
) {
776 var oldEffectiveConfig
= clone(effectiveConfig
);
778 // Script name resolution should occur *before* merging config into
780 mergeConfigInto(effectiveConfig
, config
);
784 scriptName
= getScriptName(name
, effectiveConfig
);
786 scriptName
= config
.scriptName
;
789 enqueueLoadCallback(scriptName
, function () {
790 var scriptNames
= map(deps
, function (dep
) {
791 return getScriptName(dep
, effectiveConfig
);
795 map(scriptNames
, function (dep
) {
796 addDependency(scriptName
, dep
);
800 checkCircularDependencies();
802 requestAndPullMany(scriptNames
, effectiveConfig
, function (err
, values
) {
805 context
.userCallback(scriptName
, callback
, values
, scriptNames
, deps
.slice(), function (err
, value
) {
808 push(scriptName
, value
);
816 console
.log('Pulling:', pulling
);
824 if (ENABLE_SPACEPORT
&& typeof loadScript
=== 'function') {
825 // Must be first, because Spaceport has the window object, too.
827 'loadScriptAsync': function (scriptName
, callback
) {
828 loadScript(scriptName
, function () {
832 'loadScriptSync': function (scriptName
) {
838 require
= un
['require'];
839 define
= un
['define'];
840 } else if (ENABLE_BROWSER
&& typeof window
!== 'undefined') {
841 var goodResponseCodes
= [ 200, 204, 206, 301, 302, 303, 304, 307 ];
842 var doc
= window
.document
;
844 var onreadystatechange
= 'onreadystatechange';
845 var onload
= 'onload';
846 var onerror
= 'onerror';
848 function isCleanPath(scriptName
) {
849 // If the path is "back" too much, it's not clean.
850 var x
= stringToPath(dirname(window
.location
.pathname
))
851 .concat(stringToPath(scriptName
));
852 x
= normalizePath(x
);
853 return x
[0] !== '..';
856 var webkitOnloadFlag
= false;
859 'loadScriptAsync': function loadScriptAsync(scriptName
, callback
) {
860 if (!isCleanPath(scriptName
)) {
861 setTimeout(function () {
862 callback(new Error('Path ' + scriptName
+ ' is not clean'));
867 var script
= doc
.createElement('script');
870 // Modelled after jQuery (src/ajax/script.js)
871 script
[onload
] = script
[onreadystatechange
] = function () {
872 if (!script
.readyState
|| /loaded|complete/.test(script
.readyState
)) {
874 var parent
= script
.parentNode
;
876 parent
.removeChild(script
);
880 script
[onload
] = script
[onreadystatechange
] = script
[onerror
] = null;
887 script
[onerror
] = function () {
888 callback(new Error('Failed to load script'));
891 // Remember: we need to attach event handlers before
892 // assigning `src`. Events may be fired as soon as we set
894 script
.src
= scriptName
;
896 doc
['head'].appendChild(script
);
898 'loadScriptSync': function loadScriptSync(scriptName
) {
899 // We provide synchronous script loading via XHR for
900 // browsers specifically to work around a Webkit bug.
901 // After document.onload is called, any script dynamically
902 // loaded will be loaded from Webkit's local cache; *no
903 // HTTP request is made at all*.
907 if (/loaded|complete/.test(document
.readyState
)) {
908 // Don't load synchronously if the document has already loaded
909 if (WARNINGS
&& !webkitOnloadFlag
) {
910 console
.warn('Scripts being loaded after document.onload; scripts may be loaded from out-of-date cache');
911 webkitOnloadFlag
= true;
915 console
.warn('Script possibly loaded from out-of-date cache: ' + scriptName
);
921 // Fall through; load synchronously anyway
927 if (!isCleanPath(scriptName
)) {
934 var xhr
= new XMLHttpRequest();
935 xhr
.open('GET', scriptName
, false);
938 if (goodResponseCodes
.indexOf(xhr
.status
) < 0) {
942 scriptSource
= xhr
.responseText
;
943 scriptSource
+= '\n\n//*/\n//@ sourceURL=' + scriptName
;
950 fn
= Function(scriptSource
);
955 // Don't wrap user code in try/catch
962 window
['require'] = un
['require'];
963 window
['define'] = un
['define'];
964 } else if (ENABLE_NODEJS
&& typeof module
!== 'undefined') {
965 un
= module
.exports
= create({
967 'loadScriptSync': function (scriptName
) {
968 // require here is the Node.JS-provided require
972 code
= require('fs')['readFileSync'](scriptName
, 'utf8');
974 // TODO Detect file-not-found errors only
978 require('vm')['runInNewContext'](code
, un
['context'] || { }, scriptName
);
984 un
['context']['define'] = un
['define'];
985 un
['context']['require'] = un
['require'];
987 throw new Error('Unsupported environment');