Last active
June 21, 2025 01:32
-
-
Save shoveller/a5189e4fbc6a19f039559d51de8383f7 to your computer and use it in GitHub Desktop.
pnpm + turborepo + eslint + prettier + husky + typescript + sementic-release + reacr-router latest
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
set -e | |
# Color codes for output | |
RED='\033[0;31m' | |
GREEN='\033[0;32m' | |
YELLOW='\033[1;33m' | |
BLUE='\033[0;34m' | |
NC='\033[0m' # No Color | |
# Pure function to check if pnpm is installed | |
check_pnpm_installed() { | |
echo -e "${BLUE}pnpm 설치 상태를 확인합니다...${NC}" >&2 | |
if ! command -v pnpm &> /dev/null; then | |
echo -e "${RED}pnpm이 설치되어 있지 않습니다.${NC}" >&2 | |
echo -e "${YELLOW}pnpm을 설치하려면 다음 명령을 실행하세요:${NC}" >&2 | |
echo -e "${YELLOW}npm install -g pnpm${NC}" >&2 | |
exit 1 | |
fi | |
local pnpm_version | |
pnpm_version=$(pnpm -v 2>/dev/null | head -1 | tr -d '[:space:]' | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+') | |
if [[ -z "$pnpm_version" ]]; then | |
echo -e "${RED}pnpm 버전을 확인할 수 없습니다.${NC}" >&2 | |
exit 1 | |
fi | |
echo -e "${GREEN}pnpm이 설치되어 있습니다.${NC}" >&2 | |
echo "$pnpm_version" | |
} | |
# Pure function to get user input for project name and package scope | |
get_project_inputs() { | |
echo -e "${BLUE}프로젝트 이름을 입력하세요:${NC}" >&2 | |
read -r project_name </dev/tty | |
echo -e "${BLUE}패키지 스코프를 입력하세요 (예: @company):${NC}" >&2 | |
read -r package_scope </dev/tty | |
if [[ -z "$project_name" || -z "$package_scope" ]]; then | |
echo -e "${RED}프로젝트 이름과 패키지 스코프를 모두 입력해야 합니다.${NC}" >&2 | |
exit 1 | |
fi | |
echo "$project_name $package_scope" | |
} | |
# Pure function to initialize project directory | |
init_project() { | |
local project_name=$1 | |
echo -e "${GREEN}프로젝트 '$project_name' 디렉토리를 생성합니다...${NC}" | |
mkdir -p "$project_name" | |
cd "$project_name" | |
echo -e "${GREEN}Git 저장소를 초기화합니다...${NC}" | |
git init | |
echo -e "${GREEN}pnpm을 초기화합니다...${NC}" | |
pnpm init | |
} | |
# Pure function to setup gitignore | |
setup_gitignore() { | |
echo -e "${GREEN}.gitignore 파일을 생성합니다...${NC}" | |
pnpm dlx mrm@latest gitignore | |
echo -e "${GREEN}.gitignore 파일을 수정합니다...${NC}" | |
# .vscode/ 항목 삭제 | |
sed -i.bak '/^\.vscode\/$/d' .gitignore | |
# .lh/ 항목 추가 | |
echo ".lh/" >> .gitignore | |
# 백업 파일 삭제 | |
rm -f .gitignore.bak | |
} | |
# Pure function to setup @types/node | |
setup_types_node() { | |
echo -e "${GREEN}@types/node를 설정합니다...${NC}" | |
local node_version | |
node_version=$(node -v 2>/dev/null | grep -o '[0-9]\+' | head -1) | |
if [[ -z "$node_version" ]]; then | |
echo -e "${RED}Node.js 버전을 확인할 수 없습니다.${NC}" >&2 | |
exit 1 | |
fi | |
echo -e "${GREEN}Node.js 버전 $node_version을 감지했습니다.${NC}" | |
# Install @types/node with major version only | |
pnpm i -D "@types/node@$node_version" | |
} | |
# Pure function to setup TypeScript | |
setup_typescript() { | |
echo -e "${GREEN}TypeScript를 설치합니다...${NC}" | |
pnpm i -D typescript | |
echo -e "${GREEN}tsconfig.json 파일을 생성합니다...${NC}" | |
cat > tsconfig.json << 'EOF' | |
{ | |
"compilerOptions": { | |
/* 컴파일 성능 최적화 */ | |
"skipLibCheck": true, // 라이브러리 타입 정의 파일 검사 건너뛰기 (빌드 속도 향상) | |
"incremental": true, // 증분 컴파일 활성화 (이전 빌드 정보 재사용) | |
"tsBuildInfoFile": "./node_modules/.cache/tsc/tsbuildinfo", // 증분 컴파일 정보 저장 위치 | |
/* 출력 제어 */ | |
"noEmit": true, // JavaScript 파일 생성하지 않음 (타입 검사만 수행) | |
/* 엄격한 타입 검사 */ | |
"strict": true, // 모든 엄격한 타입 검사 옵션 활성화 | |
"noUnusedLocals": true, // 사용하지 않는 지역 변수 에러 처리 | |
"noUnusedParameters": true, // 사용하지 않는 함수 매개변수 에러 처리 | |
"noFallthroughCasesInSwitch": true, // switch문에서 break 누락 시 에러 처리 | |
"noUncheckedSideEffectImports": true, // 부작용이 있는 import 구문의 타입 검사 강화 | |
/* 구문 분석 최적화 */ | |
"erasableSyntaxOnly": true // TypeScript 고유 구문만 제거하고 JavaScript 호환성 유지 | |
} | |
} | |
EOF | |
} | |
# Pure function to setup semantic-release | |
setup_semantic_release() { | |
local pnpm_version=$1 | |
echo -e "${GREEN}semantic-release를 설치합니다...${NC}" | |
pnpm i -D semantic-release @semantic-release/commit-analyzer @semantic-release/release-notes-generator @semantic-release/changelog @semantic-release/npm @semantic-release/github @semantic-release/git | |
echo -e "${GREEN}release.config.ts 파일을 생성합니다...${NC}" | |
cat > release.config.ts << 'EOF' | |
import { GlobalConfig } from 'semantic-release' | |
// GitHub Actions 환경 변수로부터 저장소 URL 생성 | |
const getRepositoryUrl = (): string => { | |
// GitHub Actions 환경에서 실행 중인 경우 | |
if (!process.env.GITHUB_REPOSITORY) { | |
throw new Error('env.GITHUB_REPOSITORY not found') | |
} | |
// 로컬 환경 또는 환경 변수가 없는 경우 기본값 사용 | |
return `${process.env.GITHUB_SERVER_URL || 'https://github.com'}/${process.env.GITHUB_REPOSITORY}` | |
} | |
const config: GlobalConfig = { | |
branches: ['main'], | |
repositoryUrl: getRepositoryUrl(), | |
tagFormat: '${version}', | |
plugins: [ | |
'@semantic-release/commit-analyzer', // 커밋 메시지를 분석하여 버전 결정 | |
'@semantic-release/release-notes-generator', // CHANGELOG.md에 들어갈 릴리스 노트를 생성 | |
'@semantic-release/changelog', // CHANGELOG.md 업데이트 | |
[ | |
'@semantic-release/npm', | |
{ | |
npmPublish: false | |
} | |
], // npm 배포, package.json 업데이트 | |
'@semantic-release/github', // GitHub Release를 생성 | |
[ | |
'@semantic-release/git', // Git 커밋 및 푸시 | |
{ | |
assets: ['CHANGELOG.md', 'package.json', 'packages/*/package.json', 'apps/*/package.json'], | |
message: | |
'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}' | |
} | |
] | |
] | |
} | |
export default config | |
EOF | |
echo -e "${GREEN}GitHub Actions workflow 디렉토리를 생성합니다...${NC}" | |
mkdir -p .github/workflows | |
echo -e "${GREEN}semantic-release GitHub Actions workflow를 생성합니다...${NC}" | |
cat > .github/workflows/semantic-release.yml << EOF | |
name: semantic-release | |
on: | |
workflow_dispatch: | |
push: | |
branches: | |
- main | |
permissions: | |
contents: write | |
issues: write | |
pull-requests: write | |
jobs: | |
release: | |
runs-on: ubuntu-latest | |
steps: | |
- name: Checkout repository | |
uses: actions/checkout@v4 | |
- name: Install pnpm | |
uses: pnpm/action-setup@v4 | |
with: | |
version: '$pnpm_version' | |
run_install: false | |
- name: Setup Node.js | |
uses: actions/setup-node@v4 | |
with: | |
node-version: '20' | |
cache: 'pnpm' | |
- name: Install dependencies | |
run: pnpm i --frozen-lockfile | |
- name: Semantic Release | |
env: | |
GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }} | |
run: pnpm semantic-release | |
- name: Sync package versions | |
run: | | |
# semantic-release 실행 후 서브패키지들 버전 동기화 | |
cd packages/scripts && node sync-versions.mjs | |
- name: Commit version sync | |
run: | | |
git config --local user.email "[email protected]" | |
git config --local user.name "GitHub Action" | |
git add packages/*/package.json | |
git diff --staged --quiet || git commit -m "chore: sync package versions [skip ci]" | |
git push | |
EOF | |
} | |
# Pure function to setup package.json private field and scripts | |
setup_package_json_private() { | |
local pnpm_version=$1 | |
echo -e "${GREEN}package.json에 private: true, packageManager, scripts를 설정합니다...${NC}" | |
# Use jq if available, otherwise use sed | |
if command -v jq &> /dev/null; then | |
jq --arg version "$pnpm_version" '. + {"private": true, "packageManager": ("pnpm@" + $version), "scripts": {"format": "turbo format", "dev": "turbo dev", "sync-catalog": "sync-catalog"}}' package.json > package.json.tmp && mv package.json.tmp package.json | |
else | |
# Fallback: Create a proper package.json using Node.js | |
node -e " | |
const fs = require('fs'); | |
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); | |
pkg.private = true; | |
pkg.packageManager = 'pnpm@$pnpm_version'; | |
pkg.scripts = { | |
'format': 'turbo format', | |
'dev': 'turbo dev', | |
'sync-catalog': 'sync-catalog' | |
}; | |
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n'); | |
" | |
fi | |
} | |
# Pure function to install and setup turborepo | |
setup_turborepo() { | |
echo -e "${GREEN}Turborepo를 설치합니다...${NC}" | |
pnpm i turbo | |
} | |
# Pure function to install and setup husky | |
setup_husky() { | |
echo -e "${GREEN}Husky를 설치합니다...${NC}" | |
pnpm i husky | |
pnpm husky init | |
echo -e "${GREEN}pre-commit 훅을 설정합니다...${NC}" | |
echo "pnpm format" > .husky/pre-commit | |
} | |
# Pure function to create workspace structure | |
create_workspace_structure() { | |
echo -e "${GREEN}워크스페이스 구조를 생성합니다...${NC}" | |
mkdir -p apps packages | |
echo -e "${GREEN}pnpm-workspace.yaml을 생성합니다...${NC}" | |
cat > pnpm-workspace.yaml << 'EOF' | |
packages: | |
- 'apps/*' | |
- 'packages/*' | |
EOF | |
echo -e "${GREEN}turbo.json을 생성합니다...${NC}" | |
cat > turbo.json << 'EOF' | |
{ | |
"$schema": "https://turbo.build/schema.json", | |
"remoteCache": { | |
"enabled": false | |
}, | |
"tasks": { | |
"format": {}, | |
"dev": { | |
"cache": false, | |
"persistent": true | |
}, | |
"version": { | |
"dependsOn": ["^version"] | |
} | |
} | |
} | |
EOF | |
} | |
# Pure function to create sync-catalog script | |
create_sync_catalog_script() { | |
echo -e "${GREEN}sync-catalog.mjs 파일을 생성합니다...${NC}" | |
cat > sync-catalog.mjs << 'EOF' | |
#!/usr/bin/env node | |
import { execSync } from 'child_process' | |
import { readFileSync, writeFileSync, readdirSync, statSync } from 'fs' | |
import { join, dirname } from 'path' | |
import { fileURLToPath } from 'url' | |
const __filename = fileURLToPath(import.meta.url) | |
const __dirname = dirname(__filename) | |
// 프로젝트 루트 디렉토리 | |
const rootDir = join(__dirname, '../../') | |
/** | |
* 프로젝트 루트의 package.json에서 pnpm 버전을 추출 | |
*/ | |
function getPnpmVersionFromPackageJson() { | |
try { | |
const packageJsonPath = join(rootDir, 'package.json') | |
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) | |
if (!packageJson.packageManager) { | |
throw new Error('package.json에 packageManager 필드가 없습니다.') | |
} | |
// "[email protected]" 형태에서 버전만 추출 | |
const match = packageJson.packageManager.match(/pnpm@(.+)/) | |
if (!match) { | |
throw new Error('packageManager 필드에서 pnpm 버전을 찾을 수 없습니다.') | |
} | |
return match[1] | |
} catch (error) { | |
console.error('❌ pnpm 버전 추출 실패:', error.message) | |
process.exit(1) | |
} | |
} | |
/** | |
* .github/workflows 디렉토리에서 모든 workflow 파일을 찾기 | |
*/ | |
function findWorkflowFiles() { | |
const workflowDir = join(rootDir, '.github/workflows') | |
const files = [] | |
try { | |
const entries = readdirSync(workflowDir) | |
for (const entry of entries) { | |
const fullPath = join(workflowDir, entry) | |
const stat = statSync(fullPath) | |
if ( | |
stat.isFile() && | |
(entry.endsWith('.yml') || entry.endsWith('.yaml')) | |
) { | |
files.push(fullPath) | |
} | |
} | |
} catch (error) { | |
console.warn( | |
'⚠️ .github/workflows 디렉토리를 찾을 수 없습니다:', | |
error.message | |
) | |
} | |
return files | |
} | |
/** | |
* GitHub Actions workflow 파일에서 pnpm 버전 업데이트 | |
*/ | |
function updatePnpmVersionInWorkflow(filePath, newVersion) { | |
try { | |
let content = readFileSync(filePath, 'utf8') | |
let updated = false | |
// "- name: Install pnpm" 다음에 오는 pnpm/action-setup의 version 찾기 | |
const regex = | |
/(- name:\s*Install pnpm[\s\S]*?uses:\s*pnpm\/action-setup@[^\n]*\n\s*with:[\s\S]*?version:\s*['"]?)([^'"\n]+)(['"]?)/gi | |
content = content.replace( | |
regex, | |
(match, prefix, currentVersion, suffix) => { | |
if (currentVersion !== newVersion) { | |
console.log( | |
` 📝 ${filePath}에서 pnpm 버전 업데이트: ${currentVersion} → ${newVersion}` | |
) | |
updated = true | |
return prefix + newVersion + suffix | |
} | |
return match | |
} | |
) | |
if (updated) { | |
writeFileSync(filePath, content, 'utf8') | |
return true | |
} | |
return false | |
} catch (error) { | |
console.error(`❌ ${filePath} 업데이트 실패:`, error.message) | |
return false | |
} | |
} | |
/** | |
* pnpm codemod-catalog 실행 | |
*/ | |
function runCodemodCatalog() { | |
try { | |
console.log('🔄 pnpm codemod-catalog 실행 중...') | |
execSync('pnpx codemod pnpm/catalog', { | |
cwd: rootDir, | |
stdio: 'inherit' | |
}) | |
console.log('✅ codemod-catalog 실행 완료') | |
} catch (error) { | |
console.error('❌ codemod-catalog 실행 실패:', error.message) | |
console.error( | |
'오류 세부사항:', | |
error.stderr?.toString() || '알 수 없는 오류' | |
) | |
process.exit(1) | |
} | |
} | |
/** | |
* 메인 실행 함수 | |
*/ | |
function main() { | |
console.log('🎯 sync-catalog 스크립트 시작\n') | |
// 1. pnpm codemod-catalog 실행 | |
runCodemodCatalog() | |
// 2. package.json에서 pnpm 버전 추출 | |
const pnpmVersion = getPnpmVersionFromPackageJson() | |
console.log(`📦 현재 pnpm 버전: ${pnpmVersion}\n`) | |
// 3. GitHub Actions workflow 파일들 찾기 | |
const workflowFiles = findWorkflowFiles() | |
console.log(`🔍 발견된 workflow 파일: ${workflowFiles.length}개\n`) | |
// 4. 각 workflow 파일에서 pnpm 버전 업데이트 | |
let totalUpdated = 0 | |
for (const filePath of workflowFiles) { | |
console.log(`🔧 ${filePath} 처리 중...`) | |
if (updatePnpmVersionInWorkflow(filePath, pnpmVersion)) { | |
totalUpdated++ | |
} else { | |
console.log(` ℹ️ ${filePath}는 업데이트가 필요하지 않습니다.`) | |
} | |
} | |
console.log(`\n✨ 완료! ${totalUpdated}개 파일이 업데이트되었습니다.`) | |
if (totalUpdated > 0) { | |
console.log('\n💡 변경사항을 커밋하는 것을 잊지 마세요!') | |
} | |
} | |
// 스크립트 실행 | |
main() | |
EOF | |
} | |
# Pure function to setup scripts package README | |
setup_scripts_readme() { | |
echo -e "${GREEN}scripts 패키지 README.md 파일을 생성합니다...${NC}" | |
cat > packages/scripts/README.md << 'EOF' | |
# 유틸리티 설명 | |
## format.mjs | |
- 서브패키지에서 타입체크(`tsc`) > prettier > eslint 를 순차 실행하는 유틸리티입니다. | |
### 사용법 | |
1. package.json 의 devDependencies 에 `"@company/scripts": "workspace:*"` 를 추가하세요. | |
2. package.json 의 scripts 에 `"format": "format-app apps/web"` 을 추가하세요. | |
3. turbo.json 에 일괄 실행하는 명령어가 있고, 이것을 프로젝트 루트의 package.json 이 호출합니다. | |
4. 프로젝트 루트에서 `pnpm format` 을 호출하면 수동으로 실행할 수 있습니다. | |
5. `.husky/pre-commit` 에 `pnpm format` 을 등록했으므로 커밋할때 자동으로 호출됩니다. | |
## sync-catalog.mjs | |
- 서브패키지의 중복 디펜던시를 pnpm 의 카탈로그로 관리하는 유틸리티입니다. | |
- [pnpm codemod](https://github.com/pnpm/codemod) 라는 프로그램을 사용합니다. | |
- .github/workflows 아래의 워크플로우가 참조하는 pnpm 버전을 업데이트하는 부가기능이 있습니다. | |
- 바이너리로 등록이 되어 있습니다. | |
### 사용법 | |
1. 프로젝트 루트의 package.json 의 devDependencies 에 `"@company/scripts": "workspace:*"` 가 추가되어 있습니다. | |
2. 프로젝트 루트에서 `pnpm sync-catalog` 을 호출하면 수동으로 실행됩니다. | |
## sync-versions.mjs | |
- 모든 서브패키지의 package.json 버전을 프로젝트 루트의 package.json 버전으로 동기화합니다. | |
EOF | |
} | |
# Pure function to add scripts package to root devDependencies | |
add_scripts_to_root_dependencies() { | |
local package_scope=$1 | |
echo -e "${GREEN}루트 package.json에 scripts 패키지 의존성을 추가합니다...${NC}" | |
# Use jq if available, otherwise use Node.js | |
if command -v jq &> /dev/null; then | |
jq --arg scope "$package_scope" '.devDependencies = (.devDependencies // {}) + {($scope + "/scripts"): "workspace:*"}' package.json > package.json.tmp && mv package.json.tmp package.json | |
else | |
# Fallback: Use Node.js for safe JSON manipulation | |
node -e " | |
const fs = require('fs'); | |
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); | |
pkg.devDependencies = pkg.devDependencies || {}; | |
pkg.devDependencies['$package_scope/scripts'] = 'workspace:*'; | |
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n'); | |
" | |
fi | |
} | |
# Pure function to setup scripts package | |
setup_scripts_package() { | |
local package_scope=$1 | |
echo -e "${GREEN}scripts 패키지를 설정합니다...${NC}" | |
mkdir -p packages/scripts | |
cd packages/scripts | |
pnpm init | |
# Update package.json for scripts package | |
if command -v jq &> /dev/null; then | |
jq --arg scope "$package_scope" '. + {"name": ($scope + "/scripts"), "private": true, "main": "index.js", "scripts": {"version": "node sync-versions.mjs"}}' package.json > package.json.tmp && mv package.json.tmp package.json | |
else | |
# Fallback: Use Node.js for safe JSON manipulation | |
node -e " | |
const fs = require('fs'); | |
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); | |
pkg.name = '$package_scope/scripts'; | |
pkg.private = true; | |
pkg.main = 'index.js'; | |
pkg.scripts = { 'version': 'node sync-versions.mjs' }; | |
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n'); | |
" | |
fi | |
echo -e "${GREEN}sync-versions.mjs 파일을 생성합니다...${NC}" | |
cat > sync-versions.mjs << 'EOF' | |
#!/usr/bin/env node | |
import fs from 'fs'; | |
import path from 'path'; | |
import { fileURLToPath } from 'url'; | |
const __filename = fileURLToPath(import.meta.url); | |
const __dirname = path.dirname(__filename); | |
// 루트 package.json에서 버전 읽기 | |
const rootPackagePath = path.join(__dirname, '..', '..', 'package.json'); | |
const rootPackage = JSON.parse(fs.readFileSync(rootPackagePath, 'utf8')); | |
const rootVersion = rootPackage.version; | |
console.log(`Syncing all packages to version: ${rootVersion}`); | |
// packages 디렉토리의 모든 서브패키지 찾기 | |
const packagesDir = path.join(__dirname, '..'); | |
const packages = fs.readdirSync(packagesDir); | |
packages.forEach(packageName => { | |
const packagePath = path.join(packagesDir, packageName, 'package.json'); | |
if (fs.existsSync(packagePath) && packageName !== 'scripts') { | |
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8')); | |
const oldVersion = packageJson.version; | |
packageJson.version = rootVersion; | |
fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2) + '\n'); | |
console.log(`Updated ${packageJson.name}: ${oldVersion} → ${rootVersion}`); | |
} | |
}); | |
console.log('Version sync completed!'); | |
EOF | |
echo -e "${GREEN}format.mjs 파일을 생성합니다...${NC}" | |
cat > format.mjs << 'EOF' | |
#!/usr/bin/env node | |
import { execSync } from 'child_process'; | |
import path from 'path'; | |
import { fileURLToPath } from 'url'; | |
const __filename = fileURLToPath(import.meta.url); | |
const __dirname = path.dirname(__filename); | |
function runCommand(command, cwd) { | |
try { | |
execSync(command, { | |
cwd, | |
stdio: 'inherit', | |
encoding: 'utf8' | |
}); | |
} catch (error) { | |
console.error(`Error running command: ${command}`); | |
process.exit(1); | |
} | |
} | |
const projectRoot = path.resolve(__dirname, '../..'); | |
const targetDir = process.argv[2]; | |
if (!targetDir) { | |
console.error('Usage: node format.mjs <app-directory>'); | |
process.exit(1); | |
} | |
const appPath = path.resolve(projectRoot, targetDir); | |
console.log(`Running format in ${appPath}`); | |
runCommand('tsc', appPath); | |
runCommand('prettier --write "**/*.{ts,tsx,cjs,mjs,json,html,css,js,jsx}" --cache --config prettier.config.mjs', appPath); | |
runCommand('eslint --fix --cache --cache-location ./node_modules/.cache/eslint .', appPath); | |
console.log('Format completed successfully!'); | |
EOF | |
# sync-catalog.mjs 파일 생성 | |
create_sync_catalog_script | |
# package.json에 bin 섹션 추가 | |
echo -e "${GREEN}package.json에 bin 섹션을 추가합니다...${NC}" | |
if command -v jq &> /dev/null; then | |
jq --arg scope "$package_scope" '. + {"name": ($scope + "/scripts"), "private": true, "main": "index.js", "bin": {"format-app": "./format.mjs", "sync-catalog": "./sync-catalog.mjs"}, "scripts": {"version": "node sync-versions.mjs"}}' package.json > package.json.tmp && mv package.json.tmp package.json | |
else | |
# Fallback: Use Node.js for safe JSON manipulation | |
node -e " | |
const fs = require('fs'); | |
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); | |
pkg.name = '$package_scope/scripts'; | |
pkg.private = true; | |
pkg.main = 'index.js'; | |
pkg.bin = { | |
'format-app': './format.mjs', | |
'sync-catalog': './sync-catalog.mjs' | |
}; | |
pkg.scripts = { 'version': 'node sync-versions.mjs' }; | |
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n'); | |
" | |
fi | |
cd ../.. | |
} | |
# Pure function to setup ESLint package | |
setup_eslint_package() { | |
local package_scope=$1 | |
echo -e "${GREEN}ESLint 패키지를 설정합니다...${NC}" | |
mkdir -p packages/eslint | |
cd packages/eslint | |
pnpm init | |
# Update package.json for ESLint package | |
if command -v jq &> /dev/null; then | |
jq --arg scope "$package_scope" '. + {"name": ($scope + "/eslint"), "private": true, "main": "index.mjs"}' package.json > package.json.tmp && mv package.json.tmp package.json | |
else | |
# Fallback: Use Node.js for safe JSON manipulation | |
node -e " | |
const fs = require('fs'); | |
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); | |
pkg.name = '$package_scope/eslint'; | |
pkg.private = true; | |
pkg.main = 'index.mjs'; | |
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n'); | |
" | |
fi | |
echo -e "${GREEN}ESLint 의존성을 설치합니다...${NC}" | |
pnpm i @eslint/js eslint globals typescript-eslint eslint-plugin-unused-imports @typescript-eslint/eslint-plugin @typescript-eslint/parser | |
echo -e "${GREEN}ESLint 설정 파일을 생성합니다...${NC}" | |
cat > index.mjs << 'EOF' | |
import js from '@eslint/js' | |
import globals from 'globals' | |
import tseslint from 'typescript-eslint' | |
import unusedImports from 'eslint-plugin-unused-imports' | |
const defaultCodeStyle = { | |
files: ['**/*.{ts,tsx}'], | |
languageOptions: { | |
ecmaVersion: 'latest', | |
globals: { | |
...globals.browser, | |
...globals.node | |
} | |
}, | |
plugins: { | |
'unused-imports': unusedImports | |
}, | |
rules: { | |
'max-depth': ['error', 2], | |
'padding-line-between-statements': [ | |
'error', | |
{ blankLine: 'always', prev: '*', next: 'return' }, | |
{ blankLine: 'always', prev: '*', next: 'if' }, | |
{ blankLine: 'always', prev: 'function', next: '*' }, | |
{ blankLine: 'always', prev: '*', next: 'function' } | |
], | |
'no-restricted-syntax': [ | |
'error', | |
{ | |
selector: 'TSInterfaceDeclaration', | |
message: 'Interface 대신 type 을 사용하세요.' | |
}, | |
{ | |
selector: 'VariableDeclaration[kind="let"]', | |
message: 'let 대신 const 를 사용하세요.' | |
}, | |
{ | |
selector: 'VariableDeclaration[kind="var"]', | |
message: 'var 대신 const 를 사용하세요.' | |
}, | |
{ | |
selector: 'SwitchStatement', | |
message: 'switch 대신 if 를 사용하세요.' | |
}, | |
{ | |
selector: 'ConditionalExpression', | |
message: '삼항 연산자 대신 if 를 사용하세요.' | |
}, | |
{ | |
selector: 'IfStatement[alternate]', | |
message: 'else 대신 early return 을 사용하세요.' | |
}, | |
{ | |
selector: 'ForStatement', | |
message: | |
'for 루프 대신 배열 메서드(map, filter, reduce 등)를 사용하세요.' | |
}, | |
{ | |
selector: 'WhileStatement', | |
message: 'while 루프 대신 배열 메서드나 재귀를 사용하세요.' | |
}, | |
{ | |
selector: 'DoWhileStatement', | |
message: 'do-while 루프 대신 배열 메서드나 재귀를 사용하세요.' | |
}, | |
{ | |
selector: 'ForInStatement', | |
message: | |
'for-in 루프 대신 Object.keys(), Object.values(), Object.entries()를 사용하세요.' | |
}, | |
{ | |
selector: 'ForOfStatement', | |
message: 'for-of 루프 대신 배열 메서드를 사용하세요.' | |
}, | |
{ | |
selector: 'CallExpression[callee.property.name="push"]', | |
message: | |
'push() 대신 concat() 또는 스프레드 연산자를 사용하세요. (부수효과 방지)' | |
}, | |
{ | |
selector: 'CallExpression[callee.property.name="pop"]', | |
message: 'pop() 대신 slice() 메소드를 사용하세요. (부수효과 방지)' | |
}, | |
{ | |
selector: 'CallExpression[callee.property.name="shift"]', | |
message: 'shift() 대신 slice() 메소드를 사용하세요. (부수효과 방지)' | |
}, | |
{ | |
selector: 'CallExpression[callee.property.name="unshift"]', | |
message: | |
'unshift() 대신 concat() 또는 스프레드 연산자를 사용하세요. (부수효과 방지)' | |
}, | |
{ | |
selector: 'CallExpression[callee.property.name="splice"]', | |
message: | |
'splice() 대신 slice() 및 스프레드 연산자를 사용하세요. (부수효과 방지)' | |
}, | |
{ | |
selector: 'CallExpression[callee.property.name="reverse"]', | |
message: | |
'reverse() 대신 [...array].reverse()를 사용하세요. (부수효과 방지)' | |
}, | |
{ | |
selector: 'CallExpression[callee.property.name="fill"]', | |
message: 'fill() 대신 map()을 사용하세요. (부수효과 방지)' | |
}, | |
{ | |
selector: 'CallExpression[callee.property.name="copyWithin"]', | |
message: 'copyWithin() 대신 map()을 사용하세요. (부수효과 방지)' | |
}, | |
{ | |
selector: | |
'CallExpression[callee.object.name="Object"][callee.property.name="assign"]', | |
message: | |
'Object.assign() 대신 스프레드 연산자를 사용하세요. (부수효과 방지)' | |
}, | |
{ | |
selector: | |
'CallExpression[callee.object.name="Object"][callee.property.name="defineProperty"]', | |
message: | |
'Object.defineProperty() 대신 새 객체를 생성하세요. (부수효과 방지)' | |
}, | |
{ | |
selector: | |
'CallExpression[callee.object.name="Object"][callee.property.name="defineProperties"]', | |
message: | |
'Object.defineProperties() 대신 새 객체를 생성하세요. (부수효과 방지)' | |
}, | |
{ | |
selector: | |
'CallExpression[callee.object.name="Object"][callee.property.name="setPrototypeOf"]', | |
message: | |
'Object.setPrototypeOf() 대신 Object.create()를 사용하세요. (부수효과 방지)' | |
}, | |
{ | |
selector: 'UnaryExpression[operator="delete"]', | |
message: | |
'delete 연산자 대신 새 객체를 생성하고 원하는 속성만 포함하세요. (부수효과 방지)' | |
}, | |
{ | |
selector: | |
'AssignmentExpression[left.type="Identifier"][left.name=/^(params?|args?|arguments|prop|props|parameter|parameters)$/]', | |
message: | |
'함수 파라미터는 직접 수정하지 마세요. 새 변수를 만들어 사용하세요.' | |
}, | |
{ | |
selector: | |
'AssignmentExpression[left.type="MemberExpression"][left.object.name=/^(params?|args?|arguments|prop|props|parameter|parameters)$/]', | |
message: | |
'함수 파라미터의 속성은 직접 수정하지 마세요. 객체를 복사하여 사용하세요.' | |
}, | |
{ | |
selector: | |
'FunctionDeclaration > BlockStatement > ExpressionStatement > AssignmentExpression[left.type="Identifier"]', | |
message: | |
'함수 내에서 파라미터를 재할당하지 마세요. 새 변수를 만들어 사용하세요.' | |
}, | |
{ | |
selector: | |
'ArrowFunctionExpression > BlockStatement > ExpressionStatement > AssignmentExpression[left.type="Identifier"]', | |
message: | |
'함수 내에서 파라미터를 재할당하지 마세요. 새 변수를 만들어 사용하세요.' | |
} | |
], | |
'no-unused-vars': 'off', | |
'unused-imports/no-unused-imports': 'error', | |
'unused-imports/no-unused-vars': [ | |
'warn', | |
{ | |
vars: 'all', | |
varsIgnorePattern: '^_', | |
args: 'after-used', | |
argsIgnorePattern: '^_' | |
} | |
], | |
'no-param-reassign': ['error', { props: true }], | |
'no-shadow': 'off', // 기본 ESLint 규칙은 비활성화 | |
'@typescript-eslint/no-shadow': [ | |
'error', | |
{ | |
builtinGlobals: true, | |
hoist: 'all', | |
allow: [] // 예외를 허용하고 싶은 변수 이름들 | |
} | |
] | |
} | |
} | |
export default tseslint.config(defaultCodeStyle, { | |
extends: [js.configs.recommended, ...tseslint.configs.recommended], | |
files: ['**/*.{ts,tsx}'], | |
languageOptions: { | |
ecmaVersion: 'latest', | |
globals: globals.browser | |
} | |
}) | |
EOF | |
cd ../.. | |
} | |
# Pure function to setup Prettier package | |
setup_prettier_package() { | |
local package_scope=$1 | |
echo -e "${GREEN}Prettier 패키지를 설정합니다...${NC}" | |
mkdir -p packages/prettier | |
cd packages/prettier | |
pnpm init | |
# Update package.json for Prettier package | |
if command -v jq &> /dev/null; then | |
jq --arg scope "$package_scope" '. + {"name": ($scope + "/prettier"), "private": true, "main": "index.mjs"}' package.json > package.json.tmp && mv package.json.tmp package.json | |
else | |
# Fallback: Use Node.js for safe JSON manipulation | |
node -e " | |
const fs = require('fs'); | |
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); | |
pkg.name = '$package_scope/prettier'; | |
pkg.private = true; | |
pkg.main = 'index.mjs'; | |
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n'); | |
" | |
fi | |
echo -e "${GREEN}Prettier 의존성을 설치합니다...${NC}" | |
pnpm i prettier prettier-plugin-classnames prettier-plugin-css-order @ianvs/prettier-plugin-sort-imports | |
echo -e "${GREEN}Prettier 설정 파일을 생성합니다...${NC}" | |
cat > index.mjs << 'EOF' | |
/** @type {import('prettier').Config} */ | |
export default { | |
endOfLine: 'lf', | |
semi: false, | |
singleQuote: true, | |
tabWidth: 2, | |
trailingComma: 'none', | |
// import sort[s] | |
plugins: [ | |
'@ianvs/prettier-plugin-sort-imports', | |
'prettier-plugin-css-order', | |
'prettier-plugin-classnames' | |
], | |
endingPosition: 'absolute-with-indent', | |
importOrder: [ | |
'^react', | |
'', | |
'<BUILTIN_MODULES>', | |
'<THIRD_PARTY_MODULES>', | |
'', | |
'.css$', | |
'.scss$', | |
'^[.]' | |
], | |
importOrderParserPlugins: ['typescript', 'jsx', 'decorators-legacy'] | |
// import sort[e] | |
} | |
EOF | |
cd ../.. | |
} | |
# Pure function to create root config files | |
create_root_config_files() { | |
local package_scope=$1 | |
echo -e "${GREEN}루트 설정 파일을 생성합니다...${NC}" | |
# Create eslint.config.mjs | |
echo "export { default } from '$package_scope/eslint'" > eslint.config.mjs | |
# Create prettier.config.mjs | |
echo "export { default } from '$package_scope/prettier'" > prettier.config.mjs | |
} | |
# Pure function to setup React Router web app | |
setup_react_router_web() { | |
local package_scope=$1 | |
echo -e "${GREEN}React Router 웹 앱을 생성합니다...${NC}" | |
# Move to apps directory and create React Router project | |
cd apps | |
pnpm create react-router@latest web --no-install --no-git-init | |
# Move to web directory and update package.json | |
cd web | |
echo -e "${GREEN}package.json의 name 필드를 업데이트합니다...${NC}" | |
if command -v jq &> /dev/null; then | |
jq --arg scope "$package_scope" '. + {"name": ($scope + "/web")}' package.json > package.json.tmp && mv package.json.tmp package.json | |
else | |
# Fallback: Use Node.js for safe JSON manipulation | |
node -e " | |
const fs = require('fs'); | |
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); | |
pkg.name = '$package_scope/web'; | |
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n'); | |
" | |
fi | |
echo -e "${GREEN}devDependencies에 scripts, eslint, prettier 패키지를 추가합니다...${NC}" | |
if command -v jq &> /dev/null; then | |
jq --arg scope "$package_scope" '.devDependencies += {($scope + "/scripts"): "workspace:*", ($scope + "/eslint"): "workspace:*", ($scope + "/prettier"): "workspace:*"}' package.json > package.json.tmp && mv package.json.tmp package.json | |
else | |
# Fallback: Use Node.js for safe JSON manipulation | |
node -e " | |
const fs = require('fs'); | |
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); | |
pkg.devDependencies = pkg.devDependencies || {}; | |
pkg.devDependencies['$package_scope/scripts'] = 'workspace:*'; | |
pkg.devDependencies['$package_scope/eslint'] = 'workspace:*'; | |
pkg.devDependencies['$package_scope/prettier'] = 'workspace:*'; | |
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n'); | |
" | |
fi | |
echo -e "${GREEN}npm scripts에 format 스크립트를 추가합니다...${NC}" | |
if command -v jq &> /dev/null; then | |
jq '.scripts += {"format": "format-app apps/web"}' package.json > package.json.tmp && mv package.json.tmp package.json | |
else | |
# Fallback: Use Node.js for safe JSON manipulation | |
node -e " | |
const fs = require('fs'); | |
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); | |
pkg.scripts = pkg.scripts || {}; | |
pkg.scripts.format = 'format-app apps/web'; | |
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n'); | |
" | |
fi | |
echo -e "${GREEN}eslint.config.mjs 파일을 생성합니다...${NC}" | |
cat > eslint.config.mjs << EOF | |
import defaultConfig from '$package_scope/eslint' | |
export default [ | |
...defaultConfig, | |
{ | |
ignores: ['build/**', 'node_modules/**', '.react-router'] | |
} | |
] | |
EOF | |
echo -e "${GREEN}prettier.config.mjs 파일을 생성합니다...${NC}" | |
cat > prettier.config.mjs << EOF | |
export { default } from '$package_scope/prettier' | |
EOF | |
echo -e "${GREEN}tsconfig.json에 extends 설정을 추가합니다...${NC}" | |
if command -v jq &> /dev/null; then | |
jq '. + {"extends": "../../tsconfig.json"}' tsconfig.json > tsconfig.json.tmp && mv tsconfig.json.tmp tsconfig.json | |
else | |
# Fallback: Use Node.js for safe JSON manipulation | |
node -e " | |
const fs = require('fs'); | |
const config = JSON.parse(fs.readFileSync('tsconfig.json', 'utf8')); | |
config.extends = '../../tsconfig.json'; | |
fs.writeFileSync('tsconfig.json', JSON.stringify(config, null, 2) + '\n'); | |
" | |
fi | |
echo -e "${GREEN}의존성을 설치하고 타입 체크를 실행합니다...${NC}" | |
pnpm i | |
pnpm typecheck | |
cd ../.. | |
} | |
# Pure function to setup VS Code workspace settings | |
setup_vscode_workspace() { | |
echo -e "${GREEN}.vscode 워크스페이스 설정을 생성합니다...${NC}" | |
mkdir -p .vscode | |
echo -e "${GREEN}.vscode/extensions.json 파일을 생성합니다...${NC}" | |
cat > .vscode/extensions.json << 'EOF' | |
{ | |
"recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"] | |
} | |
EOF | |
echo -e "${GREEN}.vscode/settings.json 파일을 생성합니다...${NC}" | |
cat > .vscode/settings.json << 'EOF' | |
{ | |
"explorer.compactFolders": false, | |
"typescript.tsdk": "node_modules/typescript/lib", | |
"prettier.prettierPath": "./node_modules/prettier", | |
"prettier.configPath": "prettier.config.mjs", | |
"eslint.options": { | |
"overrideConfigFile": "eslint.config.mjs" | |
}, | |
"editor.formatOnSave": true, | |
"editor.defaultFormatter": "esbenp.prettier-vscode", | |
"editor.codeActionsOnSave": { | |
"source.fixAll.eslint": "explicit" | |
}, | |
"[javascript]": { | |
"editor.formatOnSave": true, | |
"editor.defaultFormatter": "esbenp.prettier-vscode" | |
}, | |
"[typescript]": { | |
"editor.formatOnSave": true, | |
"editor.defaultFormatter": "esbenp.prettier-vscode" | |
}, | |
"local-history.browser.descending": false | |
} | |
EOF | |
} | |
# Main execution function | |
main() { | |
echo -e "${BLUE}=== 프로젝트 스캐폴딩을 시작합니다 ===${NC}" | |
# Check if pnpm is installed and get version | |
pnpm_version=$(check_pnpm_installed) | |
# Get user inputs | |
inputs=$(get_project_inputs) | |
project_name=$(echo "$inputs" | cut -d' ' -f1) | |
package_scope=$(echo "$inputs" | cut -d' ' -f2) | |
echo -e "${YELLOW}프로젝트명: $project_name${NC}" | |
echo -e "${YELLOW}패키지 스코프: $package_scope${NC}" | |
# Execute setup functions in order | |
init_project "$project_name" | |
setup_gitignore | |
setup_types_node | |
setup_typescript | |
setup_vscode_workspace | |
setup_semantic_release "$pnpm_version" | |
setup_package_json_private "$pnpm_version" | |
setup_turborepo | |
setup_husky | |
create_workspace_structure | |
setup_scripts_package "$package_scope" | |
setup_scripts_readme | |
add_scripts_to_root_dependencies "$package_scope" | |
setup_eslint_package "$package_scope" | |
setup_prettier_package "$package_scope" | |
create_root_config_files "$package_scope" | |
setup_react_router_web "$package_scope" | |
setup_vscode_workspace | |
echo -e "${GREEN}=== 프로젝트 스캐폴딩이 완료되었습니다! ===${NC}" | |
echo -e "${BLUE}프로젝트 디렉토리: $(pwd)${NC}" | |
} | |
# Run main function | |
main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
사용법