diff options
author | Florian Dold <florian.dold@gmail.com> | 2016-10-10 03:50:11 +0200 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2016-10-10 03:50:11 +0200 |
commit | 9af485a584e47fd503ed5c62b9f6482574715f1e (patch) | |
tree | 687abbcff412bd4ce5c51d51de030a06e3963cf5 /lib/package.js |
Squashed 'thirdparty/systemjs/' content from commit 5ed69b6
git-subtree-dir: thirdparty/systemjs
git-subtree-split: 5ed69b6344e8fc1cd43bf758350b2236f57e1499
Diffstat (limited to 'lib/package.js')
-rw-r--r-- | lib/package.js | 596 |
1 files changed, 596 insertions, 0 deletions
diff --git a/lib/package.js b/lib/package.js new file mode 100644 index 000000000..cdd3815e9 --- /dev/null +++ b/lib/package.js @@ -0,0 +1,596 @@ +/* + * Package Configuration Extension + * + * Example: + * + * SystemJS.packages = { + * jquery: { + * main: 'index.js', // when not set, package name is requested directly + * format: 'amd', + * defaultExtension: 'ts', // defaults to 'js', can be set to false + * modules: { + * '*.ts': { + * loader: '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.js': './another/index.js', + * // test.js / test -> lib/test.js + * './test.js': './lib/test.js', + * + * // environment-specific map configurations + * './index.js': { + * '~browser': './index-node.js', + * './custom-condition.js|~export': './index-custom.js' + * } + * }, + * // allows for setting package-prefixed depCache + * // keys are normalized module names relative to the package itself + * depCache: { + * // import 'package/index.js' loads in parallel package/lib/test.js,package/vendor/sizzle.js + * './index.js': ['./test'], + * './test.js': ['external-dep'], + * 'external-dep/path.js': ['./another.js'] + * } + * } + * }; + * + * Then: + * import 'jquery' -> jquery/index.js + * import 'jquery/submodule' -> jquery/submodule.js + * import 'jquery/submodule.ts' -> jquery/submodule.ts loaded as typescript + * import 'jquery/vendor/another' -> another/index.js + * + * Detailed Behaviours + * - main can have a leading "./" can be added optionally + * - map and defaultExtension are applied to the main + * - defaultExtension adds the extension only if the exact extension is not present + * - defaultJSExtensions applies after map when defaultExtension is not set + * - if a meta value is available for a module, map and defaultExtension are skipped + * - like global map, package map also applies to subpaths (sizzle/x, ./vendor/another/sub) + * - condition module map is '@env' module in package or '@system-env' globally + * - map targets support conditional interpolation ('./x': './x.#{|env}.js') + * - internal package map targets cannot use boolean conditionals + * + * Package Configuration Loading + * + * Not all packages may already have their configuration present in the System config + * For these cases, a list of packageConfigPaths can be provided, which when matched against + * a request, will first request a ".json" file by the package name to derive the package + * configuration from. This allows dynamic loading of non-predetermined code, a key use + * case in SystemJS. + * + * Example: + * + * SystemJS.packageConfigPaths = ['packages/test/package.json', 'packages/*.json']; + * + * // will first request 'packages/new-package/package.json' for the package config + * // before completing the package request to 'packages/new-package/path' + * SystemJS.import('packages/new-package/path'); + * + * // will first request 'packages/test/package.json' before the main + * SystemJS.import('packages/test'); + * + * When a package matches packageConfigPaths, it will always send a config request for + * the package configuration. + * The package name itself is taken to be the match up to and including the last wildcard + * or trailing slash. + * The most specific package config path will be used. + * Any existing package configurations for the package will deeply merge with the + * package config, with the existing package configurations taking preference. + * To opt-out of the package configuration request for a package that matches + * packageConfigPaths, use the { configured: true } package config option. + * + */ +(function() { + + hookConstructor(function(constructor) { + return function() { + constructor.call(this); + this.packages = {}; + this.packageConfigPaths = []; + }; + }); + + function getPackage(loader, normalized) { + // use most specific package + var curPkg, curPkgLen = 0, pkgLen; + for (var p in loader.packages) { + if (normalized.substr(0, p.length) === p && (normalized.length === p.length || normalized[p.length] === '/')) { + pkgLen = p.split('/').length; + if (pkgLen > curPkgLen) { + curPkg = p; + curPkgLen = pkgLen; + } + } + } + return curPkg; + } + + function addDefaultExtension(loader, pkg, pkgName, subPath, skipExtensions) { + // don't apply extensions to folders or if defaultExtension = false + if (!subPath || subPath[subPath.length - 1] == '/' || skipExtensions || pkg.defaultExtension === false) + return subPath; + + var metaMatch = false; + + // exact meta or meta with any content after the last wildcard skips extension + if (pkg.meta) + getMetaMatches(pkg.meta, subPath, function(metaPattern, matchMeta, matchDepth) { + if (matchDepth == 0 || metaPattern.lastIndexOf('*') != metaPattern.length - 1) + return metaMatch = true; + }); + + // exact global meta or meta with any content after the last wildcard skips extension + if (!metaMatch && loader.meta) + getMetaMatches(loader.meta, pkgName + '/' + subPath, function(metaPattern, matchMeta, matchDepth) { + if (matchDepth == 0 || metaPattern.lastIndexOf('*') != metaPattern.length - 1) + return metaMatch = true; + }); + + if (metaMatch) + return subPath; + + // work out what the defaultExtension is and add if not there already + // NB reconsider if default should really be ".js"? + var defaultExtension = '.' + (pkg.defaultExtension || 'js'); + if (subPath.substr(subPath.length - defaultExtension.length) != defaultExtension) + return subPath + defaultExtension; + else + return subPath; + } + + function applyPackageConfigSync(loader, pkg, pkgName, subPath, skipExtensions) { + // main + if (!subPath) { + if (pkg.main) + subPath = pkg.main.substr(0, 2) == './' ? pkg.main.substr(2) : pkg.main; + // also no submap if name is package itself (import 'pkg' -> 'path/to/pkg.js') + else + // NB can add a default package main convention here when defaultJSExtensions is deprecated + // if it becomes internal to the package then it would no longer be an exit path + return pkgName + (loader.defaultJSExtensions ? '.js' : ''); + } + + // map config checking without then with extensions + if (pkg.map) { + var mapPath = './' + subPath; + + var mapMatch = getMapMatch(pkg.map, mapPath); + + // we then check map with the default extension adding + if (!mapMatch) { + mapPath = './' + addDefaultExtension(loader, pkg, pkgName, subPath, skipExtensions); + if (mapPath != './' + subPath) + mapMatch = getMapMatch(pkg.map, mapPath); + } + if (mapMatch) { + var mapped = doMapSync(loader, pkg, pkgName, mapMatch, mapPath, skipExtensions); + if (mapped) + return mapped; + } + } + + // normal package resolution + return pkgName + '/' + addDefaultExtension(loader, pkg, pkgName, subPath, skipExtensions); + } + + function validMapping(mapMatch, mapped, pkgName, path) { + // disallow internal to subpath maps + if (mapMatch == '.') + throw new Error('Package ' + pkgName + ' has a map entry for "." which is not permitted.'); + + // allow internal ./x -> ./x/y or ./x/ -> ./x/y recursive maps + // but only if the path is exactly ./x and not ./x/z + if (mapped.substr(0, mapMatch.length) == mapMatch && path.length > mapMatch.length) + return false; + + return true; + } + + function doMapSync(loader, pkg, pkgName, mapMatch, path, skipExtensions) { + if (path[path.length - 1] == '/') + path = path.substr(0, path.length - 1); + var mapped = pkg.map[mapMatch]; + + if (typeof mapped == 'object') + throw new Error('Synchronous conditional normalization not supported sync normalizing ' + mapMatch + ' in ' + pkgName); + + if (!validMapping(mapMatch, mapped, pkgName, path) || typeof mapped != 'string') + return; + + // package map to main / base-level + if (mapped == '.') + mapped = pkgName; + + // internal package map + else if (mapped.substr(0, 2) == './') + return pkgName + '/' + addDefaultExtension(loader, pkg, pkgName, mapped.substr(2) + path.substr(mapMatch.length), skipExtensions); + + // external map reference + return loader.normalizeSync(mapped + path.substr(mapMatch.length), pkgName + '/'); + } + + function applyPackageConfig(loader, pkg, pkgName, subPath, skipExtensions) { + // main + if (!subPath) { + if (pkg.main) + subPath = pkg.main.substr(0, 2) == './' ? pkg.main.substr(2) : pkg.main; + // also no submap if name is package itself (import 'pkg' -> 'path/to/pkg.js') + else + // NB can add a default package main convention here when defaultJSExtensions is deprecated + // if it becomes internal to the package then it would no longer be an exit path + return Promise.resolve(pkgName + (loader.defaultJSExtensions ? '.js' : '')); + } + + // map config checking without then with extensions + var mapPath, mapMatch; + + if (pkg.map) { + mapPath = './' + subPath; + mapMatch = getMapMatch(pkg.map, mapPath); + + // we then check map with the default extension adding + if (!mapMatch) { + mapPath = './' + addDefaultExtension(loader, pkg, pkgName, subPath, skipExtensions); + if (mapPath != './' + subPath) + mapMatch = getMapMatch(pkg.map, mapPath); + } + } + + return (mapMatch ? doMap(loader, pkg, pkgName, mapMatch, mapPath, skipExtensions) : Promise.resolve()) + .then(function(mapped) { + if (mapped) + return Promise.resolve(mapped); + + // normal package resolution / fallback resolution for no conditional match + return Promise.resolve(pkgName + '/' + addDefaultExtension(loader, pkg, pkgName, subPath, skipExtensions)); + }); + } + + function doStringMap(loader, pkg, pkgName, mapMatch, mapped, path, skipExtensions) { + // NB the interpolation cases should strictly skip subsequent interpolation + // package map to main / base-level + if (mapped == '.') + mapped = pkgName; + + // internal package map + else if (mapped.substr(0, 2) == './') + return Promise.resolve(pkgName + '/' + addDefaultExtension(loader, pkg, pkgName, mapped.substr(2) + path.substr(mapMatch.length), skipExtensions)) + .then(function(name) { + return interpolateConditional.call(loader, name, pkgName + '/'); + }); + + // external map reference + return loader.normalize(mapped + path.substr(mapMatch.length), pkgName + '/'); + } + + function doMap(loader, pkg, pkgName, mapMatch, path, skipExtensions) { + if (path[path.length - 1] == '/') + path = path.substr(0, path.length - 1); + + var mapped = pkg.map[mapMatch]; + + if (typeof mapped == 'string') { + if (!validMapping(mapMatch, mapped, pkgName, path)) + return Promise.resolve(); + return doStringMap(loader, pkg, pkgName, mapMatch, mapped, path, skipExtensions); + } + + // we use a special conditional syntax to allow the builder to handle conditional branch points further + if (loader.builder) + return Promise.resolve(pkgName + '/#:' + path); + + // we load all conditions upfront + var conditionPromises = []; + var conditions = []; + for (var e in mapped) { + var c = parseCondition(e); + conditions.push({ + condition: c, + map: mapped[e] + }); + conditionPromises.push(loader['import'](c.module, pkgName)); + } + + // map object -> conditional map + return Promise.all(conditionPromises) + .then(function(conditionValues) { + // first map condition to match is used + for (var i = 0; i < conditions.length; i++) { + var c = conditions[i].condition; + var value = readMemberExpression(c.prop, conditionValues[i]); + if (!c.negate && value || c.negate && !value) + return conditions[i].map; + } + }) + .then(function(mapped) { + if (mapped) { + if (!validMapping(mapMatch, mapped, pkgName, path)) + return; + return doStringMap(loader, pkg, pkgName, mapMatch, mapped, path, skipExtensions); + } + + // no environment match -> fallback to original subPath by returning undefined + }); + } + + // normalizeSync = decanonicalize + package resolution + SystemJSLoader.prototype.normalizeSync = SystemJSLoader.prototype.decanonicalize = SystemJSLoader.prototype.normalize; + + // decanonicalize must JUST handle package defaultExtension: false case when defaultJSExtensions is set + // to be deprecated! + hook('decanonicalize', function(decanonicalize) { + return function(name, parentName) { + if (this.builder) + return decanonicalize.call(this, name, parentName, true); + + var decanonicalized = decanonicalize.call(this, name, parentName, false); + + if (!this.defaultJSExtensions) + return decanonicalized; + + var pkgName = getPackage(this, decanonicalized); + + var pkg = this.packages[pkgName]; + var defaultExtension = pkg && pkg.defaultExtension; + + if (defaultExtension == undefined && pkg && pkg.meta) + getMetaMatches(pkg.meta, decanonicalized.substr(pkgName), function(metaPattern, matchMeta, matchDepth) { + if (matchDepth == 0 || metaPattern.lastIndexOf('*') != metaPattern.length - 1) { + defaultExtension = false; + return true; + } + }); + + if ((defaultExtension === false || defaultExtension && defaultExtension != '.js') && name.substr(name.length - 3, 3) != '.js' && decanonicalized.substr(decanonicalized.length - 3, 3) == '.js') + decanonicalized = decanonicalized.substr(0, decanonicalized.length - 3); + + return decanonicalized; + }; + }); + + hook('normalizeSync', function(normalizeSync) { + return function(name, parentName, isPlugin) { + var loader = this; + isPlugin = isPlugin === true; + + // apply contextual package map first + // (we assume the parent package config has already been loaded) + if (parentName) + var parentPackageName = getPackage(loader, parentName) || + loader.defaultJSExtensions && parentName.substr(parentName.length - 3, 3) == '.js' && + getPackage(loader, parentName.substr(0, parentName.length - 3)); + + var parentPackage = parentPackageName && loader.packages[parentPackageName]; + + // ignore . since internal maps handled by standard package resolution + if (parentPackage && name[0] != '.') { + var parentMap = parentPackage.map; + var parentMapMatch = parentMap && getMapMatch(parentMap, name); + + if (parentMapMatch && typeof parentMap[parentMapMatch] == 'string') { + var mapped = doMapSync(loader, parentPackage, parentPackageName, parentMapMatch, name, isPlugin); + if (mapped) + return mapped; + } + } + + var defaultJSExtension = loader.defaultJSExtensions && name.substr(name.length - 3, 3) != '.js'; + + // apply map, core, paths, contextual package map + var normalized = normalizeSync.call(loader, name, parentName, false); + + // undo defaultJSExtension + if (defaultJSExtension && normalized.substr(normalized.length - 3, 3) != '.js') + defaultJSExtension = false; + if (defaultJSExtension) + normalized = normalized.substr(0, normalized.length - 3); + + var pkgConfigMatch = getPackageConfigMatch(loader, normalized); + var pkgName = pkgConfigMatch && pkgConfigMatch.packageName || getPackage(loader, normalized); + + if (!pkgName) + return normalized + (defaultJSExtension ? '.js' : ''); + + var subPath = normalized.substr(pkgName.length + 1); + + return applyPackageConfigSync(loader, loader.packages[pkgName] || {}, pkgName, subPath, isPlugin); + }; + }); + + hook('normalize', function(normalize) { + return function(name, parentName, isPlugin) { + var loader = this; + isPlugin = isPlugin === true; + + return Promise.resolve() + .then(function() { + // apply contextual package map first + // (we assume the parent package config has already been loaded) + if (parentName) + var parentPackageName = getPackage(loader, parentName) || + loader.defaultJSExtensions && parentName.substr(parentName.length - 3, 3) == '.js' && + getPackage(loader, parentName.substr(0, parentName.length - 3)); + + var parentPackage = parentPackageName && loader.packages[parentPackageName]; + + // ignore . since internal maps handled by standard package resolution + if (parentPackage && name.substr(0, 2) != './') { + var parentMap = parentPackage.map; + var parentMapMatch = parentMap && getMapMatch(parentMap, name); + + if (parentMapMatch) + return doMap(loader, parentPackage, parentPackageName, parentMapMatch, name, isPlugin); + } + + return Promise.resolve(); + }) + .then(function(mapped) { + if (mapped) + return mapped; + + var defaultJSExtension = loader.defaultJSExtensions && name.substr(name.length - 3, 3) != '.js'; + + // apply map, core, paths, contextual package map + var normalized = normalize.call(loader, name, parentName, false); + + // undo defaultJSExtension + if (defaultJSExtension && normalized.substr(normalized.length - 3, 3) != '.js') + defaultJSExtension = false; + if (defaultJSExtension) + normalized = normalized.substr(0, normalized.length - 3); + + var pkgConfigMatch = getPackageConfigMatch(loader, normalized); + var pkgName = pkgConfigMatch && pkgConfigMatch.packageName || getPackage(loader, normalized); + + if (!pkgName) + return Promise.resolve(normalized + (defaultJSExtension ? '.js' : '')); + + var pkg = loader.packages[pkgName]; + + // if package is already configured or not a dynamic config package, use existing package config + var isConfigured = pkg && (pkg.configured || !pkgConfigMatch); + return (isConfigured ? Promise.resolve(pkg) : loadPackageConfigPath(loader, pkgName, pkgConfigMatch.configPath)) + .then(function(pkg) { + var subPath = normalized.substr(pkgName.length + 1); + + return applyPackageConfig(loader, pkg, pkgName, subPath, isPlugin); + }); + }); + }; + }); + + // check if the given normalized name matches a packageConfigPath + // if so, loads the config + var packageConfigPaths = {}; + + // data object for quick checks against package paths + function createPkgConfigPathObj(path) { + var lastWildcard = path.lastIndexOf('*'); + var length = Math.max(lastWildcard + 1, path.lastIndexOf('/')); + return { + length: length, + regEx: new RegExp('^(' + path.substr(0, length).replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^\\/]+') + ')(\\/|$)'), + wildcard: lastWildcard != -1 + }; + } + + // most specific match wins + function getPackageConfigMatch(loader, normalized) { + var pkgName, exactMatch = false, configPath; + for (var i = 0; i < loader.packageConfigPaths.length; i++) { + var packageConfigPath = loader.packageConfigPaths[i]; + var p = packageConfigPaths[packageConfigPath] || (packageConfigPaths[packageConfigPath] = createPkgConfigPathObj(packageConfigPath)); + if (normalized.length < p.length) + continue; + var match = normalized.match(p.regEx); + if (match && (!pkgName || (!(exactMatch && p.wildcard) && pkgName.length < match[1].length))) { + pkgName = match[1]; + exactMatch = !p.wildcard; + configPath = pkgName + packageConfigPath.substr(p.length); + } + } + + if (!pkgName) + return; + + return { + packageName: pkgName, + configPath: configPath + }; + } + + function loadPackageConfigPath(loader, pkgName, pkgConfigPath) { + var configLoader = loader.pluginLoader || loader; + + // NB remove this when json is default + (configLoader.meta[pkgConfigPath] = configLoader.meta[pkgConfigPath] || {}).format = 'json'; + configLoader.meta[pkgConfigPath].loader = null; + + return configLoader.load(pkgConfigPath) + .then(function() { + var cfg = configLoader.get(pkgConfigPath)['default']; + + // support "systemjs" prefixing + if (cfg.systemjs) + cfg = cfg.systemjs; + + // modules backwards compatibility + if (cfg.modules) { + cfg.meta = cfg.modules; + warn.call(loader, 'Package config file ' + pkgConfigPath + ' is configured with "modules", which is deprecated as it has been renamed to "meta".'); + } + + return setPkgConfig(loader, pkgName, cfg, true); + }); + } + + function getMetaMatches(pkgMeta, subPath, matchFn) { + // wildcard meta + var meta = {}; + var wildcardIndex; + for (var module in pkgMeta) { + // allow meta to start with ./ for flexibility + var dotRel = module.substr(0, 2) == './' ? './' : ''; + if (dotRel) + module = module.substr(2); + + wildcardIndex = module.indexOf('*'); + if (wildcardIndex === -1) + continue; + + if (module.substr(0, wildcardIndex) == subPath.substr(0, wildcardIndex) + && module.substr(wildcardIndex + 1) == subPath.substr(subPath.length - module.length + wildcardIndex + 1)) { + // alow match function to return true for an exit path + if (matchFn(module, pkgMeta[dotRel + module], module.split('/').length)) + return; + } + } + // exact meta + var exactMeta = pkgMeta[subPath] && pkgMeta.hasOwnProperty && pkgMeta.hasOwnProperty(subPath) ? pkgMeta[subPath] : pkgMeta['./' + subPath]; + if (exactMeta) + matchFn(exactMeta, exactMeta, 0); + } + + hook('locate', function(locate) { + return function(load) { + var loader = this; + return Promise.resolve(locate.call(this, load)) + .then(function(address) { + var pkgName = getPackage(loader, load.name); + if (pkgName) { + var pkg = loader.packages[pkgName]; + var subPath = load.name.substr(pkgName.length + 1); + + var meta = {}; + if (pkg.meta) { + var bestDepth = 0; + + // NB support a main shorthand in meta here? + getMetaMatches(pkg.meta, subPath, function(metaPattern, matchMeta, matchDepth) { + if (matchDepth > bestDepth) + bestDepth = matchDepth; + extendMeta(meta, matchMeta, matchDepth && bestDepth > matchDepth); + }); + + extendMeta(load.metadata, meta); + } + + // format + if (pkg.format && !load.metadata.loader) + load.metadata.format = load.metadata.format || pkg.format; + } + + return address; + }); + }; + }); + +})(); |