sca-tool
Version:
Universal static code analysis tool for calculating API operation costs across multiple frameworks (NestJS, Express, Fastify)
433 lines • 16.8 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.ConfigLoader = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const editor_detector_1 = require("./editor-detector");
/**
* Configuration loader with comprehensive fallback system
*/
class ConfigLoader {
static DEFAULT_CONFIG_PATHS = [
'configs/sca.config.ts',
'configs/sca.config.js',
'configs/sca.config.json',
'sca.config.ts',
'sca.config.js',
'sca.config.json',
'sca-tool.config.json'
];
static DEFAULT_CONFIG = {
operations: {
'findMany': 1,
'findUnique': 1,
'findFirst': 1,
'create': 1,
'update': 1,
'delete': 1,
'upsert': 2,
'createMany': 2,
'updateMany': 2,
'deleteMany': 2,
'count': 1,
'aggregate': 2,
'groupBy': 2,
'get': 1,
'post': 1,
'put': 1,
'patch': 1,
'fetch': 1,
'axios': 1,
'send': 1,
'sendMail': 1,
'unknown_operation': 1
},
patterns: {
database: {
pattern: /\.(findMany|findUnique|findFirst|create|update|delete|upsert|createMany|updateMany|deleteMany|count|aggregate|groupBy)\(/,
description: 'Generic database operations'
},
http: {
pattern: /\.(get|post|put|patch|delete)\(/,
description: 'HTTP operations'
},
async_method: {
pattern: /await\s+\w+\./,
description: 'Async method calls'
},
prisma: {
pattern: /prisma\.\w+\.(findMany|findUnique|findFirst|create|update|delete|upsert|createMany|updateMany|deleteMany|count|aggregate|groupBy)/,
description: 'Prisma ORM operations'
},
custom_service: {
pattern: /\w+Service\./,
description: 'Custom service method calls'
},
email_service: {
pattern: /(EmailService|mailService)\.(send|sendMail)/,
description: 'Email service operations'
}
},
parallelExecutionPatterns: [
'Promise.all',
'Promise.allSettled',
'Promise.race',
'await Promise.all',
'await Promise.allSettled'
],
ignorePatterns: [
'console.log',
'console.error',
'console.warn',
'logger.',
'log.'
],
costMultipliers: {
loop: 1.5,
recursion: 2.0,
nested: 1.3,
conditional: 1.1
},
output: {
defaultFormat: 'text',
defaultOutputDir: './reports',
fileNaming: {
pattern: '{filename}-analysis-{timestamp}',
includeTimestamp: true
},
html: {
theme: 'light',
includeCharts: true,
title: 'API Cost Analysis Report',
footer: 'Generated by SCA-Tool - Static Code Analyzer',
openInBrowser: false
}
},
analysis: {
costThresholds: {
low: 2,
medium: 5,
high: 10
},
recommendations: {
enabled: true,
customRules: []
},
parallel: {
detectParallelOperations: true,
parallelThreshold: 2
},
methodFilters: {
minCost: 0,
maxCost: 1000,
excludePatterns: []
}
},
paths: {
configFile: 'configs/sca.config.ts',
outputDir: './reports',
tempDir: './temp',
cacheDir: './.sca-cache',
excludePatterns: [
'node_modules/**',
'dist/**',
'build/**',
'**/*.test.ts',
'**/*.spec.ts',
'**/*.d.ts'
],
includePatterns: [
'**/*.ts',
'**/*.js',
'**/*.tsx',
'**/*.jsx'
],
watch: {
debounceMs: 500,
excludePatterns: [
'node_modules/**',
'dist/**',
'build/**'
]
}
},
editor: editor_detector_1.EditorDetector.detectEditor()
};
/**
* Load configuration with comprehensive fallback system
*/
static loadConfig(customConfigPath) {
let config = this.deepClone(this.DEFAULT_CONFIG);
// Try to load custom config
const configPath = this.findConfigFile(customConfigPath);
if (configPath) {
try {
const userConfig = this.loadConfigFile(configPath);
config = this.mergeConfigs(config, userConfig);
console.log(`📋 Loaded config from: ${configPath}`);
}
catch (error) {
console.warn(`⚠️ Failed to load config from ${configPath}, using defaults:`, error instanceof Error ? error.message : String(error));
}
}
else if (customConfigPath) {
console.warn(`⚠️ Config file not found: ${customConfigPath}, using defaults`);
}
return this.validateAndNormalizeConfig(config);
}
/**
* Find the first available config file
*/
static findConfigFile(customPath) {
const pathsToTry = customPath ? [customPath] : this.DEFAULT_CONFIG_PATHS;
for (const configPath of pathsToTry) {
const fullPath = path.resolve(configPath);
if (fs.existsSync(fullPath)) {
return fullPath;
}
}
return null;
}
/**
* Load and parse config file
*/
static loadConfigFile(configPath) {
const ext = path.extname(configPath).toLowerCase();
if (ext === '.json') {
const content = fs.readFileSync(configPath, 'utf8');
return JSON.parse(content);
}
else if (ext === '.js' || ext === '.ts') {
// For TypeScript/JavaScript files, we'll try to require them
// Note: This is a simplified approach - in production you might want to use ts-node
delete require.cache[require.resolve(configPath)];
const module = require(configPath);
return module.default || module.config || module;
}
throw new Error(`Unsupported config file format: ${ext}`);
}
/**
* Deep merge two configuration objects
*/
static mergeConfigs(base, override) {
const result = this.deepClone(base);
// Merge operations
if (override.operations) {
result.operations = { ...result.operations, ...override.operations };
}
// Merge patterns
if (override.patterns) {
result.patterns = { ...result.patterns, ...override.patterns };
}
// Merge arrays
if (override.parallelExecutionPatterns) {
result.parallelExecutionPatterns = [...result.parallelExecutionPatterns, ...override.parallelExecutionPatterns];
}
if (override.ignorePatterns) {
result.ignorePatterns = [...result.ignorePatterns, ...override.ignorePatterns];
}
// Merge nested objects
if (override.costMultipliers) {
result.costMultipliers = { ...result.costMultipliers, ...override.costMultipliers };
}
if (override.output) {
result.output = this.mergeDeep(result.output, override.output);
}
if (override.analysis) {
result.analysis = this.mergeDeep(result.analysis, override.analysis);
}
if (override.paths) {
result.paths = this.mergeDeep(result.paths, override.paths);
}
if (override.editor) {
result.editor = this.mergeDeep(result.editor, override.editor);
}
if (override.methodCosts) {
result.methodCosts = { ...result.methodCosts, ...override.methodCosts };
}
return result;
}
/**
* Deep merge utility
*/
static mergeDeep(target, source) {
const result = { ...target };
for (const key in source) {
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
result[key] = this.mergeDeep(result[key] || {}, source[key]);
}
else if (source[key] !== undefined) {
result[key] = source[key];
}
}
return result;
}
/**
* Deep clone utility
*/
static deepClone(obj) {
if (obj === null || typeof obj !== 'object')
return obj;
if (obj instanceof Date)
return new Date(obj.getTime());
if (obj instanceof Array)
return obj.map(item => this.deepClone(item));
if (obj instanceof RegExp)
return new RegExp(obj);
const cloned = {};
for (const key in obj) {
cloned[key] = this.deepClone(obj[key]);
}
return cloned;
}
/**
* Validate and normalize configuration
*/
static validateAndNormalizeConfig(config) {
// Ensure required fields exist with fallbacks
config.operations = config.operations || this.DEFAULT_CONFIG.operations;
config.patterns = config.patterns || this.DEFAULT_CONFIG.patterns;
config.parallelExecutionPatterns = config.parallelExecutionPatterns || this.DEFAULT_CONFIG.parallelExecutionPatterns;
config.ignorePatterns = config.ignorePatterns || this.DEFAULT_CONFIG.ignorePatterns;
config.costMultipliers = config.costMultipliers || this.DEFAULT_CONFIG.costMultipliers;
config.output = config.output || this.DEFAULT_CONFIG.output;
config.analysis = config.analysis || this.DEFAULT_CONFIG.analysis;
config.paths = config.paths || this.DEFAULT_CONFIG.paths;
config.editor = config.editor || this.DEFAULT_CONFIG.editor;
// Normalize patterns to RegExp objects
for (const [key, pattern] of Object.entries(config.patterns)) {
if (typeof pattern.pattern === 'string') {
try {
config.patterns[key].pattern = new RegExp(pattern.pattern);
}
catch (error) {
console.warn(`⚠️ Invalid regex pattern for ${key}: ${pattern.pattern}`);
config.patterns[key].pattern = /(?:)/; // Empty regex that matches nothing
}
}
}
return config;
}
/**
* Get output configuration with fallbacks
*/
static getOutputConfig(config) {
const output = config.output || {};
const defaults = this.DEFAULT_CONFIG.output;
return {
defaultFormat: output.defaultFormat || defaults.defaultFormat,
defaultOutputDir: output.defaultOutputDir || defaults.defaultOutputDir,
fileNaming: {
pattern: output.fileNaming?.pattern || defaults.fileNaming.pattern,
includeTimestamp: output.fileNaming?.includeTimestamp ?? defaults.fileNaming.includeTimestamp
},
html: {
theme: output.html?.theme || defaults.html.theme,
includeCharts: output.html?.includeCharts ?? defaults.html.includeCharts,
customCss: output.html?.customCss || defaults.html.customCss || '',
customJs: output.html?.customJs || defaults.html.customJs || '',
title: output.html?.title || defaults.html.title,
footer: output.html?.footer || defaults.html.footer,
openInBrowser: output.html?.openInBrowser ?? defaults.html.openInBrowser
}
};
}
/**
* Get analysis configuration with fallbacks
*/
static getAnalysisConfig(config) {
const analysis = config.analysis || {};
const defaults = this.DEFAULT_CONFIG.analysis;
return {
costThresholds: {
low: analysis.costThresholds?.low || defaults.costThresholds.low,
medium: analysis.costThresholds?.medium || defaults.costThresholds.medium,
high: analysis.costThresholds?.high || defaults.costThresholds.high
},
recommendations: {
enabled: analysis.recommendations?.enabled ?? defaults.recommendations.enabled,
customRules: analysis.recommendations?.customRules || defaults.recommendations.customRules
},
parallel: {
detectParallelOperations: analysis.parallel?.detectParallelOperations ?? defaults.parallel.detectParallelOperations,
parallelThreshold: analysis.parallel?.parallelThreshold || defaults.parallel.parallelThreshold
},
methodFilters: {
minCost: analysis.methodFilters?.minCost ?? defaults.methodFilters.minCost,
maxCost: analysis.methodFilters?.maxCost || defaults.methodFilters.maxCost,
excludePatterns: analysis.methodFilters?.excludePatterns || defaults.methodFilters.excludePatterns
}
};
}
/**
* Get paths configuration with fallbacks
*/
static getPathsConfig(config) {
const paths = config.paths || {};
const defaults = this.DEFAULT_CONFIG.paths;
return {
configFile: paths.configFile || defaults.configFile,
outputDir: paths.outputDir || defaults.outputDir,
tempDir: paths.tempDir || defaults.tempDir,
cacheDir: paths.cacheDir || defaults.cacheDir,
excludePatterns: paths.excludePatterns || defaults.excludePatterns,
includePatterns: paths.includePatterns || defaults.includePatterns,
watch: {
debounceMs: paths.watch?.debounceMs || defaults.watch.debounceMs,
excludePatterns: paths.watch?.excludePatterns || defaults.watch.excludePatterns
}
};
}
/**
* Get editor configuration with fallbacks
*/
static getEditorConfig(config) {
const editor = config.editor || {};
const defaults = this.DEFAULT_CONFIG.editor;
return {
defaultEditor: editor.defaultEditor || defaults.defaultEditor,
detectedEditor: editor.detectedEditor || defaults.detectedEditor || 'code',
openInEditor: editor.openInEditor ?? defaults.openInEditor,
editorCommands: editor.editorCommands || defaults.editorCommands,
integration: {
generateVSCodeTasks: editor.integration?.generateVSCodeTasks ?? defaults.integration.generateVSCodeTasks,
generateEditorConfig: editor.integration?.generateEditorConfig ?? defaults.integration.generateEditorConfig
}
};
}
}
exports.ConfigLoader = ConfigLoader;
//# sourceMappingURL=config-loader.js.map