UNPKG

5.71 kBPlain TextView Raw
1import fs from 'fs';
2import path from 'path';
3import { pathToFileURL } from 'url';
4
5import { PartialStrykerOptions, StrykerOptions } from '@stryker-mutator/api/core';
6import { Logger } from '@stryker-mutator/api/logging';
7import { commonTokens, tokens } from '@stryker-mutator/api/plugin';
8import { deepMerge, I } from '@stryker-mutator/util';
9
10import { coreTokens } from '../di/index.js';
11import { ConfigError } from '../errors.js';
12import { fileUtils } from '../utils/file-utils.js';
13
14import { OptionsValidator } from './options-validator.js';
15import { SUPPORTED_CONFIG_FILE_BASE_NAMES, SUPPORTED_CONFIG_FILE_EXTENSIONS } from './config-file-formats.js';
16
17export const CONFIG_SYNTAX_HELP = `
18Example of how a config file should look:
19/**
20 * @type {import('@stryker-mutator/api/core').StrykerOptions}
21 */
22export default {
23 // You're options here!
24}
25
26Or using commonjs:
27/**
28 * @type {import('@stryker-mutator/api/core').StrykerOptions}
29 */
30module.exports = {
31 // You're options here!
32}
33
34See https://stryker-mutator.io/docs/stryker-js/config-file for more information.`.trim();
35
36export class ConfigReader {
37 public static inject = tokens(commonTokens.logger, coreTokens.optionsValidator);
38 constructor(private readonly log: Logger, private readonly validator: I<OptionsValidator>) {}
39
40 public async readConfig(cliOptions: PartialStrykerOptions): Promise<StrykerOptions> {
41 const options = await this.loadOptionsFromConfigFile(cliOptions);
42
43 // merge the config from config file and cliOptions (precedence)
44 deepMerge(options, cliOptions);
45 this.validator.validate(options);
46 if (this.log.isDebugEnabled()) {
47 this.log.debug(`Loaded config: ${JSON.stringify(options, null, 2)}`);
48 }
49 return options;
50 }
51
52 private async loadOptionsFromConfigFile(cliOptions: PartialStrykerOptions): Promise<PartialStrykerOptions> {
53 const configFile = await this.findConfigFile(cliOptions.configFile);
54 if (!configFile) {
55 this.log.info('No config file specified. Running with command line arguments.');
56 this.log.info('Use `stryker init` command to generate your config file.');
57 return {};
58 }
59 this.log.debug(`Loading config from ${configFile}`);
60
61 if (path.extname(configFile).toLocaleLowerCase() === '.json') {
62 return this.readJsonConfig(configFile);
63 } else {
64 return this.importJSConfig(configFile);
65 }
66 }
67
68 private async findConfigFile(configFileName: unknown): Promise<string | undefined> {
69 if (typeof configFileName === 'string') {
70 if (await fileUtils.exists(configFileName)) {
71 return configFileName;
72 } else {
73 throw new ConfigReaderError('File does not exist!', configFileName);
74 }
75 }
76 for (const file of SUPPORTED_CONFIG_FILE_BASE_NAMES) {
77 for (const ext of SUPPORTED_CONFIG_FILE_EXTENSIONS) {
78 if (await fileUtils.exists(`${file}${ext}`)) {
79 return `${file}${ext}`;
80 }
81 }
82 }
83 return undefined;
84 }
85
86 private async readJsonConfig(configFile: string): Promise<PartialStrykerOptions> {
87 const fileContent = await fs.promises.readFile(configFile, 'utf-8');
88 try {
89 return JSON.parse(fileContent);
90 } catch (err) {
91 throw new ConfigReaderError('File contains invalid JSON', configFile, err);
92 }
93 }
94
95 private async importJSConfig(configFile: string): Promise<PartialStrykerOptions> {
96 const importedModule = await this.importJSConfigModule(configFile);
97
98 if (this.hasDefaultExport(importedModule)) {
99 const maybeOptions = importedModule.default;
100 if (typeof maybeOptions !== 'object') {
101 if (typeof maybeOptions === 'function') {
102 this.log.fatal(
103 `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}`
104 );
105 } else {
106 this.log.fatal(`Invalid config file. It must export an object, found a "${typeof maybeOptions}"!\n${CONFIG_SYNTAX_HELP}`);
107 }
108 throw new ConfigReaderError('Default export of config file must be an object!', configFile);
109 }
110 if (!maybeOptions || !Object.keys(maybeOptions).length) {
111 this.log.warn(`Stryker options were empty. Did you forget to export options from ${configFile}?`);
112 }
113
114 return { ...maybeOptions } as PartialStrykerOptions;
115 } else {
116 this.log.fatal(`Invalid config file. It is missing a default export. ${describeNamedExports()}\n${CONFIG_SYNTAX_HELP}`);
117 throw new ConfigReaderError('Config file must have a default export!', configFile);
118
119 function describeNamedExports() {
120 const namedExports: string[] = (typeof importedModule === 'object' && Object.keys(importedModule ?? {})) || [];
121 if (namedExports.length === 0) {
122 return "In fact, it didn't export anything.";
123 } else {
124 return `Found named export(s): ${namedExports.map((name) => `"${name}"`).join(', ')}.`;
125 }
126 }
127 }
128 }
129
130 private async importJSConfigModule(configFile: string): Promise<unknown> {
131 try {
132 return await fileUtils.importModule(pathToFileURL(path.resolve(configFile)).toString());
133 } catch (err) {
134 throw new ConfigReaderError('Error during import', configFile, err);
135 }
136 }
137
138 private hasDefaultExport(importedModule: unknown): importedModule is { default: unknown } {
139 return importedModule && typeof importedModule === 'object' && 'default' in importedModule ? true : false;
140 }
141}
142
143export class ConfigReaderError extends ConfigError {
144 constructor(message: string, configFileName: string, cause?: unknown) {
145 super(`Invalid config file "${configFileName}". ${message}`, cause);
146 }
147}
148
\No newline at end of file