create-express-esm 1.1.5 → 1.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -17,10 +17,11 @@
17
17
 
18
18
  기존 `express-generator`의 한계를 분석하고 개선하여 개발했습니다.
19
19
 
20
- - **⚡️ 100% ES Modules**: 구식 CommonJS(`require`) 버리고 최신 `import/export` 문법을 기본으로 채택했습니다.
21
- - **🏗 Layered Architecture**: 실무 표준인 `Controller` - `Service` - `Model` 구조를 자동으로 잡아줍니다.
22
- - **📦 Auto Installation**: 프로젝트 생성 귀찮은 `npm install` 과정을 자동으로 수행합니다.
23
- - **🛠 Ready-to-Use**: `dotenv`, `cors`, `morgan`, `nodemon` 필수 개발 환경이 세팅되어 있습니다.
20
+ - **🌐 Multi-Language Support**: JavaScript(ESM) **TypeScript** 원하는 개발 환경을 선택할 수 있습니다.
21
+ - **🧪 Integrated Testing**: 최신 테스트 프레임워크인 **Vitest**와 API 테스트용 **Supertest** 환경을 자동 설정합니다.
22
+ - **🏗 Layered Architecture**: 실무 표준인 `Controller` - `Service` - `Route` 계층 구조를 제공합니다.
23
+ - **⚡️ Modern TS Execution**: `ts-node`의 ESM 호환성 문제를 해결한 **`tsx`**를 채택하여 쾌적한 개발 환경을 제공합니다.
24
+ - **📦 Smart Auto-Installation**: 프로젝트 생성 즉시 의존성 설치 및 환경 변수(`.env`) 세팅을 완료합니다.
24
25
 
25
26
  ## 🚀 Quick Start (사용법)
26
27
 
@@ -28,7 +29,8 @@
28
29
 
29
30
  ```bash
30
31
  npx create-express-esm
31
- npm create express-esm
32
+ npm install -g create-express-esm
33
+ npm install create-express-esm
32
34
  ```
33
35
 
34
36
  또는 전역으로 설치하여 사용할 수도 있습니다
@@ -46,125 +48,47 @@ create-express-esm
46
48
  ```text
47
49
  my-app/
48
50
  ├── src/
49
-   ├── config/          # ⚙️ 환경변수DB 연결 설정
50
-   ├── controllers/     # 🕹️ 요청과 응답 처리 (Controller Layer)
51
-   ├── models/          # 🗄️ 데이터베이스 스키마 (Data Access Layer)
52
-   ├── routes/          # 🚦 API 라우팅 정의 (Route Definitions)
53
-   ├── services/        # 🧠 비즈니스 로직 (Service Layer) - 핵심 로직!
54
-   ├── app.js           # 🏗️ Express App 설정 (Middleware, CORS 등)
55
- │   └── server.js        # 🚀 서버 실행 진입점 (Entry Point)
56
- ├── .env                 # 🔐 환경 변수 (Port, DB Key 등)
57
- ├── .gitignore           # 🙈 Git 무시 설정
58
- └── package.json         # 📦 프로젝트 의존성 및 스크립트
51
+ ├── controllers/ # 🕹️ 요청 처리 응답 반환 (Controller Layer)
52
+ ├── services/ # 🧠 비즈니스 로직 처리 (Service Layer)
53
+ ├── routes/ # 🚦 API 엔드포인트 정의 (Route Layer)
54
+ ├── app.ts (or .js) # 🏗️ Express 설정 미들웨어
55
+ ├── server.ts (.js) # 🚀 서버 진입점 (Entry Point)
56
+ └── app.test.ts # 🧪 Vitest 샘플 테스트 코드
57
+ ├── .env # 🔐 환경 변수 (자동 생성)
58
+ ├── vitest.config.ts # 🧪 Vitest 설정 파일
59
+ ├── tsconfig.json # ⚙️ TS 컴파일러 설정 (TS 선택 시)
60
+ └── package.json # 📦 의존성 및 스크립트
59
61
  ```
60
62
 
