typescript-express-starter
Version:
Quick and Easy TypeScript Express Starter
365 lines (304 loc) โข 11.6 kB
JavaScript
/**
* Performance Profiler - Benchmark and optimize project performance
*/
import { updateProjectMetadata, readProjectMetadata } from './project-health.js';
import { BENCHMARKS } from './presets.js';
import chalk from 'chalk';
import fs from 'fs';
import path from 'path';
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
/**
* Measure build performance
*/
async function measureBuildPerformance(projectPath) {
console.log(chalk.blue('โฑ๏ธ Measuring build performance...'));
const pkgPath = path.join(projectPath, 'package.json');
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
if (!pkg.scripts || !pkg.scripts.build) {
throw new Error('Build script not found in package.json');
}
const buildScript = pkg.scripts.build;
const startTime = Date.now();
try {
// Clean previous build
const distPath = path.join(projectPath, 'dist');
if (fs.existsSync(distPath)) {
fs.rmSync(distPath, { recursive: true, force: true });
}
// Run build
await execAsync('npm run build', { cwd: projectPath });
const endTime = Date.now();
const buildTime = (endTime - startTime) / 1000;
// Measure bundle size
let bundleSize = 0;
if (fs.existsSync(distPath)) {
const files = fs.readdirSync(distPath, { recursive: true });
files.forEach((file) => {
if (typeof file === 'string') {
const filePath = path.join(distPath, file);
if (fs.statSync(filePath).isFile()) {
bundleSize += fs.statSync(filePath).size;
}
}
});
}
console.log(chalk.green(` โ
Build completed: ${buildTime.toFixed(2)}s`));
console.log(chalk.gray(` ๐ฆ Bundle size: ${(bundleSize / 1024).toFixed(2)} KB`));
return {
buildTime: `${buildTime.toFixed(2)}s`,
bundleSize: `${(bundleSize / 1024).toFixed(2)} KB`,
tool: detectBuildTool(buildScript),
};
} catch (error) {
console.log(chalk.red(` โ Build failed: ${error.message}`));
throw error;
}
}
/**
* Measure test performance
*/
async function measureTestPerformance(projectPath) {
console.log(chalk.blue('๐งช Measuring test performance...'));
const pkgPath = path.join(projectPath, 'package.json');
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
if (!pkg.scripts || (!pkg.scripts.test && !pkg.scripts['test:ci'])) {
console.log(chalk.yellow(' โ ๏ธ Test script not found.'));
return null;
}
const testScript = pkg.scripts['test:ci'] || pkg.scripts.test;
const startTime = Date.now();
try {
// Run tests (with CI flag to avoid watch mode)
const command = testScript.includes('jest')
? 'npx jest --passWithNoTests'
: testScript.includes('vitest')
? 'npx vitest run --reporter=verbose'
: 'npm run test:ci';
const { stdout } = await execAsync(command, { cwd: projectPath });
const endTime = Date.now();
const testTime = (endTime - startTime) / 1000;
// Extract test results
const testResults = parseTestResults(stdout, testScript);
console.log(chalk.green(` โ
Tests completed: ${testTime.toFixed(2)}s`));
console.log(
chalk.gray(` ๐ Test results: ${testResults.passed}/${testResults.total} passed`),
);
return {
testTime: `${testTime.toFixed(2)}s`,
testsTotal: testResults.total,
testsPassed: testResults.passed,
tool: detectTestTool(testScript),
};
} catch (error) {
console.log(chalk.yellow(` โ ๏ธ Test execution failed (code may have issues)`));
return {
testTime: 'Failed',
testsTotal: 0,
testsPassed: 0,
tool: detectTestTool(testScript),
};
}
}
/**
* Compare with benchmark data
*/
function compareBenchmarks(projectPath, performance) {
const metadata = readProjectMetadata(projectPath);
if (!metadata) return null;
const templateName = metadata.template.name;
const templateBenchmarks = BENCHMARKS[templateName];
if (!templateBenchmarks) return null;
const comparison = {};
// Compare build performance
if (performance.build && templateBenchmarks[performance.build.tool]) {
const benchmark = templateBenchmarks[performance.build.tool];
const actualTime = parseFloat(performance.build.buildTime);
const benchmarkTime = parseFloat(benchmark.buildTime);
comparison.build = {
tool: performance.build.tool,
actual: actualTime,
benchmark: benchmarkTime,
performance: actualTime <= benchmarkTime ? 'good' : 'poor',
difference: (((actualTime - benchmarkTime) / benchmarkTime) * 100).toFixed(1),
};
}
// Compare test performance
if (performance.test && templateBenchmarks[performance.test.tool]) {
const benchmark = templateBenchmarks[performance.test.tool];
const actualTime = parseFloat(performance.test.testTime);
const benchmarkTime = parseFloat(benchmark.testTime || benchmark.buildTime);
comparison.test = {
tool: performance.test.tool,
actual: actualTime,
benchmark: benchmarkTime,
performance: actualTime <= benchmarkTime ? 'good' : 'poor',
difference: (((actualTime - benchmarkTime) / benchmarkTime) * 100).toFixed(1),
};
}
return comparison;
}
/**
* Generate performance recommendations
*/
function generateRecommendations(projectPath, performance, comparison) {
const recommendations = [];
// Build recommendations
if (comparison?.build?.performance === 'poor') {
recommendations.push({
category: 'build',
priority: 'high',
message: `๋น๋ ์๊ฐ์ด ์์๋ณด๋ค ${comparison.build.difference}% ๋๋ฆฝ๋๋ค`,
action: 'SWC๋ก ์ปดํ์ผ๋ฌ ๋ณ๊ฒฝ์ ๊ณ ๋ คํด๋ณด์ธ์ (3x ๋น ๋ฅธ ๋น๋)',
});
}
// Test recommendations
if (comparison?.test?.performance === 'poor') {
recommendations.push({
category: 'test',
priority: 'medium',
message: `ํ
์คํธ ์คํ ์๊ฐ์ด ์์๋ณด๋ค ${comparison.test.difference}% ๋๋ฆฝ๋๋ค`,
action: 'Vitest๋ก ํ
์คํธ ๋๊ตฌ ๋ณ๊ฒฝ์ ๊ณ ๋ คํด๋ณด์ธ์ (5x ๋น ๋ฅธ ํ
์คํธ)',
});
}
// Bundle size recommendations
if (performance.build?.bundleSize) {
const bundleSizeKB = parseFloat(performance.build.bundleSize);
if (bundleSizeKB > 1000) {
// > 1MB
recommendations.push({
category: 'bundle',
priority: 'medium',
message: `Bundle size is large (${performance.build.bundleSize})`,
action: 'Use TSUP tree-shaking features to reduce bundle size',
});
}
}
return recommendations;
}
/**
* Main profiler function
*/
async function profileProject(projectPath = process.cwd()) {
console.log(chalk.blue.bold('\nโก Starting performance profiling...\n'));
try {
const performance = {};
// 1. Build performance
try {
performance.build = await measureBuildPerformance(projectPath);
} catch (error) {
console.log(chalk.yellow(`โ ๏ธ Build performance measurement failed: ${error.message}`));
}
// 2. Test performance
try {
performance.test = await measureTestPerformance(projectPath);
} catch (error) {
console.log(chalk.yellow(`โ ๏ธ Test performance measurement failed: ${error.message}`));
}
// 3. Compare with benchmarks
const comparison = compareBenchmarks(projectPath, performance);
// 4. Generate recommendations
const recommendations = generateRecommendations(projectPath, performance, comparison);
// 5. Update metadata
const performanceData = {
buildTime: performance.build?.buildTime || null,
testTime: performance.test?.testTime || null,
bundleSize: performance.build?.bundleSize || null,
lastBenchmark: new Date().toISOString(),
};
updateProjectMetadata(projectPath, { performance: performanceData });
// 6. Display results
console.log(chalk.blue.bold('\n๐ Performance results:\n'));
if (performance.build) {
console.log(chalk.green.bold('๐จ Build performance:'));
console.log(chalk.gray(` Time: ${performance.build.buildTime}`));
console.log(chalk.gray(` Bundle size: ${performance.build.bundleSize}`));
console.log(chalk.gray(` Tool: ${performance.build.tool}`));
if (comparison?.build) {
const perfIcon = comparison.build.performance === 'good' ? '๐ข' : '๐ด';
console.log(chalk.gray(` vs. Benchmark: ${perfIcon} ${comparison.build.difference}%`));
}
console.log('');
}
if (performance.test) {
console.log(chalk.green.bold('๐งช Test performance:'));
console.log(chalk.gray(` Time: ${performance.test.testTime}`));
console.log(
chalk.gray(` Passed: ${performance.test.testsPassed}/${performance.test.testsTotal}`),
);
console.log(chalk.gray(` Tool: ${performance.test.tool}`));
if (comparison?.test) {
const perfIcon = comparison.test.performance === 'good' ? '๐ข' : '๐ด';
console.log(chalk.gray(` vs. Benchmark: ${perfIcon} ${comparison.test.difference}%`));
}
console.log('');
}
if (recommendations.length > 0) {
console.log(chalk.yellow.bold('๐ก Performance improvement recommendations:\n'));
recommendations.forEach((rec, index) => {
const priorityIcon = rec.priority === 'high' ? '๐ด' : '๐ก';
console.log(chalk.yellow(` ${index + 1}. ${priorityIcon} ${rec.message}`));
console.log(chalk.blue(` โก๏ธ ${rec.action}`));
console.log('');
});
} else {
console.log(chalk.green('๐ Excellent performance! No additional optimization needed.'));
}
} catch (error) {
console.error(chalk.red.bold('โ ํ๋กํ์ผ๋ง ์คํจ:'));
console.error(error.stack || error.message);
process.exit(1);
}
}
/**
* Helper functions
*/
function detectBuildTool(buildScript) {
if (buildScript.includes('swc')) return 'swc';
if (buildScript.includes('tsup')) return 'tsup';
if (buildScript.includes('tsc')) return 'tsc';
return 'unknown';
}
function detectTestTool(testScript) {
if (testScript.includes('jest')) return 'jest';
if (testScript.includes('vitest')) return 'vitest';
return 'unknown';
}
function parseTestResults(output, tool) {
const defaultResult = { total: 0, passed: 0 };
try {
if (tool.includes('jest')) {
const match = output.match(/Tests:\s+(\d+)\s+passed,\s+(\d+)\s+total/);
if (match) {
return { passed: parseInt(match[1]), total: parseInt(match[2]) };
}
} else if (tool.includes('vitest')) {
const match = output.match(/Test Files\s+\d+\s+passed[^T]*Tests\s+(\d+)\s+passed/s);
if (match) {
const passed = parseInt(match[1]);
return { passed, total: passed }; // Vitest output format is different
}
}
} catch (error) {
// Ignore parsing errors
}
return defaultResult;
}
async function main() {
const args = process.argv.slice(2);
if (args.includes('--help') || args.includes('-h')) {
console.log(chalk.blue.bold('\nPerformance Profiler\n'));
console.log('Usage: npm run profile [options]\n');
console.log('This tool measures build and test performance of your project.');
console.log('It compares results with benchmarks and provides optimization recommendations.\n');
console.log('Example:');
console.log(' npm run profile # Run full performance analysis');
return;
}
await profileProject();
}
if (import.meta.url === `file://${process.argv[1]}`) {
main();
}