systemjs/lib/package.js
2015-10-08 14:29:43 +02:00

543 lines
20 KiB
JavaScript

/*
* Package Configuration Extension
*
* Example:
*
* System.packages = {
* jquery: {
* basePath: 'lib', // optionally only use a subdirectory within the package
* 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'
* }
* },
* // 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': ['sizzle']
* }
* }
* };
*
* 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 no other extension is present
* - defaultJSExtensions applies after map when defaultExtension is not set
* - if a modules 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
*
* In addition, the following modules properties will be allowed to be package
* -relative as well in the package module config:
*
* - loader
* - alias
*
*
* 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:
*
* System.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'
* System.import('packages/new-package/path');
*
* // will first request 'packages/test/package.json' before the main
* System.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.
* Package config paths are ordered - matching is done based on the first match found.
* 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(name) {
// use most specific package
var curPkg, curPkgLen = 0, pkgLen;
for (var p in this.packages) {
if (name.substr(0, p.length) === p && (name.length === p.length || name[p.length] === '/')) {
pkgLen = p.split('/').length;
if (pkgLen > curPkgLen) {
curPkg = p;
curPkgLen = pkgLen;
}
}
}
return curPkg;
}
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;
}
}
return bestMatch;
}
function getBasePath(pkg) {
// sanitize basePath
var basePath = pkg.basePath && pkg.basePath != '.' ? pkg.basePath : '';
if (basePath) {
if (basePath.substr(0, 2) == './')
basePath = basePath.substr(2);
if (basePath[basePath.length - 1] != '/')
basePath += '/';
}
return basePath;
}
// given the package subpath, return the resultant combined path
// defaultExtension is only added if the path does not have
// loader package meta or exact package meta
// We also re-incorporate package-level conditional syntax at this point
// allowing package map and package mains to point to conditionals
// when conditionals are present,
function toPackagePath(loader, pkgName, pkg, basePath, subPath, sync, isPlugin) {
// skip if its a plugin call already, or we have boolean / interpolation conditional syntax in subPath
var skipExtension = !!(isPlugin || subPath.indexOf('#?') != -1 || subPath.match(interpolationRegEx));
// exact meta or meta with any content after the last wildcard skips extension
if (!skipExtension && pkg.modules)
getMetaMatches(pkg.modules, pkgName, subPath, function(metaPattern, matchMeta, matchDepth) {
if (matchDepth == 0 || metaPattern.lastIndexOf('*') != metaPattern.length - 1)
skipExtension = true;
});
var normalized = pkgName + '/' + basePath + subPath + (skipExtension ? '' : getDefaultExtension(pkg, subPath));
return sync ? normalized : booleanConditional.call(loader, normalized, pkgName + '/').then(function(name) {
return interpolateConditional.call(loader, name, pkgName + '/');
});
}
function getDefaultExtension(pkg, subPath) {
// don't apply extensions to folders or if defaultExtension = false
if (subPath[subPath.length - 1] != '/' && pkg.defaultExtension !== false) {
// work out what the defaultExtension is and add if not there already
var defaultExtension = '.' + (pkg.defaultExtension || 'js');
if (subPath.substr(subPath.length - defaultExtension.length) != defaultExtension)
return defaultExtension;
}
return '';
}
function applyPackageConfig(normalized, pkgName, pkg, sync, isPlugin) {
var loader = this;
var basePath = getBasePath(pkg);
// main
// NB can add a default package main convention here
if (pkgName === normalized && pkg.main)
normalized += '/' + (pkg.main.substr(0, 2) == './' ? pkg.main.substr(2) : pkg.main);
// allow for direct package name normalization with trailling "/" (no main)
if (normalized.length == pkgName.length + 1 && normalized[pkgName.length] == '/')
return normalized;
// also no submap if name is package itself (import 'pkg' -> 'path/to/pkg.js')
if (normalized.length == pkgName.length)
return normalized + (loader.defaultJSExtensions && normalized.substr(normalized.length - 3, 3) != '.js' ? '.js' : '');
// apply map, checking without then with defaultExtension
if (pkg.map) {
var subPath = '.' + normalized.substr(pkgName.length);
var map = applyMap(pkg.map, subPath) || !isPlugin && applyMap(pkg.map, (subPath += getDefaultExtension(pkg, subPath.substr(2))));
var mapped = pkg.map[map];
}
function doMap(mapped) {
// '.' as a target is the package itself (with package main check)
if (mapped == '.')
return pkgName;
// internal package map
else if (mapped.substr(0, 2) == './')
return toPackagePath(loader, pkgName, pkg, basePath, mapped.substr(2), sync, isPlugin);
// global package map
else
return (sync ? loader.normalizeSync : loader.normalize).call(loader, mapped);
}
// apply non-environment map match
if (typeof mapped == 'string')
return doMap(mapped + subPath.substr(map.length));
// sync normalize does not apply environment map
if (sync || !mapped)
return toPackagePath(loader, pkgName, pkg, basePath, normalized.substr(pkgName.length + 1), sync, isPlugin);
// environment map build support
// -> we return [package-name]#[conditional-map] ("jquery#:index.js" in example above)
// to indicate this unique conditional branch to builder in all of its possibilities
if (loader.builder)
return pkgName + '#:' + map.substr(2);
// environment map
return loader['import'](pkg.map['@env'] || '@system-env', pkgName)
.then(function(env) {
// first map condition to match is used
for (var e in mapped) {
var negate = e[0] == '~';
var value = readMemberExpression(negate ? e.substr(1) : e, env);
if (!negate && value || negate && !value)
return mapped[e] + subPath.substr(map.length);
}
})
.then(function(mapped) {
// no environment match
if (!mapped)
return toPackagePath(loader, pkgName, pkg, basePath, normalized.substr(pkgName.length + 1), sync, isPlugin);
else
return doMap(mapped);
});
}
function createPackageNormalize(normalize, sync) {
return function(name, parentName, isPlugin) {
isPlugin = isPlugin === true;
// apply contextual package map first
if (parentName)
var parentPackage = getPackage.call(this, parentName) ||
this.defaultJSExtensions && parentName.substr(parentName.length - 3, 3) == '.js' &&
getPackage.call(this, parentName.substr(0, parentName.length - 3));
if (parentPackage) {
// remove any parent package base path for normalization
var parentBasePath = getBasePath(this.packages[parentPackage]);
if (parentBasePath && parentName.substr(parentPackage.length + 1, parentBasePath.length) == parentBasePath)
parentName = parentPackage + parentName.substr(parentPackage.length + parentBasePath.length);
if (name[0] !== '.') {
var parentMap = this.packages[parentPackage].map;
if (parentMap) {
var map = applyMap(parentMap, name);
if (map) {
name = parentMap[map] + name.substr(map.length);
// relative maps are package-relative
if (name[0] === '.')
parentName = parentPackage + '/';
}
}
}
}
var defaultJSExtension = this.defaultJSExtensions && name.substr(name.length - 3, 3) != '.js';
// apply map, core, paths
var normalized = normalize.call(this, name, parentName);
// undo defaultJSExtension
if (defaultJSExtension && normalized.substr(normalized.length - 3, 3) != '.js')
defaultJSExtension = false;
if (defaultJSExtension)
normalized = normalized.substr(0, normalized.length - 3);
// if we requested a relative path, and got the package name, remove the trailing '/' to apply main
// that is normalize('pkg/') does not apply main, while normalize('./', 'pkg/') does
if (parentPackage && name[0] == '.' && normalized == parentPackage + '/')
normalized = parentPackage;
var loader = this;
function packageResolution(normalized, pkgName, pkg) {
// check if we are inside a package
pkgName = pkgName || getPackage.call(loader, normalized);
var pkg = pkg || pkgName && loader.packages[pkgName];
if (pkg)
return applyPackageConfig.call(loader, normalized, pkgName, pkg, sync, isPlugin);
else
return normalized + (defaultJSExtension ? '.js' : '');
}
// do direct resolution if sync
if (sync)
return packageResolution(normalized);
// first check if we are in a package
var pkgName = getPackage.call(this, normalized);
var pkg = pkgName && this.packages[pkgName];
// if so, and the package is configured, then do direct resolution
if (pkg && pkg.configured)
return packageResolution(normalized, pkgName, pkg);
var pkgConfigMatch = pkgConfigPathMatch(loader, normalized);
if (!pkgConfigMatch.pkgName)
return packageResolution(normalized, pkgName, pkg);
// if we're within a known package path, then halt on
// further package configuration steps from bundles and config files
return Promise.resolve(getBundleFor(loader, normalized))
// ensure that any bundles in this package are triggered, and
// all that are triggered block any further loads in the package
.then(function(bundle) {
if (bundle || pkgBundlePromises[pkgConfigMatch.pkgName]) {
var pkgBundleLoads = pkgBundlePromises[pkgConfigMatch.pkgName] = pkgBundlePromises[pkgConfigMatch.pkgName] || { bundles: [], promise: Promise.resolve() };
if (bundle && indexOf.call(pkgBundleLoads.bundles, bundle) == -1) {
pkgBundleLoads.bundles.push(bundle);
pkgBundleLoads.promise = Promise.all([pkgBundleLoads.promise, loader.load(bundle)]);
}
return pkgBundleLoads.promise;
}
})
// having loaded any bundles, attempt a package resolution now
.then(function() {
return packageResolution(normalized, pkgConfigMatch.pkgName);
})
.then(function(curResolution) {
// if that resolution is defined in the registry use it
if (curResolution in loader.defined)
return curResolution;
// otherwise revert to loading configuration dynamically
return loadPackageConfigPaths(loader, pkgConfigMatch)
.then(function() {
// before doing a final resolution
return packageResolution(normalized);
});
});
};
}
var pkgBundlePromises = {};
// check if the given normalized name matches a packageConfigPath
// if so, loads the config
var packageConfigPathsRegExps = {};
var pkgConfigPromises = {};
function pkgConfigPathMatch(loader, normalized) {
var pkgPath, pkgConfigPaths = [];
for (var i = 0; i < loader.packageConfigPaths.length; i++) {
var p = loader.packageConfigPaths[i];
var pPkgLen = Math.max(p.lastIndexOf('*') + 1, p.lastIndexOf('/'));
var match = normalized.match(packageConfigPathsRegExps[p] ||
(packageConfigPathsRegExps[p] = new RegExp('^(' + p.substr(0, pPkgLen).replace(/\*/g, '[^\\/]+') + ')(\/|$)')));
if (match && (!pkgPath || pkgPath == match[1])) {
pkgPath = match[1];
pkgConfigPaths.push(pkgPath + p.substr(pPkgLen));
}
}
return {
pkgName: pkgPath,
configPaths: pkgConfigPaths
};
}
function loadPackageConfigPaths(loader, pkgConfigMatch) {
var curPkgConfig = loader.packages[pkgConfigMatch.pkgName];
if (curPkgConfig && curPkgConfig.configured)
return Promise.resolve();
return pkgConfigPromises[pkgConfigMatch.pkgName] || (
pkgConfigPromises[pkgConfigMatch.pkgName] = Promise.resolve()
.then(function() {
var pkgConfigPromises = [];
for (var i = 0; i < pkgConfigMatch.configPaths.length; i++) (function(pkgConfigPath) {
pkgConfigPromises.push(loader['fetch']({ name: pkgConfigPath, address: pkgConfigPath, metadata: {} })
.then(function(source) {
try {
return JSON.parse(source);
}
catch(e) {
throw new Error('Invalid JSON in package configuration file ' + pkgConfigPath);
}
})
.then(function(cfg) {
// support "systemjs" prefixing
if (cfg.systemjs)
cfg = cfg.systemjs;
// meta backwards compatibility
if (cfg.meta) {
cfg.modules = cfg.meta;
warn.call(loader, 'Package config file ' + pkgConfigPath + ' is configured with meta, which is deprecated as it has been renamed to modules.');
}
// remove any non-system properties if generic config file (eg package.json)
for (var p in cfg) {
if (indexOf.call(packageProperties, p) == -1)
delete cfg[p];
}
// support main array
if (cfg.main instanceof Array)
cfg.main = cfg.main[0];
// deeply-merge (to first level) config with any existing package config
if (curPkgConfig)
extendMeta(cfg, curPkgConfig);
// support external depCache
if (cfg.depCache)
for (var d in cfg.depCache) {
if (d.substr(0, 2) == './')
continue;
var dNormalized = loader.normalizeSync(d);
loader.depCache[dNormalized] = (loader.depCache[dNormalized] || []).concat(cfg.depCache[d]);
}
curPkgConfig = loader.packages[pkgConfigMatch.pkgName] = cfg;
}));
})(pkgConfigMatch.configPaths[i]);
return Promise.all(pkgConfigPromises);
})
);
}
SystemJSLoader.prototype.normalizeSync = SystemJSLoader.prototype.normalize;
hook('normalizeSync', function(normalize) {
return createPackageNormalize(normalize, true);
});
hook('normalize', function(normalize) {
return createPackageNormalize(normalize, false);
});
function getMetaMatches(pkgMeta, pkgName, 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)) {
matchFn(module, pkgMeta[dotRel + module], module.split('/').length);
}
}
// exact meta
var exactMeta = 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.call(loader, load.name);
if (pkgName) {
var pkg = loader.packages[pkgName];
var basePath = getBasePath(pkg);
var subPath = load.name.substr(pkgName.length + basePath.length + 1);
// format
if (pkg.format)
load.metadata.format = load.metadata.format || pkg.format;
// depCache for packages
if (pkg.depCache) {
for (var d in pkg.depCache) {
if (d != './' + subPath)
continue;
var deps = pkg.depCache[d];
for (var i = 0; i < deps.length; i++)
loader['import'](deps[i], pkgName + '/');
}
}
var meta = {};
if (pkg.modules) {
var bestDepth = 0;
getMetaMatches(pkg.modules, pkgName, subPath, function(metaPattern, matchMeta, matchDepth) {
if (matchDepth > bestDepth)
bestDepth = matchDepth;
extendMeta(meta, matchMeta, matchDepth && bestDepth > matchDepth);
});
// allow alias and loader to be package-relative
if (meta.alias && meta.alias.substr(0, 2) == './')
meta.alias = pkgName + meta.alias.substr(1);
if (meta.loader && meta.loader.substr(0, 2) == './')
meta.loader = pkgName + meta.loader.substr(1);
extendMeta(load.metadata, meta);
}
}
return address;
});
};
});
})();