@stryker-mutator/core
Version:
The extendable JavaScript mutation testing framework
153 lines • 6.2 kB
JavaScript
import path from 'path';
import fs from 'fs';
import { fileURLToPath, pathToFileURL, URL } from 'url';
import { tokens, commonTokens, } from '@stryker-mutator/api/plugin';
import { notEmpty, propertyPath } from '@stryker-mutator/util';
import { fileUtils } from '../utils/file-utils.js';
import { defaultOptions } from '../config/options-validator.js';
const IGNORED_PACKAGES = ['core', 'api', 'util', 'instrumenter'];
/**
* Can resolve modules and pull them into memory
*/
export class PluginLoader {
log;
static inject = tokens(commonTokens.logger);
constructor(log) {
this.log = log;
}
/**
* Loads plugins based on configured plugin descriptors.
* A plugin descriptor can be:
* * A full url: "file:///home/nicojs/github/my-plugin.js"
* * An absolute file path: "/home/nicojs/github/my-plugin.js"
* * A relative path: "./my-plugin.js"
* * A bare import expression: "@stryker-mutator/karma-runner"
* * A simple glob expression (only wild cards are supported): "@stryker-mutator/*"
*/
async load(pluginDescriptors) {
const pluginModules = await this.resolvePluginModules(pluginDescriptors);
const loadedPluginModules = (await Promise.all(pluginModules.map(async (moduleName) => {
const plugin = await this.loadPlugin(moduleName);
return {
...plugin,
moduleName,
};
}))).filter(notEmpty);
const result = {
schemaContributions: [],
pluginsByKind: new Map(),
pluginModulePaths: [],
};
loadedPluginModules.forEach(({ plugins, schemaContribution, moduleName }) => {
if (plugins) {
result.pluginModulePaths.push(moduleName);
plugins.forEach((plugin) => {
const pluginsForKind = result.pluginsByKind.get(plugin.kind);
if (pluginsForKind) {
pluginsForKind.push(plugin);
}
else {
result.pluginsByKind.set(plugin.kind, [plugin]);
}
});
}
if (schemaContribution) {
result.schemaContributions.push(schemaContribution);
}
});
return result;
}
async resolvePluginModules(pluginDescriptors) {
return (await Promise.all(pluginDescriptors.map(async (pluginExpression) => {
if (pluginExpression.includes('*')) {
return await this.globPluginModules(pluginExpression);
}
else if (path.isAbsolute(pluginExpression) ||
pluginExpression.startsWith('.')) {
return pathToFileURL(path.resolve(pluginExpression)).toString();
}
else {
// Bare plugin expression like "@stryker-mutator/mocha-runner" (or file URL)
return pluginExpression;
}
})))
.filter(notEmpty)
.flat();
}
async globPluginModules(pluginExpression) {
const { org, pkg } = parsePluginExpression(pluginExpression);
const pluginDirectory = path.resolve(fileURLToPath(new URL('../../../../../', import.meta.url)), org);
const regexp = new RegExp('^' + pkg.replace('*', '.*'));
this.log.debug('Loading %s from %s', pluginExpression, pluginDirectory);
const plugins = (await fs.promises.readdir(pluginDirectory))
.filter((pluginName) => !IGNORED_PACKAGES.includes(pluginName) && regexp.test(pluginName))
.map((pluginName) => `${org.length ? `${org}/` : ''}${pluginName}`);
if (plugins.length === 0 &&
!defaultOptions.plugins.includes(pluginExpression)) {
this.log.warn('Expression "%s" not resulted in plugins to load.', pluginExpression);
}
plugins.forEach((plugin) => this.log.debug('Loading plugin "%s" (matched with expression %s)', plugin, pluginExpression));
return plugins;
}
async loadPlugin(descriptor) {
this.log.debug('Loading plugin %s', descriptor);
try {
const module = await fileUtils.importModule(descriptor);
const plugins = isPluginModule(module)
? module.strykerPlugins
: undefined;
const schemaContribution = hasValidationSchemaContribution(module)
? module.strykerValidationSchema
: undefined;
if (plugins ?? schemaContribution) {
return {
plugins,
schemaContribution,
};
}
else {
this.log.warn('Module "%s" did not contribute a StrykerJS plugin. It didn\'t export a "%s" or "%s".', descriptor, propertyPath()('strykerPlugins'), propertyPath()('strykerValidationSchema'));
}
}
catch (e) {
if (e.code === 'ERR_MODULE_NOT_FOUND' &&
e.message.indexOf(descriptor) !== -1) {
this.log.warn('Cannot find plugin "%s".\n Did you forget to install it ?', descriptor);
}
else {
this.log.warn('Error during loading "%s" plugin:\n %s', descriptor, e.message);
}
}
return;
}
}
/**
* Distills organization name from a package expression.
* @example
* '@stryker-mutator/core' => { org: '@stryker-mutator', 'core' }
* 'glob' => { org: '', 'glob' }
*/
function parsePluginExpression(pluginExpression) {
const parts = pluginExpression.split('/');
if (parts.length > 1) {
return {
org: parts.slice(0, parts.length - 1).join('/'),
pkg: parts[parts.length - 1],
};
}
else {
return {
org: '',
pkg: parts[0],
};
}
}
function isPluginModule(module) {
const pluginModule = module;
return Array.isArray(pluginModule.strykerPlugins);
}
function hasValidationSchemaContribution(module) {
const pluginModule = module;
return typeof pluginModule.strykerValidationSchema === 'object';
}
//# sourceMappingURL=plugin-loader.js.map