61
- ## 🛠 Tech Stack (기술 스택)
62
-
63
- - **Runtime**: Node.js
64
- - **Framework**: Express.js
65
- - **Architecture**: Layered Pattern (Controller-Service-Model)
66
- - **Language**: JavaScript (ES6+ Modules)
67
- - **Tools**:
68
- - `dotenv` (환경변수 관리)
69
- - `cors` (Cross-Origin 리소스 공유 설정)
70
- - `morgan` (HTTP 요청 로그 기록)
71
- - `nodemon` (개발 생산성 향상/자동 재시작)
72
-
73
- ## 🛠️ Engineering Deep Dive
74
-
75
- 단순한 도구 개발을 넘어, Node.js의 런타임 환경과 모듈 시스템의 차이를 깊이 있게 이해하기 위해 진행한 프로젝트입니다.
76
-
77
- ### - Project Motivation (Why)
78
-
79
- 대부분의 Express 예제나 스캐폴딩 도구들이 여전히 CommonJS(require) 방식을 사용하고 있습니다. 하지만 최신 Node.js 생태계는 ES Modules(import)로 전환되고 있습니다.
80
- **"최신 문법을 지원하는 환경을 매번 수동으로 설정하는 비효율을 해결하자"**는 목표로 시작했으며, 프레임워크 없이 순수 Node.js의 `fs`, `path` 모듈을 다루며 CLI 도구의 동작 원리를 체득하고자 했습니다.
81
-
82
- ### - Architecture
83
-
84
- 1. **비동기 파일 시스템 제어**: 대량의 템플릿 파일을 생성할 때 I/O 블로킹을 막기 위해 `fs.promises`와 `async/await`를 사용하여 안정적인 파일 쓰기 작업을 구현했습니다.
85
- 2. **동적 템플릿 생성**: 사용자가 입력한 프로젝트 이름을 `package.json` 등에 동적으로 주입하여, 생성 즉시 실행 가능한 상태를 보장합니다.
63
+ 주인님, 요청하신 대로 🛠 Tech Stack (기술 스택) 섹션부터 마지막까지의 내용을 마크다운 코드로 정리해 드립니다. 이 부분은 주인님이 이번에 해결하신 기술적 도전 과제들이 고스란히 담겨 있어 포트폴리오로서의 가치가 매우 높습니다.
86
64
 
87
- ## 🔥 Challenges & Troubleshooting
88
-
89
- 개발 과정에서 마주친 기술적 난관과 해결 과정입니다.
90
-
91
- ### 1. ESM 환경에서의 경로 시스템 구축 (Transition to ESM)
92
-
93
- > **🔴 Problem:** `ReferenceError: __dirname is not defined`
94
-
95
- 프로젝트를 `type: "module"`(ESM)로 설정한 후, 템플릿 폴더 경로를 참조하기 위해 관습적으로 `__dirname`을 사용했으나 에러가 발생하며 앱이 종료되었습니다.
96
-
97
- **🔍 Root Cause & Solution**
98
-
99
- Node.js의 CommonJS 환경에서는 `__dirname`이 기본적으로 주입되지만, ESM 표준 스펙에는 이 변수가 존재하지 않습니다. 이를 해결하기 위해 `url`과 `path` 모듈을 조합하여 Polyfill(직접 구현) 했습니다.
100
-
101
- ```javascript
102
- import path from "path";
103
- import { fileURLToPath } from "url";
104
-
105
- // 1. 현재 파일의 URL(file://...)을 시스템 경로로 변환
106
- const __filename = fileURLToPath(import.meta.url);
107
-
108
- // 2. 파일 경로에서 디렉토리 경로만 추출하여 __dirname 구현
109
- const __dirname = path.dirname(__filename);
110
- ```
65
+ Markdown
111
66
 
