chrome-extension-test-framework
Version:
Universal testing framework for Chrome extensions - Fast static analysis without browser dependencies
271 lines (232 loc) • 8.38 kB
JavaScript
/**
* ConfigLoader - 設定ファイルの読み込み
*/
const fs = require('fs');
const path = require('path');
const ConfigValidator = require('./ConfigValidator');
class ConfigLoader {
constructor() {
this.supportedFormats = ['.json', '.js', '.config.js'];
this.validator = new ConfigValidator();
}
/**
* 設定ファイルを読み込み
*/
async load(configPath) {
// パスが指定されていない場合、デフォルトの設定ファイルを探す
if (!configPath) {
configPath = this.findDefaultConfig();
if (!configPath) {
return {}; // 設定ファイルが見つからない場合は空オブジェクトを返す
}
}
// 絶対パスに変換
const absolutePath = path.isAbsolute(configPath)
? configPath
: path.join(process.cwd(), configPath);
// ファイルの存在確認
if (!fs.existsSync(absolutePath)) {
throw new Error(`Config file not found: ${configPath}`);
}
// 拡張子によって読み込み方法を変える
const ext = path.extname(absolutePath);
let config;
switch (ext) {
case '.json':
config = this.loadJSON(absolutePath);
break;
case '.js':
config = this.loadJS(absolutePath);
break;
default:
throw new Error(`Unsupported config format: ${ext}`);
}
// 設定をバリデーション
const isValid = this.validator.validate(config);
if (!isValid) {
this.validator.displayResults();
throw new Error('Invalid configuration');
}
// 警告がある場合は表示
if (this.validator.warnings.length > 0) {
this.validator.displayResults();
}
return config;
}
/**
* JSONファイルを読み込み
*/
loadJSON(filePath) {
const content = fs.readFileSync(filePath, 'utf8');
return JSON.parse(content);
}
/**
* JavaScriptファイルを読み込み
*/
loadJS(filePath) {
// キャッシュをクリアして最新の設定を読み込む
delete require.cache[filePath];
return require(filePath);
}
/**
* デフォルトの設定ファイルを探す
*/
findDefaultConfig(extensionPath = process.cwd()) {
const configNames = [
'cext-test.config.js',
'cext-test.config.json',
'chrome-extension-test.config.js',
'chrome-extension-test.config.json',
'.cextrc.js',
'.cextrc.json'
];
// まず拡張機能のディレクトリ内を探す
for (const name of configNames) {
const configPath = path.join(extensionPath, name);
if (fs.existsSync(configPath)) {
console.log(`📁 Found config file: ${name} in extension directory`);
return configPath;
}
}
// 次に現在の作業ディレクトリを探す(後方互換性のため)
if (extensionPath !== process.cwd()) {
for (const name of configNames) {
const configPath = path.join(process.cwd(), name);
if (fs.existsSync(configPath)) {
console.log(`📁 Found config file: ${name} in working directory`);
return configPath;
}
}
}
return null;
}
/**
* 設定をマージ
*/
merge(baseConfig, ...configs) {
return configs.reduce((merged, config) => {
return this.deepMerge(merged, config);
}, baseConfig);
}
/**
* ディープマージ
*/
deepMerge(target, source) {
const output = Object.assign({}, target);
if (this.isObject(target) && this.isObject(source)) {
Object.keys(source).forEach(key => {
if (this.isObject(source[key])) {
if (!(key in target)) {
Object.assign(output, { [key]: source[key] });
} else {
output[key] = this.deepMerge(target[key], source[key]);
}
} else {
Object.assign(output, { [key]: source[key] });
}
});
}
return output;
}
/**
* オブジェクトかどうか判定
*/
isObject(item) {
return item && typeof item === 'object' && !Array.isArray(item);
}
/**
* 環境変数から設定を読み込み
*/
loadFromEnv(prefix = 'CEXT_') {
const config = {};
Object.keys(process.env).forEach(key => {
if (key.startsWith(prefix)) {
const configKey = key
.substring(prefix.length)
.toLowerCase()
.replace(/_/g, '.');
// ドット記法をオブジェクトに変換
this.setNestedProperty(config, configKey, process.env[key]);
}
});
return config;
}
/**
* ネストされたプロパティを設定
*/
setNestedProperty(obj, path, value) {
const keys = path.split('.');
let current = obj;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (!(key in current)) {
current[key] = {};
}
current = current[key];
}
// 値の型を推測
const parsedValue = this.parseValue(value);
current[keys[keys.length - 1]] = parsedValue;
}
/**
* 値の型を推測して変換
*/
parseValue(value) {
// boolean
if (value === 'true') return true;
if (value === 'false') return false;
// number
if (/^\d+$/.test(value)) return parseInt(value, 10);
if (/^\d*\.\d+$/.test(value)) return parseFloat(value);
// array
if (value.startsWith('[') && value.endsWith(']')) {
try {
return JSON.parse(value);
} catch {
// JSONパースに失敗した場合はカンマ区切りとして扱う
return value.slice(1, -1).split(',').map(s => s.trim());
}
}
// object
if (value.startsWith('{') && value.endsWith('}')) {
try {
return JSON.parse(value);
} catch {
// JSONパースに失敗した場合は文字列として扱う
}
}
return value;
}
/**
* 設定を検証
*/
validate(config, schema) {
// 簡易的な検証(必要に応じて拡張可能)
const errors = [];
// 必須フィールドのチェック
if (schema.required) {
schema.required.forEach(field => {
if (!(field in config)) {
errors.push(`Missing required field: ${field}`);
}
});
}
// 型チェック
if (schema.properties) {
Object.keys(schema.properties).forEach(key => {
if (key in config) {
const expectedType = schema.properties[key].type;
const actualType = Array.isArray(config[key]) ? 'array' : typeof config[key];
if (expectedType && expectedType !== actualType) {
errors.push(`Invalid type for ${key}: expected ${expectedType}, got ${actualType}`);
}
}
});
}
return {
valid: errors.length === 0,
errors
};
}
}
module.exports = ConfigLoader;