diff --git a/Makefile b/Makefile index b0d23b89..f72da46f 100755 --- a/Makefile +++ b/Makefile @@ -70,9 +70,9 @@ dist/system.src.js: lib/*.js $(ESML)/*.js lib/cjs.js \ lib/amd.js \ lib/map.js \ + lib/package.js \ lib/plugins.js \ lib/alias.js \ - lib/package.js \ lib/bundles.js \ lib/depCache.js \ lib/conditionals.js \ @@ -97,9 +97,9 @@ dist/system-prod.src.js: lib/*.js $(ESML)/*.js lib/register.js \ lib/global-helpers.js \ lib/map.js \ + lib/package.js \ lib/plugins.js \ lib/alias.js \ - lib/package.js \ lib/bundles.js \ lib/depCache.js \ lib/conditionals.js \ diff --git a/lib/map.js b/lib/map.js index 680c5730..0f4f25e6 100644 --- a/lib/map.js +++ b/lib/map.js @@ -4,134 +4,46 @@ Provides map configuration through System.map['jquery'] = 'some/module/map' - As well as contextual map config through - System.map['bootstrap'] = { - jquery: 'some/module/map2' - } - - Note that this applies for subpaths, just like RequireJS + Note that this applies for subpaths, just like RequireJS: jquery -> 'some/module/map' jquery/path -> 'some/module/map/path' bootstrap -> 'bootstrap' - Inside any module name of the form 'bootstrap' or 'bootstrap/*' - jquery -> 'some/module/map2' - jquery/p -> 'some/module/map2/p' - - Maps are carefully applied from most specific contextual map, to least specific global map + The most specific map is always taken, as longest path length */ -(function() { +hookConstructor(function(constructor) { + return function() { + constructor.call(this); + this.map = {}; + }; +}); - hookConstructor(function(constructor) { - return function() { - constructor.call(this); - this.map = {}; - }; - }); +hook('normalize', function(normalize) { + return function(name, parentName, parentAddress) { + var loader = this; + return Promise.resolve(normalize.call(loader, name, parentName, parentAddress)) + .then(function(name) { + var bestMatch, bestMatchLength = 0; - // return if prefix parts (separated by '/') match the name - // eg prefixMatch('jquery/some/thing', 'jquery') -> true - // prefixMatch('jqueryhere/', 'jquery') -> false - function prefixMatch(name, prefix) { - if (name.length < prefix.length) - return false; - if (name.substr(0, prefix.length) != prefix) - return false; - if (name[prefix.length] && name[prefix.length] != '/') - return false; - return true; - } - - // get the depth of a given path - // eg pathLen('some/name') -> 2 - function pathLen(name) { - var len = 1; - for (var i = 0, l = name.length; i < l; i++) - if (name[i] === '/') - len++; - return len; - } - - function doMap(name, matchLen, map) { - return map + name.substr(matchLen); - } - - // given a relative-resolved module name and normalized parent name, - // apply the map configuration - function applyMap(name, parentName, loader) { - var curMatch, curMatchLength = 0; - var curParent, curParentMatchLength = 0; - var tmpParentLength, tmpPrefixLength; - var subPath; - var nameParts; - - // first find most specific contextual match - if (parentName) { + // now do the global map for (var p in loader.map) { - var curMap = loader.map[p]; - if (typeof curMap != 'object') - continue; + if (typeof loader.map[p] != 'string') + throw new TypeError('Map configuration no longer permits object submaps. Use package map instead (`System.packages[name].map`).'); - // most specific parent match wins first - if (!prefixMatch(parentName, p)) - continue; - - tmpParentLength = pathLen(p); - if (tmpParentLength <= curParentMatchLength) - continue; - - for (var q in curMap) { - // most specific name match wins - if (!prefixMatch(name, q)) + if (name.substr(0, p.length) == p && (name.length == p.length || name[p.length] == '/')) { + var curMatchLength = p.split('/').length; + if (curMatchLength <= bestMatchLength) continue; - tmpPrefixLength = pathLen(q); - if (tmpPrefixLength <= curMatchLength) - continue; - - curMatch = q; - curMatchLength = tmpPrefixLength; - curParent = p; - curParentMatchLength = tmpParentLength; + bestMatch = p; + bestMatchLength = curMatchLength; } } - } - // if we found a contextual match, apply it now - if (curMatch) - return doMap(name, curMatch.length, loader.map[curParent][curMatch]); + if (bestMatch) + return loader.map[bestMatch] + name.substr(bestMatch.length); - // now do the global map - for (var p in loader.map) { - var curMap = loader.map[p]; - if (typeof curMap != 'string') - continue; - - if (!prefixMatch(name, p)) - continue; - - var tmpPrefixLength = pathLen(p); - - if (tmpPrefixLength <= curMatchLength) - continue; - - curMatch = p; - curMatchLength = tmpPrefixLength; - } - - if (curMatch) - return doMap(name, curMatch.length, loader.map[curMatch]); - - return name; - } - - hook('normalize', function(normalize) { - return function(name, parentName, parentAddress) { - var loader = this; - return Promise.resolve(normalize.call(loader, name, parentName, parentAddress)) - .then(function(name) { - return applyMap(name, parentName, loader); - }); - }; - }); -})(); + return name; + }); + }; +}); diff --git a/lib/package.js b/lib/package.js index b318f73c..040f2a2b 100644 --- a/lib/package.js +++ b/lib/package.js @@ -7,47 +7,35 @@ * * Example: * - * System.package('jquery', { - * main: 'index.js', - * format: 'amd', - * defaultJSExtension: true, - * modules: { - * 'vendor/sizzle.js': { - * format: 'global' - * } - * }, - * map: { - * // map internal require('sizzle') to local require('./vendor/sizzle') - * sizzle: './vendor/sizzle.js', - * - * // map external require of 'jquery/vendor/another.js' to 'another/index.js' - * './vendor/another.js': 'another/index.js' - * } - * }); - * - * Is equivalent to: - * * System.config({ - * meta: { - * 'jquery/*': { - * format: 'amd', - * } - * 'jquery/vendor/sizzle.js': { - * format: 'global' - * } - * }, - * map: { - * 'jquery/vendor/another.js': 'another/index.js' + * packages: { * jquery: { - * sizzle: 'jquery/vendor/sizzle.js' + * main: 'index.js', // this main is actually set by default + * format: 'amd', + * defaultExtension: 'js', + * meta: { + * '*.ts': { + * plugin: 'typescript' + * }, + * 'vendor/sizzle.js': { + * format: 'global' + * } + * }, + * map: { + * // map internal require('sizzle') to local require('./vendor/sizzle') + * sizzle: './vendor/sizzle.js', + * + * // map any internal or external require of 'jquery/vendor/another' to 'another/index.js' + * './vendor/another': 'another/index.js' + * } * } * } * }); * - * With normalization hooks providing main and defaultExtension support - * so that: - * import 'jquery' => import 'jquery/index.js' - * import 'jquery/module' => import 'jquery/module.js' + * Then: + * import 'jquery' -> jquery/index.js + * import 'jquery/submodule' -> jquery/submodule.js + * import 'jquery/vendor/another' -> jquery/another/index.js * * In addition, the following meta properties will be allowed to be package * -relative as well in the package meta config: @@ -61,86 +49,136 @@ hookConstructor(function(constructor) { return function() { constructor.call(this); - this.packageMains = {}; - this.packageDefaultJSExtensions = {}; + this.packages = {}; }; }); + function getPackage(name) { + for (var p in this.packages) { + if (name.substr(0, p.length) === p && (name.length === p.length || name[p.length] === '/')) + return p; + } + } + + function applyMap(map, name) { + var bestMatch, bestMatchLength = 0; + + for (var p in map) { + if (name.substr(0, p.length) == p && (name.length == p.length || name[p.length] == '/')) { + var curMatchLength = p.split('/').length; + if (curMatchLength <= bestMatchLength) + continue; + bestMatch = p; + bestMatchLength = curMatchLength; + } + } + if (bestMatch) + name = map[bestMatch] + name.substr(bestMatch.length); + + return name; + } + hook('normalize', function(normalize) { return function(name, parentName, parentAddress) { var loader = this; + + // apply contextual package map first + if (parentName) + var parentPackage = getPackage.call(loader, parentName); + + if (parentPackage && name[0] !== '.') { + var parentMap = loader.packages[parentPackage].map; + if (parentMap) { + name = applyMap(parentMap, name); + + // relative maps are package-relative + if (name[0] === '.') + parentName = parentPackage; + } + } + + // apply global map, relative normalization return normalize.call(loader, name, parentName, parentAddress) .then(function(normalized) { - // main - if (loader.packageMains[normalized]) - normalized = normalized + '/' + loader.packageMains[normalized]; + // check if we are inside a package + var pkgName = getPackage.call(loader, normalized); - // defaultJSExtension - if (normalized.split('/').pop().indexOf('.') == -1) { - for (var d in loader.packageDefaultExtensions) { - if (normalized.substr(0, d.length + 1) == d + '/') { - normalized += '.js'; - break; - } + if (pkgName) { + var pkg = loader.packages[pkgName]; + + // main + if (pkgName === normalized) + normalized += '/' + (pkg.main || 'index.js'); + + // relative maps + if (pkg.map) { + normalized = pkgName + applyMap(pkg.map, '.' + normalized.substr(pkgName.length)).substr(1); + + // normalize package-relative maps + if (normalized.substr(0, 2) == './') + normalized = pkgName + normalized.substr(1); } + + // defaultExtension + if (pkg.defaultExtension + && (!pkg.meta || !pkg.meta[normalized.substr(pkgName.length + 1)]) + && normalized.split('/').pop().indexOf('.') == -1) + normalized += '.' + pkg.defaultExtension; } + return normalized; }); }; }); - SystemJSLoader.prototype.package = function(p, pkgConfig) { - // main - var main = pkgCfg.main; - if (main) - this.packageMains[p] = main.substr(0, 2) == './' ? main.substr(2) : main; - - // defaultJSExtension - if (pkgCfg.defaultJSExtension) - this.packageDefaultJSExtensions[p] = true; - - // format - if (pkgCfg.format) { - var formatMeta = this.meta[p + '/*'] || {}; - formatMeta.format = pkgCfg.format; - this.meta[p + '/*'] = formatMeta; + function extend(a, b, overwrite) { + for (var p in b) { + if (overwrite || !(p in a)) + a[p] = b[p]; } + } - // meta - for (var m in pkgCfg.modules) { - var meta = this.meta[p + '/' + m] || {}; - var pkgMeta = pkgCfg.modules[m]; - // alias shorthand - if (typeof pkgMeta === 'string') - pkgMeta = { alias: pkgMeta }; - for (var p in pkgMeta) { - // plugin and alias can be package-relative - if ((p === 'plugin' || p === 'alias') - && pkgMeta[p].substr(0, 2) == './') - meta[p] = p + pkgMeta[p].substr(1); - else - meta[p] = pkgMeta[p]; - } - this.meta[p + '/' + m] = meta; - } + hook('locate', function(locate) { + return function(load) { + var loader = this; + return Promise.resolve(locate.call(this, load)) + .then(function(address) { + var pkgName = getPackage.call(loader, load.name); + if (pkgName) { + var pkg = loader.packages[pkgName]; + + // format + if (pkg.format) + load.metadata.format = load.metadata.format || pkg.format; - // map - for (var m in pkgCfg.map) { - var target = pkgCfg.map[m]; - if (target.substr(0, 2) == './') - target = p + target.substr(1); + if (pkg.meta) { + // wildcard meta + var meta = {}; + var bestDepth = 0; + var wildcardIndex; + for (var module in pkg.meta) { + wildcardIndex = indexOf.call(module, '*'); + if (wildcardIndex === -1) + continue; + if (module.substr(0, wildcardIndex) === load.name.substr(0, wildcardIndex) + && module.substr(wildcardIndex + 1) === load.name.substr(load.name.length - module.length + wildcardIndex + 1)) { + var depth = module.split('/').length; + if (depth > bestDepth) + bestDetph = depth; + extend(meta, pkg.meta[module], bestDepth == depth); + } + } + // exact meta + if (pkg.meta[load.name]) + extend(meta, meta[load.name], true); + + extend(load.metadata, meta); + } + } - // package-relative base map - if (m.substr(0, 2) == './') { - this.map[p + m.substr(1)] = target; - } - - // package-contextual map - else { - this.map[p] = this.map[p] || {}; - this.map[p][m] = target; - } - } - }; + return address; + }); + }; + }); })(); \ No newline at end of file diff --git a/test/test.js b/test/test.js index dc6bf8d9..6095e953 100644 --- a/test/test.js +++ b/test/test.js @@ -155,34 +155,31 @@ asyncTest('Map configuration subpath', function() { }, err); }); -asyncTest('Contextual map configuration', function() { - System.map['tests/contextual-map.js'] = { - maptest: 'tests/contextual-map-dep.js' +asyncTest('Package map configuration', function() { + System.packages['tests/contextual-test'] = { + main: 'contextual-map.js', + map: { + maptest: 'tests/contextual-map-dep.js' + } }; - System['import']('tests/contextual-map.js').then(function(m) { + System['import']('tests/contextual-test').then(function(m) { ok(m.mapdep == 'mapdep', 'Contextual map dep not loaded'); start(); }, err); }); -asyncTest('Submodule contextual map configuration', function() { - System.map['tests/subcontextual-map'] = { - dep: 'tests/subcontextual-mapdep.js' +asyncTest('Package map with shim', function() { + System.packages['tests/shim-package'] = { + meta: { + '*': { + deps: ['shim-map-dep'] + } + }, + map: { + 'shim-map-dep': 'tests/shim-map-test-dep.js' + } }; - System['import']('tests/subcontextual-map/submodule.js').then(function(m) { - ok(m == 'submapdep', 'Submodule contextual map not loaded'); - start(); - }, err); -}); - -asyncTest('Contextual map with shim', function() { - System.meta['tests/shim-map-test.js'] = { - deps: ['shim-map-dep'] - }; - System.map['tests/shim-map-test.js'] = { - 'shim-map-dep': 'tests/shim-map-test-dep.js' - }; - System['import']('tests/shim-map-test.js').then(function(m) { + System['import']('tests/shim-package/shim-map-test.js').then(function(m) { ok(m == 'depvalue', 'shim dep not loaded'); start(); }, err); diff --git a/test/tests/contextual-map.js b/test/tests/contextual-test/contextual-map.js similarity index 100% rename from test/tests/contextual-map.js rename to test/tests/contextual-test/contextual-map.js diff --git a/test/tests/shim-map-test.js b/test/tests/shim-package/shim-map-test.js similarity index 100% rename from test/tests/shim-map-test.js rename to test/tests/shim-package/shim-map-test.js