-
Notifications
You must be signed in to change notification settings - Fork 250
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(esm config): support config file as pure esm (#3432)
Support your `stryker.conf.js` file to be a pure ECMAScript Module (ESM). Either using the `.mjs` extension, or `.js` if you use `{ "type": module" }` in your `package.json`. Example: ```js // @ts-check /** @type {import('@stryker-mutator/api/core').PartialStrykerOptions} */ const config = { // Your config here! }; export default config; ``` More changes: * Support `stryker.conf.cjs` for completeness sake. That way you can keep using `cjs` for your stryker config if you prefer. * Remove the workaround for exporting a `function` from your `stryker.conf.js` file. BREAKING CHANGE: Exporting a function (using `module.exports = function(config) {}`) from your `stryker.conf.js` file is no longer supported. This was already deprecated but now will give an error.
- Loading branch information
Showing
54 changed files
with
681 additions
and
283 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
{ | ||
"mutate": ["src/*.js"], | ||
"testRunner": "karma", | ||
"reporters": ["json", "clear-text", "html", "event-recorder"], | ||
"karma": { | ||
"config": { | ||
"frameworks": ["mocha", "chai"], | ||
"files": ["src/*.js", "test/*.js"] | ||
} | ||
}, | ||
"timeoutMS": 60000, | ||
"concurrency": 2, | ||
"coverageAnalysis": "perTest", | ||
"plugins": ["@stryker-mutator/karma-runner"] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
/* eslint-disable import/no-default-export */ | ||
// @ts-check | ||
import fs from 'fs'; | ||
import { URL } from 'url'; | ||
|
||
const settings = JSON.parse(fs.readFileSync(new URL('../../stryker.parent.conf.json', import.meta.url), 'utf-8')); | ||
settings.dashboard.module = import.meta.url.split('/').slice(-2)[0]; | ||
/** | ||
* @type {import('../api/dist/src/core/index.js').PartialStrykerOptions} | ||
*/ | ||
export default settings; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export const DEFAULT_CONFIG_FILE_BASE_NAME = 'stryker.conf'; | ||
export const SUPPORTED_CONFIG_FILE_EXTENSIONS = Object.freeze(['.json', '.js', '.mjs', '.cjs'] as const); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,94 +1,146 @@ | ||
import fs from 'fs'; | ||
import path from 'path'; | ||
import { createRequire } from 'module'; | ||
import { pathToFileURL } from 'url'; | ||
|
||
import { PartialStrykerOptions, StrykerOptions } from '@stryker-mutator/api/core'; | ||
import { Logger } from '@stryker-mutator/api/logging'; | ||
import { commonTokens, tokens } from '@stryker-mutator/api/plugin'; | ||
import { deepMerge } from '@stryker-mutator/util'; | ||
import { deepMerge, I } from '@stryker-mutator/util'; | ||
|
||
import { coreTokens } from '../di/index.js'; | ||
import { ConfigError } from '../errors.js'; | ||
import { fileUtils } from '../utils/file-utils.js'; | ||
|
||
import { defaultOptions, OptionsValidator } from './options-validator.js'; | ||
import { createConfig } from './create-config.js'; | ||
import { OptionsValidator } from './options-validator.js'; | ||
import { DEFAULT_CONFIG_FILE_BASE_NAME, SUPPORTED_CONFIG_FILE_EXTENSIONS } from './config-file-formats.js'; | ||
|
||
export const CONFIG_SYNTAX_HELP = ` | ||
Example of how a config file should look: | ||
/** | ||
* @type {import('@stryker-mutator/api/core').StrykerOptions} | ||
*/ | ||
export default { | ||
// You're options here! | ||
} | ||
Or using commonjs: | ||
/** | ||
* @type {import('@stryker-mutator/api/core').StrykerOptions} | ||
*/ | ||
module.exports = { | ||
// You're options here! | ||
}`.trim(); | ||
} | ||
const DEFAULT_CONFIG_FILE = 'stryker.conf'; | ||
See https://stryker-mutator.io/docs/stryker-js/config-file for more information.`.trim(); | ||
|
||
export class ConfigReader { | ||
public static inject = tokens(commonTokens.logger, coreTokens.optionsValidator); | ||
constructor(private readonly log: Logger, private readonly validator: OptionsValidator) {} | ||
constructor(private readonly log: Logger, private readonly validator: I<OptionsValidator>) {} | ||
|
||
public async readConfig(cliOptions: PartialStrykerOptions): Promise<StrykerOptions> { | ||
const configModule = this.loadConfigModule(cliOptions); | ||
let options: StrykerOptions; | ||
if (typeof configModule === 'function') { | ||
this.log.warn( | ||
'Usage of `module.exports = function(config) {}` is deprecated. Please use `module.export = {}` or a "stryker.conf.json" file. For more details, see https://stryker-mutator.io/blog/2020-03-11/stryker-version-3#new-config-format' | ||
); | ||
options = defaultOptions(); | ||
configModule(createConfig(options)); | ||
} else { | ||
this.validator.validate(configModule); | ||
options = configModule; | ||
} | ||
const options = await this.loadOptionsFromConfigFile(cliOptions); | ||
|
||
// merge the config from config file and cliOptions (precedence) | ||
deepMerge(options, cliOptions); | ||
this.validator.validate(options); | ||
if (this.log.isDebugEnabled()) { | ||
this.log.debug(`Loaded config: ${JSON.stringify(options, null, 2)}`); | ||
} | ||
return options; | ||
} | ||
|
||
private loadConfigModule(cliOptions: PartialStrykerOptions): PartialStrykerOptions | ((options: StrykerOptions) => void) { | ||
let configModule: PartialStrykerOptions | ((config: StrykerOptions) => void) = {}; | ||
const require = createRequire(import.meta.url); | ||
|
||
if (!cliOptions.configFile) { | ||
try { | ||
const configFile = require.resolve(path.resolve(`./${DEFAULT_CONFIG_FILE}`)); | ||
this.log.info(`Using ${path.basename(configFile)}`); | ||
cliOptions.configFile = configFile; | ||
} catch (e) { | ||
this.log.info('No config file specified. Running with command line arguments.'); | ||
this.log.info('Use `stryker init` command to generate your config file.'); | ||
} | ||
private async loadOptionsFromConfigFile(cliOptions: PartialStrykerOptions): Promise<PartialStrykerOptions> { | ||
const configFile = await this.findConfigFile(cliOptions.configFile); | ||
if (!configFile) { | ||
this.log.info('No config file specified. Running with command line arguments.'); | ||
this.log.info('Use `stryker init` command to generate your config file.'); | ||
return {}; | ||
} | ||
this.log.debug(`Loading config from ${configFile}`); | ||
|
||
if (path.extname(configFile).toLocaleLowerCase() === '.json') { | ||
return this.readJsonConfig(configFile); | ||
} else { | ||
return this.importJSConfig(configFile); | ||
} | ||
} | ||
|
||
if (typeof cliOptions.configFile === 'string') { | ||
this.log.debug(`Loading config ${cliOptions.configFile}`); | ||
const configFile = this.resolveConfigFile(cliOptions.configFile); | ||
try { | ||
// eslint-disable-next-line @typescript-eslint/no-require-imports | ||
configModule = require(configFile); | ||
} catch (error: unknown) { | ||
this.log.info('Stryker can help you setup a `stryker.conf` file for your project.'); | ||
this.log.info("Please execute `stryker init` in your project's root directory."); | ||
throw new ConfigError('Invalid config file', error); | ||
private async findConfigFile(configFileName: unknown): Promise<string | undefined> { | ||
if (typeof configFileName === 'string') { | ||
if (await fileUtils.exists(configFileName)) { | ||
return configFileName; | ||
} else { | ||
throw new ConfigReaderError('File does not exist!', configFileName); | ||
} | ||
if (typeof configModule !== 'function' && typeof configModule !== 'object') { | ||
this.log.fatal('Config file must export an object!\n' + CONFIG_SYNTAX_HELP); | ||
throw new ConfigError('Config file must export an object!'); | ||
} | ||
const candidates = SUPPORTED_CONFIG_FILE_EXTENSIONS.map((ext) => `${DEFAULT_CONFIG_FILE_BASE_NAME}${ext}`); | ||
for (const candidate of candidates) { | ||
if (await fileUtils.exists(candidate)) { | ||
return candidate; | ||
} | ||
} | ||
return undefined; | ||
} | ||
|
||
private async readJsonConfig(configFile: string): Promise<PartialStrykerOptions> { | ||
const fileContent = await fs.promises.readFile(configFile, 'utf-8'); | ||
try { | ||
return JSON.parse(fileContent); | ||
} catch (err) { | ||
throw new ConfigReaderError('File contains invalid JSON', configFile, err); | ||
} | ||
} | ||
|
||
private async importJSConfig(configFile: string): Promise<PartialStrykerOptions> { | ||
const importedModule = await this.importJSConfigModule(configFile); | ||
|
||
return configModule; | ||
if (this.hasDefaultExport(importedModule)) { | ||
const maybeOptions = importedModule.default; | ||
if (typeof maybeOptions !== 'object') { | ||
if (typeof maybeOptions === 'function') { | ||
this.log.fatal( | ||
`Invalid config file. Exporting a function is no longer supported. Please export an object with your configuration instead, or use a "stryker.conf.json" file.\n${CONFIG_SYNTAX_HELP}` | ||
); | ||
} else { | ||
this.log.fatal(`Invalid config file. It must export an object, found a "${typeof maybeOptions}"!\n${CONFIG_SYNTAX_HELP}`); | ||
} | ||
throw new ConfigReaderError('Default export of config file must be an object!', configFile); | ||
} | ||
if (!maybeOptions || !Object.keys(maybeOptions).length) { | ||
this.log.warn(`Stryker options were empty. Did you forget to export options from ${configFile}?`); | ||
} | ||
|
||
return { ...maybeOptions } as PartialStrykerOptions; | ||
} else { | ||
this.log.fatal(`Invalid config file. It is missing a default export. ${describeNamedExports()}\n${CONFIG_SYNTAX_HELP}`); | ||
throw new ConfigReaderError('Config file must have a default export!', configFile); | ||
|
||
function describeNamedExports() { | ||
const namedExports: string[] = (typeof importedModule === 'object' && Object.keys(importedModule ?? {})) || []; | ||
if (namedExports.length === 0) { | ||
return "In fact, it didn't export anything."; | ||
} else { | ||
return `Found named export(s): ${namedExports.map((name) => `"${name}"`).join(', ')}.`; | ||
} | ||
} | ||
} | ||
} | ||
|
||
private resolveConfigFile(configFileName: string) { | ||
const configFile = path.resolve(configFileName); | ||
private async importJSConfigModule(configFile: string): Promise<unknown> { | ||
try { | ||
const require = createRequire(import.meta.url); | ||
return require.resolve(configFile); | ||
} catch { | ||
throw new ConfigError(`File ${configFile} does not exist!`); | ||
return await fileUtils.importModule(pathToFileURL(path.resolve(configFile)).toString()); | ||
} catch (err) { | ||
throw new ConfigReaderError('Error during import', configFile, err); | ||
} | ||
} | ||
|
||
private hasDefaultExport(importedModule: unknown): importedModule is { default: unknown } { | ||
return importedModule && typeof importedModule === 'object' && 'default' in importedModule ? true : false; | ||
} | ||
} | ||
|
||
export class ConfigReaderError extends ConfigError { | ||
constructor(message: string, configFileName: string, cause?: unknown) { | ||
super(`Invalid config file "${configFileName}". ${message}`, cause); | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.