112
- ### 2. CLI의 실행 위치와 파일 위치의 혼동 (Execution Context)
113
-
114
- > **🔴 Problem:** `ENOENT: no such file or directory`
115
-
116
- 로컬 테스트(`node bin/cli.js`) 시에는 정상 작동했으나, `npm link` 후 다른 경로(바탕화면 등)에서 실행했을 때 템플릿 폴더를 찾지 못하는 문제가 발생했습니다.
117
-
118
- **🔍 Root Cause**
119
-
120
- CLI 도구 개발 시 **'코드의 위치(Source)'**와 **'명령어 실행 위치(Target)'**가 다를 수 있음을 간과했습니다. 템플릿을 찾을 때 `process.cwd()`(사용자 위치)를 기준으로 탐색했기 때문에 발생한 문제였습니다.
121
-
122
- **✅ Solution**
123
-
124
- 리소스의 성격에 따라 기준 경로를 명확히 분리하여 해결했습니다.
125
-
126
- - **Source (Template)**: 코드가 설치된 곳에 항상 함께 존재하므로 `__dirname` 기준.
127
- - **Target (Project)**: 사용자가 명령어를 실행한 위치에 생성되어야 하므로 `process.cwd()` 기준.
128
-
129
- ```javascript
130
- // [Source] 템플릿 경로: 내 코드가 설치된 곳 기준
131
- const templateDir = path.join(__dirname, "../template");
132
-
133
- // [Target] 생성 경로: 사용자가 명령어를 실행한 곳 기준
134
- const targetDir = path.join(process.cwd(), projectName);
135
-
136
- // Copy: Source -> Target
137
- await copyDir(templateDir, targetDir);
138
- ```
139
-
140
- ### 3. 배포 파이프라인 최적화 (Appropriate Technology)
141
-
142
- > **🔴 Problem**
143
-
144
- 잦은 업데이트 과정에서 `버전 수정` -> `태그 생성` -> `깃 푸시` -> `npm 배포`라는 4단계 프로세스를 수동으로 반복하다 보니, 순서를 누락하거나 버전을 잘못 기입하는 휴먼 에러가 발생했습니다.
145
-
146
- **🔍 Approach & Solution**
147
-
148
- GitHub Actions와 같은 CI/CD 도구 도입을 고려했으나, 1인 개발 프로젝트 규모에 비해 설정 비용이 크고 오버엔지니어링이라는 판단이 들었습니다. 대신 Node.js의 내장 기능인 **NPM Scripts**를 활용하는 것이 가장 효율적인 **'적정 기술(Appropriate Technology)'**이라 판단했습니다.
67
+ ## 🛠 Tech Stack (기술 스택)
149
68
 
150
- ```json
151
- // package.json
152
- "scripts": {
153
- // patch 버전 업 -> 깃 태그 푸시 -> npm 배포를 명령어 한 줄로 원자적(Atomic) 실행
154
- "deploy": "npm version patch && git push origin main --tags && npm publish"
155
- }
156
- ```
69
+ - **Runtime**: Node.js (v20+)
70
+ - **Framework**: Express.js
71
+ - **Language**: JavaScript (ESM) / TypeScript 5.x
72
+ - **Testing**: Vitest, Supertest
73
+ - **Dev Tools**:
74
+ - `tsx` (TypeScript Execution Engine)
75
+ - `nodemon` (Hot Reload)
76
+ - `dotenv` (Environment Variables)
77
+ - `cors` (Cross-Origin Resource Sharing)
78
+ - `chalk` (CLI Styling)
157
79
 
158
80
  ## 📝 Retrospective
159
81
 
