typescript-express-starter
Version:
Quick and Easy TypeScript Express Starter
365 lines (317 loc) • 10.9 kB
JavaScript
/**
* DevTools 설정 병합 시스템
* Phase 3 - 빌드/테스트 도구 최적화
*/
import fs from 'fs';
import path from 'path';
/**
* 깊은 객체 병합 함수
* @param {object} target - 대상 객체
* @param {...object} sources - 병합할 소스 객체들
* @returns {object} 병합된 객체
*/
function deepMerge(target, ...sources) {
if (!sources.length) return target;
const source = sources.shift();
if (isObject(target) && isObject(source)) {
for (const key in source) {
if (isObject(source[key])) {
if (!target[key]) Object.assign(target, { [key]: {} });
deepMerge(target[key], source[key]);
} else {
Object.assign(target, { [key]: source[key] });
}
}
}
return deepMerge(target, ...sources);
}
/**
* 객체 여부 확인 함수
* @param {any} item - 확인할 항목
* @returns {boolean} 객체 여부
*/
function isObject(item) {
return item && typeof item === 'object' && !Array.isArray(item);
}
/**
* 설정 파일을 읽는 함수 (개선된 버전)
* @param {string} configPath - 설정 파일 경로
* @returns {object|null} 파싱된 설정 객체 또는 null
*/
function readConfig(configPath) {
try {
const fullPath = path.resolve(configPath);
if (!fs.existsSync(fullPath)) {
return null;
}
const ext = path.extname(fullPath);
const content = fs.readFileSync(fullPath, 'utf8');
switch (ext) {
case '.json':
return JSON.parse(content);
case '.js':
case '.mjs':
// 동적 import로 ES 모듈 지원
delete require.cache[fullPath];
return require(fullPath);
case '.ts':
// TypeScript 파일의 경우 빌드된 JS 파일을 찾아 사용
const jsPath = fullPath.replace('.ts', '.js');
if (fs.existsSync(jsPath)) {
delete require.cache[jsPath];
return require(jsPath);
}
throw new Error(`TypeScript config found but no compiled JS file: ${jsPath}`);
case '.swcrc':
case '.babelrc':
return JSON.parse(content);
default:
// 확장자가 없거나 인식되지 않는 경우 JSON으로 시도
try {
return JSON.parse(content);
} catch {
// JSON이 아닌 경우 문자열로 반환
return { content };
}
}
} catch (error) {
console.warn(`Failed to read config file ${configPath}:`, error.message);
return null;
}
}
/**
* 도구별 계층화된 설정을 병합하는 함수 (개선된 버전)
* @param {string} tool - 도구 이름 (swc, tsup, jest, vitest 등)
* @param {string} template - 템플릿 이름
* @returns {object} 병합된 설정 객체
*/
export function mergeConfigs(tool, template) {
const devtoolsPath = path.join(process.cwd(), 'devtools');
// 1. 기본 설정 읽기
const basePath = path.join(devtoolsPath, getToolPath(tool), 'base', `base.${getConfigExtension(tool)}`);
const baseConfig = readConfig(basePath) || {};
// 2. 템플릿별 설정 읽기
const templatePath = path.join(devtoolsPath, getToolPath(tool), template, `${template}.${getConfigExtension(tool)}`);
const templateConfig = readConfig(templatePath) || {};
// 3. 오버라이드 설정 읽기
const ormName = template.split('-')[0];
const overridePath = path.join(devtoolsPath, getToolPath(tool), 'overrides', `${ormName}.${getConfigExtension(tool)}`);
const overrideConfig = readConfig(overridePath) || {};
// 4. ORM별 공통 설정 읽기 (해당하는 경우)
const ormPath = path.join(devtoolsPath, getToolPath(tool), 'orm-templates', `${ormName}.${getConfigExtension(tool)}`);
const ormConfig = readConfig(ormPath) || {};
// 설정 병합: base → template → orm → override 순서
const mergedConfig = deepMerge({}, baseConfig, templateConfig, ormConfig, overrideConfig);
// 설정 후처리 (도구별 특화)
return postProcessConfig(tool, template, mergedConfig);
}
/**
* 도구별 설정 후처리 함수
* @param {string} tool - 도구 이름
* @param {string} template - 템플릿 이름
* @param {object} config - 병합된 설정
* @returns {object} 후처리된 설정
*/
function postProcessConfig(tool, template, config) {
switch (tool) {
case 'swc':
return postProcessSwcConfig(template, config);
case 'tsup':
return postProcessTsupConfig(template, config);
case 'jest':
return postProcessJestConfig(template, config);
case 'vitest':
return postProcessVitestConfig(template, config);
default:
return config;
}
}
/**
* SWC 설정 후처리
*/
function postProcessSwcConfig(template, config) {
// 템플릿별 최적화 적용
if (template.includes('mongodb')) {
config.jsc.loose = true; // MongoDB는 loose 모드 활용
}
if (template.includes('prisma')) {
config.exclude = config.exclude || [];
if (!config.exclude.includes('prisma/migrations/**/*')) {
config.exclude.push('prisma/migrations/**/*');
}
}
return config;
}
/**
* TSUP 설정 후처리
*/
function postProcessTsupConfig(template, config) {
// ORM별 외부 의존성 설정
const ormName = template.split('-')[0];
const ormExternals = {
'prisma': ['@prisma/client'],
'drizzle': ['drizzle-orm', 'drizzle-kit'],
'typeorm': ['typeorm', 'reflect-metadata'],
'mikroorm': ['@mikro-orm/core', 'reflect-metadata'],
'mongoose': ['mongoose']
};
if (ormExternals[ormName]) {
config.external = [...(config.external || []), ...ormExternals[ormName]];
}
return config;
}
/**
* Jest 설정 후처리
*/
function postProcessJestConfig(template, config) {
// 템플릿별 테스트 환경 설정
const ormName = template.split('-')[0];
if (['typeorm', 'mikroorm'].includes(ormName)) {
config.testTimeout = config.testTimeout || 30000; // Decorator 처리 시간
} else if (['drizzle', 'prisma'].includes(ormName)) {
config.testTimeout = config.testTimeout || 20000;
}
return config;
}
/**
* Vitest 설정 후처리
*/
function postProcessVitestConfig(template, config) {
// 템플릿별 성능 최적화
const ormName = template.split('-')[0];
// TypeORM은 싱글스레드, 나머지는 멀티스레드
if (ormName === 'typeorm') {
config.test = config.test || {};
config.test.poolOptions = config.test.poolOptions || {};
config.test.poolOptions.threads = { singleThread: true, isolate: true };
}
return config;
}
console.warn(`Warning: Could not read config file ${configPath}:`, error.message);
return {};
}
}
/**
* 객체 깊은 병합 함수
* @param {...object} sources - 병합할 객체들
* @returns {object} 병합된 객체
*/
export function deepMerge(...sources) {
if (sources.length === 0) return {};
if (sources.length === 1) return sources[0];
const [target, ...rest] = sources;
const result = { ...target };
for (const source of rest) {
if (!source || typeof source !== 'object') continue;
for (const key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
if (
result[key] &&
typeof result[key] === 'object' &&
typeof source[key] === 'object' &&
!Array.isArray(result[key]) &&
!Array.isArray(source[key])
) {
result[key] = deepMerge(result[key], source[key]);
} else {
result[key] = source[key];
}
}
}
}
return result;
}
/**
* 템플릿별 설정을 병합하는 함수
* @param {string} tool - 도구 이름 (swc, jest, vitest 등)
* @param {string} template - 템플릿 이름
* @returns {object} 병합된 설정
*/
export function mergeConfigs(tool, template) {
const devtoolsPath = path.resolve(process.cwd(), 'devtools');
// 1. 기본 설정 읽기
let baseConfig = {};
try {
if (tool === 'swc') {
baseConfig = readConfig(path.join(devtoolsPath, 'build/swc/base/base.swcrc'));
} else if (tool === 'tsup') {
baseConfig = readConfig(path.join(devtoolsPath, 'build/tsup/base/base.tsup.config.ts'));
} else if (tool === 'jest') {
baseConfig = readConfig(path.join(devtoolsPath, 'test/jest/jest.config.cjs'));
} else if (tool === 'vitest') {
baseConfig = readConfig(path.join(devtoolsPath, 'test/vitest/vitest.config.ts'));
}
} catch (error) {
console.warn(`Warning: Could not read base config for ${tool}:`, error.message);
}
// 2. 템플릿별 설정 읽기
let templateConfig = {};
try {
if (tool === 'swc') {
const templatePath = path.join(devtoolsPath, `build/swc/${template}/${template}.swcrc`);
if (fs.existsSync(templatePath)) {
templateConfig = readConfig(templatePath);
}
} else if (tool === 'tsup') {
const templatePath = path.join(devtoolsPath, `build/tsup/${template}/${template}.tsup.config.ts`);
if (fs.existsSync(templatePath)) {
templateConfig = readConfig(templatePath);
}
}
} catch (error) {
console.warn(`Warning: Could not read template config for ${tool}/${template}:`, error.message);
}
// 3. 오버라이드 설정 읽기
let overrideConfig = {};
try {
if (tool === 'swc') {
const overridePath = path.join(devtoolsPath, `build/swc/overrides/${template}.swcrc`);
if (fs.existsSync(overridePath)) {
overrideConfig = readConfig(overridePath);
}
}
} catch (error) {
console.warn(`Warning: Could not read override config for ${tool}/${template}:`, error.message);
}
// 4. 설정 병합
return deepMerge(baseConfig, templateConfig, overrideConfig);
}
/**
* 설정을 파일로 저장하는 함수
* @param {string} filePath - 저장할 파일 경로
* @param {object} config - 저장할 설정 객체
* @param {string} format - 파일 형식 (json, js)
*/
export function saveConfig(filePath, config, format = 'json') {
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
if (format === 'json') {
fs.writeFileSync(filePath, JSON.stringify(config, null, 2));
} else if (format === 'js') {
const content = `export default ${JSON.stringify(config, null, 2)};`;
fs.writeFileSync(filePath, content);
} else {
// 문자열로 저장
fs.writeFileSync(filePath, config.toString());
}
}
/**
* 모든 템플릿에 대해 특정 도구의 설정을 생성하는 함수
* @param {string} tool - 도구 이름
* @param {string[]} templates - 템플릿 목록
* @returns {object} 템플릿별 설정 맵
*/
export function generateAllConfigs(tool, templates) {
const configs = {};
for (const template of templates) {
try {
configs[template] = mergeConfigs(tool, template);
} catch (error) {
console.error(`Error generating config for ${tool}/${template}:`, error.message);
configs[template] = {};
}
}
return configs;
}