aboutsummaryrefslogtreecommitdiff
path: root/lib/package.js
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2016-10-10 03:50:11 +0200
committerFlorian Dold <florian.dold@gmail.com>2016-10-10 03:50:11 +0200
commit9af485a584e47fd503ed5c62b9f6482574715f1e (patch)
tree687abbcff412bd4ce5c51d51de030a06e3963cf5 /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.js596
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;
+ });
+ };
+ });
+
+})();