160
- - **Legacy to Modern**: Node.js 생태계가 CJS에서 ESM으로 전환되는 과도기에서 발생하는 호환성 문제를 직접 겪고 해결하며, 모던 자바스크립트 환경에 대한 이해도를 높였습니다.
161
- - **Consumer to Producer**: 항상 라이브러리를 사용하기만 하던 입장에서, 직접 도구를 만들어 배포하는 생산자가 되어봄으로써 오픈소스 생태계의 순환 구조를 체감했습니다.
162
- - **JavaScript to TypeScript (Next Step)**: 순수 ESM 환경을 구축하며 모듈 시스템은 표준화했으나, 컴파일 단계에서 오류를 잡을 수 없는 동적 타입 언어의 한계를 체감했습니다. 프로젝트의 안정성과 유지보수성을 높이기 위해 **TypeScript** 도입과 엄격한 타입 시스템의 필요성을 깨달았으며, 차기 버전에서는 이를 지원할 계획입니다.
82
+ - **표준화된 환경의 중요성**: CJS에서 ESM으로 넘어가는 과도기적 문제를 해결하며 모던 자바스크립트 모듈 시스템에 대한 깊은 이해를 얻었습니다.
83
+ - **UX 기반 설계**: 사용자가 프로젝트를 생성하자마자 `npm run dev`와 `npm test`를 즉시 실행할 있는 "Zero-Config" 환경을 제공하는 데 집중했습니다.
84
+ - **배포 프로세스의 성숙**: 수동 배포의 위험성을 CI/CD와 OIDC 도입을 통해 자동화하며 소프트웨어 릴리스 과정의 안정성을 확보했습니다.
163
85
 
164
86
  ## 🗺️ Roadmap (Future Plans)
165
87
 
166
- - [ ] **TypeScript Support**: `tsconfig.json` 자동 설정 `.ts` 템플릿 지원
167
- - [ ] **Test Environment**: Jest/Supertest 설정 자동화
88
+ - [x] **TypeScript Support**: `.ts` 템플릿 및 `tsx` 환경 최적화
89
+ - [x] **Test Environment**: Vitest 및 Supertest 설정 자동화
90
+ - [ ] **Interactive UI Upgrade**: `Clack` 라이브러리를 통한 시각적 CLI UI 개선
91
+ - [ ] **Database Integration**: Prisma/Sequelize 등 ORM 선택 옵션 추가
168
92
 
169
93
  ## 📝 License
170
94
 
package/bin/cli.js CHANGED
@@ -1,22 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { input, select } from '@inquirer/prompts'; // 현대적인 방식으로 교체
3
+ import { input, select, confirm } from '@inquirer/prompts';
4
4
  import fs from 'fs-extra';
5
5
  import path from 'path';
6
6
  import chalk from 'chalk';
7
7
  import { execSync } from 'child_process';
8
8
  import { fileURLToPath } from 'url';
9
9
 
10
- // ESM에서 __dirname 사용하기 위한 설정
11
10
  const __filename = fileURLToPath(import.meta.url);
12
11
  const __dirname = path.dirname(__filename);
13
12
 
