karma-systemjs
Version:
A Karma plugin. Adapter for SystemJS module loader.
274 lines (250 loc) • 10 kB
JavaScript
var path = require('path');
var fs = require('fs');
var _ = require('lodash');
var Minimatch = require('minimatch').Minimatch;
/**
* Helper for mapping include file paths to karma file patterns - served, included, but not watched
* @param path {string}
* @returns {object}
*/
var createIncludePattern = function(path) {
return {
pattern: path,
included: true,
served: true,
watched: false
};
};
/**
* Resolve paths for dependencies now
* @param moduleName {string}
* @param relativePath {string}
* @returns {string}
*/
var getDependencyPath = function(moduleName, relativePath) {
try {
return path.join(path.dirname(require.resolve(moduleName)), relativePath);
} catch (e) {
if (e.code === 'MODULE_NOT_FOUND' && e.message.indexOf(moduleName) !== -1) {
console.warn('Cannot find "%s".\n Did you forget to install it ?\n' +
' npm install %s --save-dev', moduleName, moduleName);
} else {
console.warn('Error during loading "%s":\n %s', moduleName, e.message);
}
}
};
/**
* Loads up a SystemJS config file and returns the configuration
* Taken from how systemjs-builder loads config files inside node
* @param filePath {string}
* @returns {object}
*/
var readConfigFile = function(filePath) {
var curSystem = global.System;
var fileConfig = {};
global.System = {
config: function(cfg) {
_.merge(fileConfig, cfg);
}
};
// jshint evil:true
new Function(fs.readFileSync(filePath).toString()).call(global);
global.System = curSystem;
return fileConfig;
};
/**
* Returns a dependency path based on 'paths' configuration, or node_modules/ lookup
* @param systemjsConfig {object} The SystemJS config object
* @param basePath {string} Path that SystemJS paths are relative to
* @param systemModuleName {string} Name of the module as the SystemJS config knows it.
* @param npmModuleName {string} Name of module as npm knows it.
* @param npmPath {string} Path to use relative to a `require.resolve()` call
* @returns {string}
*/
var pathOrNpm = function(systemjsConfig, basePath, systemModuleName, npmModuleName, npmPath) {
var filePath = getModulePath(systemjsConfig, basePath, systemModuleName);
if (filePath) {
return filePath;
} else {
console.warn('[WARNING] Looking up paths with require.resolve() is deprecated.\n' +
'Please add "' + systemModuleName + '" to your SystemJS config paths.');
return getDependencyPath(npmModuleName, npmPath);
}
};
/**
* Returns a dependency path based on 'paths' or 'map' configuration
* @param systemjsConfig {object} The SystemJS config object
* @param basePath {string} Path that SystemJS paths are relative to
* @param systemModuleName {string} Name of the module as the SystemJS config knows it.
* @returns {string|undefined}
*/
var getModulePath = function(systemjsConfig, basePath, systemModuleName) {
var filePath = (systemjsConfig.paths && systemjsConfig.paths[systemModuleName]) || (systemjsConfig.map && systemjsConfig.map[systemModuleName]);
if (filePath) {
return path.join(basePath, systemjsConfig.baseURL || '', filePath);
}
};
/**
* Returns the path to the transpiler.
* @param config {object} SystemJS config
* @param basePath {string}
* @returns {string}
*/
var getTranspilerPath = function(config, basePath) {
// Path should come from SystemJS, but for backwards compatibility will also check for a known npm path
switch (config.transpiler) {
case 'none':
case false:
case null: // If 'none', false, or null, no transpiler in use
return '';
case 'babel':
return pathOrNpm(config, basePath, 'babel', 'babel-core', '/lib/api/browser.js');
case undefined: // Traceur is still the default transpiler if undefined
case 'traceur':
return pathOrNpm(config, basePath, 'traceur', 'traceur', '/../../bin/traceur.js');
case 'typescript':
return pathOrNpm(config, basePath, 'typescript', 'typescript', '/typescript.js');
default:
return pathOrNpm(config, basePath, config.transpiler, config.transpiler, config.transpiler + '.js');
}
};
/**
* Returns the list of '{included: true}' file patterns before setting them to '{included: false}'
* @param fileList {object[]}
* @param basePath {string}
* @returns {string[]}
*/
var processIncludedPatterns = function(fileList, basePath) {
var included = _.filter(fileList, {included: true});
_.forEach(included, function (file) {
file.included = false;
});
return _.map(included, function(file) {
// Rather than make Minimatch available in the browser,
// it's simpler to use it within Karma and extract the RegExps as strings.
// Also take into account the '/base' or '/absolute' path that Karma serves files from
var path = !basePath || file.pattern.indexOf(basePath) === 0 ?
'/base' + file.pattern.replace(basePath, '') :
'/absolute' + file.pattern;
var regexString = (new Minimatch(path)).makeRe().toString();
return regexString.substring(1, regexString.length - 1); // Remove the "/" from the start and end, so it'll fit into 'new RegExp()'
});
};
/**
* Run during karma initialisation.
* Alters the karma configuration to use SystemJS.
* @param config {object}
*/
var initSystemjs = function(config) {
// Final files array should look like this:
// - SystemJS libraries - included
// - SystemJS config - included & watched
// - 'files' - served, watched, and marked to be called with 'System.import()' (if {included: true})
// - 'systemjs.serveFiles' - served only
// - Plugin adapter - included
// Create list of file patterns to load with 'System.import()'
var importPatterns = processIncludedPatterns(config.files, config.basePath);
var kSystemjsConfig = config.systemjs || {};
kSystemjsConfig.config = kSystemjsConfig.config || {};
var basePath = (config.basePath || '.') + '/';
// If there's an external SystemJS configuration file...
if (kSystemjsConfig.configFile) {
// Load it, and merge it with the config
// karma-systemjs.config should take precedence over external SystemJS configuration file
var cfgPath = basePath + kSystemjsConfig.configFile;
var kConfig = readConfigFile(cfgPath);
// Absolute paths for modules should be moved to /base, where Karma serves them
['map', 'paths'].forEach(function(cfgKey) {
if (typeof kConfig[cfgKey] === 'object') {
Object.keys(kConfig[cfgKey]).forEach(function(module) {
if (kConfig[cfgKey][module][0] === '/') {
kConfig[cfgKey][module] = '/base' + kConfig[cfgKey][module];
}
});
}
});
// Merge the SystemJS config file with the SystemJS config in the karma config
_.merge(kConfig, kSystemjsConfig.config, function(objectValue, sourceValue) {
// Overwrite array values - don't merge them
if (_.isArray(sourceValue)) {
return sourceValue;
}
});
kSystemjsConfig.config = kConfig;
}
// Resolve the paths for systemjs and it's dependencies, then adds to start of config.files.
// Ignores dependencies if not found in SystemJS config
var systemjsPath = pathOrNpm(kSystemjsConfig.config, basePath,
'systemjs', 'systemjs', '/dist/system.src.js');
config.files.unshift(createIncludePattern(systemjsPath));
var polyfillsPath = getModulePath(kSystemjsConfig.config, basePath, 'system-polyfills');
if (polyfillsPath) {
config.files.unshift(createIncludePattern(polyfillsPath));
}
var es6LoaderPath = getModulePath(kSystemjsConfig.config, basePath, 'es6-module-loader');
if (es6LoaderPath) {
config.files.unshift(createIncludePattern(es6LoaderPath));
}
// If a transpiler is being used, serve it
var transpilerPath = getTranspilerPath(kSystemjsConfig.config, basePath);
if (transpilerPath) {
// Don't watch, since this file should never change
// Don't include, as SystemJS will need to load it
config.files.unshift({
pattern: transpilerPath,
included: false,
served: true,
watched: false
});
}
// system.js-0.16+ uses Function.prototype.bind, which PhantomJS does not support.
if (config.browsers && config.browsers.indexOf('PhantomJS') !== -1) {
var phantomjsPolyfillPath = getModulePath(kSystemjsConfig.config, basePath, 'phantomjs-polyfill');
if (phantomjsPolyfillPath) {
config.files.unshift(
createIncludePattern(phantomjsPolyfillPath)
);
}
}
// Adds file patterns from config.systemjs.serveFiles to config.files, set to be served but not included
if (kSystemjsConfig.serveFiles) {
kSystemjsConfig.serveFiles.forEach(function(pathOrPattern) {
if (typeof pathOrPattern === 'object') {
config.files.push(pathOrPattern);
} else {
config.files.push({
pattern: basePath + pathOrPattern,
included: false,
served: true,
watched: true
});
}
});
}
// Add file patterns to always be included back into files
if (kSystemjsConfig.includeFiles) {
var filesToInclude = kSystemjsConfig.includeFiles.map(function(pathOrPattern) {
if (typeof pathOrPattern === 'object') {
return pathOrPattern;
} else {
return createIncludePattern(basePath + pathOrPattern);
}
});
filesToInclude.reverse().forEach(function(file) {
config.files.unshift(file);
});
}
// Adds karma-systemjs adapter.js to end of config.files
config.files.push(createIncludePattern(path.join(__dirname, 'adapter.js')));
// Adding configuration to be passed to the adapter running on the browser
config.client.systemjs = {
// SystemJS config needs to be converted to JSON to guarantee arrays are encoded correctly
// https://github.com/rolaveric/karma-systemjs/issues/44
config: JSON.stringify(kSystemjsConfig.config),
importPatterns: importPatterns,
strictImportSequence: kSystemjsConfig.strictImportSequence
};
};
initSystemjs.$inject = ['config'];
module.exports = initSystemjs;
;