14
- // 1.2.0 버전 정보 및 메인 로직
15
13
  async function run() {
16
14
  console.log(chalk.blue.bold('\n🚀 Create Express ESM 시작!\n'));
17
15
 
18
16
  try {
19
- // 1. 사용자 질문 (비동기 함수 방식으로 변경)
17
+ // 1. 사용자 질문
20
18
  const projectName = await input({
21
19
  message: '생성할 프로젝트 이름을 입력하세요:',
22
20
  default: 'my-app',
@@ -30,6 +28,11 @@ async function run() {
30
28
  ],
31
29
  });
32
30
 
31
+ const useTest = await confirm({
32
+ message: 'Vitest 테스트 환경을 추가하시겠습니까?',
33
+ default: true,
34
+ });
35
+
33
36
  const targetPath = path.join(process.cwd(), projectName);
34
37
  const templatePath = path.join(__dirname, '../template', language);
35
38
 
@@ -39,18 +42,11 @@ async function run() {
39
42
  process.exit(1);
40
43
  }
41
44
 
42
- // 3. 템플릿 복사
43
- console.log(chalk.cyan(`\n📂 [${language.toUpperCase()}] 템플릿을 복사하는 중...`));
44
-
45
- if (!fs.existsSync(templatePath)) {
46
- console.error(chalk.red(`\n❌ 오류: ${language} 템플릿 폴더를 찾을 수 없습니다.`));
47
- console.log(chalk.gray(`경로 확인: ${templatePath}`));
48
- process.exit(1);
49
- }
50
-
45
+ // 3. 기본 템플릿 복사
46
+ console.log(chalk.cyan(`\n📂 [${language.toUpperCase()}] 템플릿 구성을 시작합니다...`));
51
47
  await fs.copy(templatePath, targetPath);
52
48
 
53
- // 4. 도트 파일 변환 환경 설정
49
+ // 4. 도트 파일 변환 (_env -> .env 등)
54
50
  const renameMap = {
55
51
  'gitignore': '.gitignore',
56
52
  '_gitignore': '.gitignore',
@@ -60,46 +56,104 @@ async function run() {
60
56
  for (const [oldName, newName] of Object.entries(renameMap)) {
61
57
  const oldFilePath = path.join(targetPath, oldName);
62
58
  const newFilePath = path.join(targetPath, newName);
63
-
64
59
  if (await fs.pathExists(oldFilePath)) {
65
60
  await fs.move(oldFilePath, newFilePath, { overwrite: true });
66
61
  if (newName === '.env') {
67
- const exampleEnvPath = path.join(targetPath, '.env.example');
68
- await fs.copy(newFilePath, exampleEnvPath);
62
+ await fs.copy(newFilePath, path.join(targetPath, '.env.example'));
69
63
  }
70
64
  }
71
65
  }
72
66
 
73
- // 5. package.json 프로젝트 이름 수정
67
+ // 5. package.json 동적 수정
74
68
  const pkgPath = path.join(targetPath, 'package.json');
75
- if (await fs.pathExists(pkgPath)) {
76
- const pkg = await fs.readJson(pkgPath);
77
- pkg.name = projectName;
78
- await fs.writeJson(pkgPath, pkg, { spaces: 2 });
69
+ const pkg = await fs.readJson(pkgPath);
70
+ pkg.name = projectName;
71
+
72
+ // [추가된 부분] TypeScript 환경에서 ESM 에러를 방지하기 위한 tsx 설정
73
+ if (language === 'ts') {
74
+ console.log(chalk.yellow(`⚙️ TypeScript ESM 실행 환경(tsx)을 최적화하는 중...`));
75
+
76
+ // ts-node 대신 tsx를 사용하여 .js 확장자 임포트 문제 해결
77
+ pkg.scripts.dev = "nodemon --exec tsx src/server.ts";
78
+
79
+ // 의존성 교체
80
+ pkg.devDependencies = {
81
+ ...pkg.devDependencies,
82
+ "tsx": "^4.7.0"
83
+ };
84
+
85
+ // 기존에 ts-node가 있다면 제거 (중복 방지)
86
+ delete pkg.devDependencies['ts-node'];
79
87
  }
80
-
81
- console.log(chalk.green(`✅ 템플릿 구성 완료!`));
88
+
89
+ // Vitest 설정 (이슈 #3 구현 부분)
90
+ if (useTest) {
91
+ console.log(chalk.yellow(`🧪 Vitest 설정 및 샘플 테스트를 생성하는 중...`));
92
+
93
+ pkg.scripts = {
94
+ ...pkg.scripts,
95
+ "test": "vitest",
96
+ "test:ui": "vitest --ui",
97
+ "test:run": "vitest run"
98
+ };
99
+
100
+ const testDeps = {
101
+ "vitest": "^1.0.0",
102
+ "supertest": "^6.3.3"
103
+ };
104
+
105
+ if (language === 'ts') {
106
+ testDeps["@types/supertest"] = "^2.0.12";
107
+ }
108
+
109
+ pkg.devDependencies = {
110
+ ...pkg.devDependencies,
111
+ ...testDeps
112
+ };
113
+
114
+ // Vitest 설정 파일 생성
115
+ const configExt = language === 'ts' ? 'ts' : 'js';
116
+ const configContent = `import { defineConfig } from 'vitest/config';
117
+
118
+ export default defineConfig({
119
+ test: {
120
+ globals: true,
121
+ environment: 'node',
122
+ },
123
+ });`;
124
+ await fs.writeFile(path.join(targetPath, `vitest.config.${configExt}`), configContent);
125
+
126
+ // 샘플 테스트 파일 생성
127
+ const testFileExt = language === 'ts' ? 'ts' : 'js';
128
+ const testContent = `import { describe, it, expect } from 'vitest';
129
+ import request from 'supertest';
130
+ import app from './app.js';
131
+
132
+ describe('API Health Check Test', () => {
133
+ it('GET / 요청이 성공해야 한다', async () => {
134
+ const res = await request(app).get('/');
135
+ expect(res.status).toBe(200);
136
+ expect(res.text).toContain('Server is Running');
137
+ });
138
+ });`;
139
+ await fs.writeFile(path.join(targetPath, `src/app.test.${testFileExt}`), testContent);
140
+ }
141
+
142
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
143
+ console.log(chalk.green(`✅ 모든 구성 완료!`));
82
144
 
83
145
  // 6. 패키지 자동 설치
84
- console.log(chalk.yellow(`\n📦 패키지 자동 설치를 진행합니다... (npm install)`));
85
-
86
- execSync('npm install', {
87
- cwd: targetPath,
88
- stdio: 'inherit'
89
- });
146
+ console.log(chalk.yellow(`\n📦 의존성 패키지를 설치합니다... (npm install)`));
147
+ execSync('npm install', { cwd: targetPath, stdio: 'inherit' });
90
148
 
91
- console.log(chalk.green(`\n✨ 모든 설치가 완료되었습니다!`));
92
- console.log(chalk.white(`\n다음 명령어로 시작하세요:\n`));
149
+ console.log(chalk.green(`\n✨ 프로젝트 생성 성공!`));
150
+ console.log(chalk.white(`\n다음 명령어를 입력해 보세요:\n`));
93
151
  console.log(chalk.cyan(` cd ${projectName}`));
94
- if (language === 'ts') {
95
- console.log(chalk.cyan(` npm run dev (또는 npm run build)`));
96
- } else {
97
- console.log(chalk.cyan(` npm run dev`));
98
- }
99
- console.log('\n');
152
+ if (useTest) console.log(chalk.cyan(` npm test`));
153
+ console.log(chalk.cyan(` npm run dev\n`));
100
154
 
101
155
  } catch (error) {
102
- if (error.name === 'ExitPromptError') {
156
+ if (error.name === 'ExitPromptError') { // 오타 수정: ExitPnromptError -> ExitPromptError
103
157
  console.log(chalk.yellow('\n\n👋 설치를 중단했습니다.'));
104
158
  } else {
105
159
  console.error(chalk.red('\n❌ 오류 발생:'), error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-express-esm",
3
- "version": "1.1.5",
3
+ "version": "1.1.9",
4
4
  "description": "A modern CLI tool to bootstrap Express.js applications with ES Modules and Layered Architecture.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -24,6 +24,7 @@
24
24
  "author": "munjuin",
25
25
  "license": "MIT",
26
26
  "dependencies": {
27
+ "@inquirer/prompts": "^8.1.0",
27
28
  "chalk": "^5.6.2",
28
29
  "commander": "^14.0.2",
29
30
  "fs-extra": "^11.3.2",
@@ -36,5 +37,8 @@
36
37
  "bugs": {
37
38
  "url": "https://github.com/munjuin/create-express-esm/issues"
38
39
  },
39
- "homepage": "https://github.com/munjuin/create-express-esm#readme"
40
+ "homepage": "https://github.com/munjuin/create-express-esm#readme",
41
+ "devDependencies": {
42
+ "tsx": "^4.21.0"
43
+ }
40
44
  }
@@ -3,9 +3,9 @@
3
3
  "version": "1.0.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
- "start": "node dist/server.js",
7
- "dev": "nodemon --exec ts-node src/server.ts",
8
- "build": "tsc"
6
+ "dev": "nodemon --exec tsx src/server.ts",
7
+ "build": "tsc",
8
+ "start": "node dist/server.js"
9
9
  },
10
10
  "dependencies": {
11
11
  "express": "^4.18.2",