create-mendix-widget-gleam 1.0.0

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.
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { main } from "../src/index.mjs";
3
+
4
+ main(process.argv.slice(2));
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "create-mendix-widget-gleam",
3
+ "version": "1.0.0",
4
+ "description": "Scaffold a Mendix Pluggable Widget powered by Gleam",
5
+ "type": "module",
6
+ "license": "Apache-2.0",
7
+ "bin": "bin/create-mendix-widget-gleam.mjs",
8
+ "files": [
9
+ "bin/",
10
+ "src/",
11
+ "template/"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18"
15
+ },
16
+ "keywords": [
17
+ "mendix",
18
+ "gleam",
19
+ "widget",
20
+ "pluggable-widget",
21
+ "scaffold",
22
+ "create"
23
+ ]
24
+ }
package/src/index.mjs ADDED
@@ -0,0 +1,291 @@
1
+ /**
2
+ * create-mendix-widget-gleam 메인 오케스트레이션
3
+ */
4
+
5
+ import { resolve, dirname, join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { mkdir } from "node:fs/promises";
8
+ import { execSync } from "node:child_process";
9
+ import { collectOptions } from "./prompts.mjs";
10
+ import { generateNames } from "./naming.mjs";
11
+ import { getPmConfig } from "./pm.mjs";
12
+ import { scaffold } from "./scaffold.mjs";
13
+
14
+ const __dirname = dirname(fileURLToPath(import.meta.url));
15
+ const TEMPLATE_DIR = resolve(__dirname, "..", "template");
16
+
17
+ const BOLD = "\x1b[1m";
18
+ const RESET = "\x1b[0m";
19
+ const GREEN = "\x1b[32m";
20
+ const CYAN = "\x1b[36m";
21
+ const DIM = "\x1b[2m";
22
+ const YELLOW = "\x1b[33m";
23
+
24
+ const VERSION = "1.0.0";
25
+
26
+ const HELP = `
27
+ ${BOLD}create-mendix-widget-gleam${RESET} — Gleam + Mendix Pluggable Widget 프로젝트 생성
28
+
29
+ ${BOLD}사용법:${RESET}
30
+ npx create-mendix-widget-gleam [project-name]
31
+
32
+ ${BOLD}옵션:${RESET}
33
+ --help, -h 도움말 표시
34
+ --version, -v 버전 표시
35
+
36
+ ${BOLD}예시:${RESET}
37
+ npx create-mendix-widget-gleam my-cool-widget
38
+ npx create-mendix-widget-gleam MyCoolWidget
39
+ `;
40
+
41
+ export async function main(args) {
42
+ // 플래그 처리
43
+ if (args.includes("--help") || args.includes("-h")) {
44
+ console.log(HELP);
45
+ return;
46
+ }
47
+ if (args.includes("--version") || args.includes("-v")) {
48
+ console.log(VERSION);
49
+ return;
50
+ }
51
+
52
+ console.log(
53
+ `\n${BOLD}${CYAN}create-mendix-widget-gleam${RESET} ${DIM}v${VERSION}${RESET}\n`,
54
+ );
55
+
56
+ // CLI 인자에서 프로젝트명 추출 (플래그 제외)
57
+ const positional = args.filter((a) => !a.startsWith("-"));
58
+ const cliProjectName = positional[0] || null;
59
+
60
+ // 프롬프트로 설정 수집
61
+ const { projectName, pm } = await collectOptions(cliProjectName);
62
+
63
+ // 이름 변환
64
+ const names = generateNames(projectName);
65
+ if (!names) {
66
+ console.error(`${YELLOW}오류: 유효하지 않은 프로젝트 이름입니다.${RESET}`);
67
+ process.exit(1);
68
+ }
69
+
70
+ const pmConfig = getPmConfig(pm);
71
+ const targetDir = resolve(process.cwd(), names.kebabCase);
72
+
73
+ // 요약 표시
74
+ console.log(`\n${BOLD}프로젝트 설정:${RESET}`);
75
+ console.log(` 디렉토리: ${CYAN}${names.kebabCase}/${RESET}`);
76
+ console.log(` 위젯 이름: ${names.pascalCase}`);
77
+ console.log(` Gleam 모듈: ${names.snakeCase}`);
78
+ console.log(` 패키지 매니저: ${pm}`);
79
+ console.log();
80
+
81
+ // 디렉토리 생성
82
+ await mkdir(targetDir, { recursive: true });
83
+
84
+ // 템플릿 스케폴딩
85
+ console.log(`${DIM}파일 생성 중...${RESET}`);
86
+ const created = await scaffold(TEMPLATE_DIR, targetDir, names, pmConfig);
87
+ console.log(`${GREEN}✓${RESET} ${created.length}개 파일 생성 완료`);
88
+
89
+ // CLAUDE.md 생성
90
+ await generateClaudeMd(targetDir, names, pm, pmConfig);
91
+ console.log(`${GREEN}✓${RESET} CLAUDE.md 생성 완료`);
92
+
93
+ // README.md 생성
94
+ await generateReadme(targetDir, names, pm, pmConfig);
95
+ console.log(`${GREEN}✓${RESET} README.md 생성 완료`);
96
+
97
+ // git init
98
+ try {
99
+ execSync("git init", { cwd: targetDir, stdio: "ignore" });
100
+ console.log(`${GREEN}✓${RESET} git 저장소 초기화 완료`);
101
+ } catch {
102
+ // git이 없어도 계속 진행
103
+ }
104
+
105
+ // 완료 메시지
106
+ console.log(`
107
+ ${GREEN}${BOLD}프로젝트가 생성되었습니다!${RESET}
108
+
109
+ ${BOLD}다음 단계:${RESET}
110
+
111
+ ${CYAN}cd ${names.kebabCase}${RESET}
112
+ ${CYAN}gleam run -m scripts/install${RESET} ${DIM}# 의존성 설치${RESET}
113
+ ${CYAN}gleam run -m scripts/dev${RESET} ${DIM}# 개발 서버 시작${RESET}
114
+ ${CYAN}gleam run -m scripts/build${RESET} ${DIM}# 프로덕션 빌드${RESET}
115
+ `);
116
+ }
117
+
118
+ /** CLAUDE.md 생성 */
119
+ async function generateClaudeMd(targetDir, names, pm, pmConfig) {
120
+ const { writeFile } = await import("node:fs/promises");
121
+
122
+ const content = `# ${names.pascalCase}
123
+
124
+ Gleam 언어로 Mendix Pluggable Widget을 개발하여 "Hello World"를 화면에 렌더링하는 프로젝트.
125
+
126
+ ## Goal
127
+
128
+ **JSX를 사용하지 않고, 오직 Gleam으로만** 위젯을 작성한다. Gleam 코드를 JavaScript로 컴파일하고, 컴파일된 JS가 곧 Mendix Pluggable Widget의 진입점이 된다.
129
+
130
+ ## Tech Stack
131
+
132
+ - **Gleam** → JavaScript 컴파일 (target: javascript)
133
+ - **Gleam FFI** (\`@external\` 어노테이션 + \`.ffi.mjs\` 파일) — React API를 Gleam에서 직접 호출
134
+ - **Mendix Pluggable Widget** (React 19)
135
+ - **Package Manager**: ${pm} (npm 의존성은 \`gleam run -m scripts/install\`로 설치)
136
+ - **Build**: \`@mendix/pluggable-widgets-tools\` (Rollup 기반)
137
+
138
+ ## Architecture
139
+
140
+ \`\`\`
141
+ src/
142
+ widget/ # 핵심 Gleam 코드 (개발자가 작업하는 곳)
143
+ ${names.snakeCase}.gleam # 위젯 메인 모듈
144
+ ${names.snakeCase}_ffi.mjs # React FFI 어댑터
145
+ editor_config.gleam # Studio Pro 속성 패널 설정
146
+ scripts/ # 빌드/개발 스크립트 (gleam run -m으로 실행)
147
+ cmd.gleam # 셸 명령어 실행 유틸리티
148
+ cmd_ffi.mjs # Node.js child_process FFI
149
+ install.gleam # npm 의존성 설치
150
+ build.gleam # 프로덕션 빌드
151
+ dev.gleam # 개발 서버
152
+ start.gleam # Mendix 테스트 프로젝트 연동
153
+ release.gleam # 릴리즈 빌드
154
+ lint.gleam # ESLint 실행
155
+ lint_fix.gleam # ESLint 자동 수정
156
+ ${names.pascalCase}.js # 브릿지 진입점
157
+ ${names.pascalCase}.editorConfig.js # 브릿지 (editorConfig)
158
+ ${names.pascalCase}.xml # 위젯 속성 정의
159
+ package.xml # Mendix 패키지 매니페스트
160
+ ui/
161
+ ${names.pascalCase}.css # 위젯 스타일시트
162
+ gleam.toml # Gleam 프로젝트 설정
163
+ docs/
164
+ gleam_language_tour.md # Gleam 언어 레퍼런스
165
+ \`\`\`
166
+
167
+ ## Build Pipeline
168
+
169
+ \`\`\`
170
+ [src/widget/${names.snakeCase}.gleam] + [src/widget/${names.snakeCase}_ffi.mjs]
171
+ ↓ gleam run -m scripts/build
172
+ [build/dev/javascript/${names.snakeCase}/widget/${names.snakeCase}.mjs]
173
+ ↓ src/${names.pascalCase}.js (브릿지)가 import
174
+ ↓ Rollup (pluggable-widgets-tools build:web)
175
+ [dist/1.0.0/mendix.${names.lowerCase}.${names.pascalCase}.mpk]
176
+ \`\`\`
177
+
178
+ ## Commands
179
+
180
+ \`\`\`bash
181
+ gleam run -m scripts/install # 의존성 설치
182
+ gleam run -m scripts/build # 위젯 프로덕션 빌드 (.mpk 생성)
183
+ gleam run -m scripts/dev # 개발 서버 (HMR, port 3000)
184
+ gleam run -m scripts/start # Mendix 테스트 프로젝트와 연동 개발
185
+ gleam run -m scripts/lint # ESLint 실행
186
+ gleam run -m scripts/lint_fix # ESLint 자동 수정
187
+ gleam run -m scripts/release # 릴리즈 빌드
188
+ gleam build --target javascript # Gleam → JS 컴파일만
189
+ gleam test # Gleam 테스트 실행
190
+ gleam format # Gleam 코드 포맷팅
191
+ \`\`\`
192
+
193
+ ## Gleam FFI Convention
194
+
195
+ - FFI 파일명: \`<module_name>_ffi.mjs\`
196
+ - \`@external(javascript, "./<module>_ffi.mjs", "<function>")\` 형식으로 바인딩
197
+ - FFI 파일에는 React API 래핑만 작성. 위젯 로직은 반드시 Gleam으로 작성
198
+
199
+ ## Mendix Widget Conventions
200
+
201
+ - 위젯 ID: \`mendix.${names.lowerCase}.${names.pascalCase}\`
202
+ - JSX 파일을 작성하지 않는다. 모든 React 로직은 Gleam + FFI로 구현
203
+ - Redraw 등 외부 Gleam React 라이브러리는 사용하지 않는다
204
+
205
+ ## Code Style
206
+
207
+ - Gleam 파일: \`gleam format\` 사용
208
+ - FFI 파일(\`.ffi.mjs\`): React API 노출만 담당, 최소한으로 유지
209
+ - 한국어 주석 사용
210
+ `;
211
+
212
+ await writeFile(join(targetDir, "CLAUDE.md"), content, "utf-8");
213
+ }
214
+
215
+ /** README.md 생성 */
216
+ async function generateReadme(targetDir, names, pm, pmConfig) {
217
+ const { writeFile } = await import("node:fs/promises");
218
+
219
+ const runCmd = pm === "npm" ? "npm run" : pm;
220
+
221
+ const content = `# ${names.pascalCase}
222
+
223
+ Gleam 언어로 작성된 Mendix Pluggable Widget.
224
+
225
+ ## 시작하기
226
+
227
+ ### 사전 요구사항
228
+
229
+ - [Gleam](https://gleam.run/getting-started/installing/) (최신 버전)
230
+ - [Node.js](https://nodejs.org/) (v18+)
231
+ - ${pm}
232
+
233
+ ### 설치
234
+
235
+ \`\`\`bash
236
+ gleam run -m scripts/install
237
+ \`\`\`
238
+
239
+ ### 개발
240
+
241
+ \`\`\`bash
242
+ gleam run -m scripts/dev
243
+ \`\`\`
244
+
245
+ ### 빌드
246
+
247
+ \`\`\`bash
248
+ gleam run -m scripts/build
249
+ \`\`\`
250
+
251
+ 빌드 결과물(\`.mpk\`)은 \`dist/\` 디렉토리에 생성됩니다.
252
+
253
+ ### 기타 명령어
254
+
255
+ \`\`\`bash
256
+ gleam run -m scripts/start # Mendix 테스트 프로젝트 연동
257
+ gleam run -m scripts/lint # ESLint 실행
258
+ gleam run -m scripts/lint_fix # ESLint 자동 수정
259
+ gleam run -m scripts/release # 릴리즈 빌드
260
+ gleam build --target javascript # Gleam → JS 컴파일만
261
+ gleam test # 테스트 실행
262
+ gleam format # 코드 포맷팅
263
+ \`\`\`
264
+
265
+ ## 프로젝트 구조
266
+
267
+ \`\`\`
268
+ src/
269
+ widget/ # Gleam 위젯 코드
270
+ ${names.snakeCase}.gleam # 메인 위젯 모듈
271
+ ${names.snakeCase}_ffi.mjs # React FFI 어댑터
272
+ editor_config.gleam # Studio Pro 속성 패널
273
+ scripts/ # 빌드/개발 스크립트
274
+ ${names.pascalCase}.js # Mendix 브릿지 진입점
275
+ ${names.pascalCase}.xml # 위젯 속성 정의
276
+ \`\`\`
277
+
278
+ ## 기술 스택
279
+
280
+ - **Gleam** → JavaScript 컴파일
281
+ - **Gleam FFI** — React API 직접 바인딩
282
+ - **Mendix Pluggable Widget** (React 19)
283
+ - **${pm}** — 패키지 매니저
284
+
285
+ ## 라이센스
286
+
287
+ Apache-2.0
288
+ `;
289
+
290
+ await writeFile(join(targetDir, "README.md"), content, "utf-8");
291
+ }
package/src/naming.mjs ADDED
@@ -0,0 +1,56 @@
1
+ /**
2
+ * 이름 변환 유틸리티
3
+ * 사용자 입력을 다양한 형식으로 변환한다.
4
+ */
5
+
6
+ /** 입력 문자열을 단어 배열로 분리 */
7
+ export function splitWords(input) {
8
+ // PascalCase / camelCase 경계에서 분리
9
+ let result = input.replace(/([a-z])([A-Z])/g, "$1 $2");
10
+ // 숫자-문자 경계
11
+ result = result.replace(/([0-9])([a-zA-Z])/g, "$1 $2");
12
+ result = result.replace(/([a-zA-Z])([0-9])/g, "$1 $2");
13
+ // 구분자(-, _, 공백)로 분리
14
+ return result
15
+ .split(/[-_\s]+/)
16
+ .map((w) => w.trim().toLowerCase())
17
+ .filter((w) => w.length > 0);
18
+ }
19
+
20
+ /** PascalCase: MyCoolWidget */
21
+ export function toPascalCase(words) {
22
+ return words.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
23
+ }
24
+
25
+ /** snake_case: my_cool_widget */
26
+ export function toSnakeCase(words) {
27
+ return words.join("_");
28
+ }
29
+
30
+ /** lowercase: mycoolwidget */
31
+ export function toLowerCase(words) {
32
+ return words.join("");
33
+ }
34
+
35
+ /** Display Name: My Cool Widget */
36
+ export function toDisplayName(words) {
37
+ return words.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
38
+ }
39
+
40
+ /** kebab-case: my-cool-widget */
41
+ export function toKebabCase(words) {
42
+ return words.join("-");
43
+ }
44
+
45
+ /** 모든 형식을 한 번에 생성 */
46
+ export function generateNames(input) {
47
+ const words = splitWords(input);
48
+ if (words.length === 0) return null;
49
+ return {
50
+ pascalCase: toPascalCase(words),
51
+ snakeCase: toSnakeCase(words),
52
+ lowerCase: toLowerCase(words),
53
+ displayName: toDisplayName(words),
54
+ kebabCase: toKebabCase(words),
55
+ };
56
+ }
package/src/pm.mjs ADDED
@@ -0,0 +1,26 @@
1
+ /**
2
+ * 패키지 매니저 설정
3
+ */
4
+
5
+ const PM_CONFIG = {
6
+ npm: { install: "npm install", runner: "npx" },
7
+ yarn: { install: "yarn install", runner: "npx" },
8
+ pnpm: { install: "pnpm install", runner: "pnpm exec" },
9
+ bun: { install: "bun install", runner: "bunx" },
10
+ };
11
+
12
+ export const PM_CHOICES = Object.keys(PM_CONFIG);
13
+
14
+ /** 패키지 매니저별 명령어 반환 */
15
+ export function getPmConfig(pm) {
16
+ return PM_CONFIG[pm] || PM_CONFIG.npm;
17
+ }
18
+
19
+ /** 실행 환경에서 패키지 매니저 자동 감지 */
20
+ export function detectPm() {
21
+ const ua = process.env.npm_config_user_agent || "";
22
+ if (ua.startsWith("pnpm/")) return "pnpm";
23
+ if (ua.startsWith("yarn/")) return "yarn";
24
+ if (ua.startsWith("bun/")) return "bun";
25
+ return "npm";
26
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * 인터랙티브 프롬프트 (node:readline 기반)
3
+ */
4
+
5
+ import { createInterface } from "node:readline/promises";
6
+ import { stdin, stdout } from "node:process";
7
+ import { existsSync } from "node:fs";
8
+ import { resolve } from "node:path";
9
+ import { splitWords } from "./naming.mjs";
10
+ import { PM_CHOICES, detectPm } from "./pm.mjs";
11
+
12
+ const BOLD = "\x1b[1m";
13
+ const DIM = "\x1b[2m";
14
+ const RESET = "\x1b[0m";
15
+ const CYAN = "\x1b[36m";
16
+ const YELLOW = "\x1b[33m";
17
+
18
+ /** 프로젝트 이름 검증 */
19
+ function validateName(name) {
20
+ if (!name || name.trim().length === 0) {
21
+ return "프로젝트 이름을 입력해주세요.";
22
+ }
23
+ const words = splitWords(name.trim());
24
+ if (words.length === 0) {
25
+ return "유효한 영문자를 포함해야 합니다.";
26
+ }
27
+ // 영문자, 숫자, -, _ 만 허용
28
+ if (!/^[a-zA-Z][a-zA-Z0-9\-_]*$/.test(name.trim())) {
29
+ return "영문자로 시작해야 하며, 영문자/숫자/-/_ 만 사용 가능합니다.";
30
+ }
31
+ return null;
32
+ }
33
+
34
+ /** 인터랙티브 프롬프트로 설정 수집 */
35
+ export async function collectOptions(cliProjectName) {
36
+ const rl = createInterface({ input: stdin, output: stdout });
37
+ let done = false;
38
+
39
+ // Ctrl+C 처리 (프롬프트 완료 전에만)
40
+ rl.on("close", () => {
41
+ if (!done) {
42
+ console.log("\n취소되었습니다.");
43
+ process.exit(0);
44
+ }
45
+ });
46
+
47
+ let projectName = cliProjectName;
48
+
49
+ try {
50
+ // 1. 프로젝트 이름
51
+ if (!projectName) {
52
+ projectName = await rl.question(
53
+ `${BOLD}프로젝트 이름:${RESET} `,
54
+ );
55
+ }
56
+
57
+ const nameError = validateName(projectName);
58
+ if (nameError) {
59
+ done = true;
60
+ rl.close();
61
+ console.error(`\n${YELLOW}오류: ${nameError}${RESET}`);
62
+ process.exit(1);
63
+ }
64
+
65
+ projectName = projectName.trim();
66
+
67
+ // 디렉토리 충돌 확인
68
+ const targetDir = resolve(process.cwd(), projectName);
69
+ if (existsSync(targetDir)) {
70
+ done = true;
71
+ rl.close();
72
+ console.error(
73
+ `\n${YELLOW}오류: '${projectName}' 디렉토리가 이미 존재합니다.${RESET}`,
74
+ );
75
+ process.exit(1);
76
+ }
77
+
78
+ // 2. 패키지 매니저 선택
79
+ const detected = detectPm();
80
+ console.log(
81
+ `\n${BOLD}패키지 매니저 선택:${RESET} ${DIM}(감지: ${detected})${RESET}`,
82
+ );
83
+ PM_CHOICES.forEach((pm, i) => {
84
+ const marker = pm === detected ? ` ${CYAN}← 감지됨${RESET}` : "";
85
+ console.log(` ${i + 1}) ${pm}${marker}`);
86
+ });
87
+
88
+ let pmAnswer = "";
89
+ try {
90
+ pmAnswer = await rl.question(
91
+ `선택 ${DIM}(1-${PM_CHOICES.length}, 기본: ${detected})${RESET}: `,
92
+ );
93
+ } catch {
94
+ // stdin이 닫혀도 기본값 사용
95
+ }
96
+
97
+ let pm = detected;
98
+ const pmIndex = parseInt(pmAnswer, 10);
99
+ if (pmIndex >= 1 && pmIndex <= PM_CHOICES.length) {
100
+ pm = PM_CHOICES[pmIndex - 1];
101
+ } else if (pmAnswer.trim() && PM_CHOICES.includes(pmAnswer.trim())) {
102
+ pm = pmAnswer.trim();
103
+ }
104
+
105
+ done = true;
106
+ rl.close();
107
+ return { projectName, pm };
108
+ } catch (err) {
109
+ done = true;
110
+ rl.close();
111
+ if (err.code === "ERR_USE_AFTER_CLOSE") {
112
+ process.exit(0);
113
+ }
114
+ throw err;
115
+ }
116
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * 파일 복사 + 템플릿 치환
3
+ */
4
+
5
+ import { readdir, readFile, writeFile, mkdir, copyFile } from "node:fs/promises";
6
+ import { join, relative } from "node:path";
7
+
8
+ /** 바이너리 판별용 확장자 */
9
+ const BINARY_EXTS = new Set([".png", ".jpg", ".gif", ".ico", ".woff", ".woff2", ".ttf", ".eot"]);
10
+
11
+ /** 재귀적으로 디렉토리 내 모든 파일 경로를 수집 */
12
+ async function walkDir(dir) {
13
+ const entries = await readdir(dir, { withFileTypes: true });
14
+ const files = [];
15
+ for (const entry of entries) {
16
+ const fullPath = join(dir, entry.name);
17
+ if (entry.isDirectory()) {
18
+ files.push(...(await walkDir(fullPath)));
19
+ } else {
20
+ files.push(fullPath);
21
+ }
22
+ }
23
+ return files;
24
+ }
25
+
26
+ // npm publish 시 제외되는 dotfile을 언더스코어 접두사로 보관하고 복원
27
+ const DOTFILE_MAP = { _gitignore: ".gitignore" };
28
+
29
+ /** 파일명에서 플레이스홀더 치환 + dotfile 복원 */
30
+ function replaceFileName(name, names) {
31
+ if (DOTFILE_MAP[name]) return DOTFILE_MAP[name];
32
+ return name
33
+ .replace(/__WidgetName__/g, names.pascalCase)
34
+ .replace(/__widget_name__/g, names.snakeCase);
35
+ }
36
+
37
+ /** 파일 내용에서 플레이스홀더 치환 */
38
+ function replaceContent(content, names, pmConfig) {
39
+ return content
40
+ .replace(/\{\{PASCAL_CASE\}\}/g, names.pascalCase)
41
+ .replace(/\{\{SNAKE_CASE\}\}/g, names.snakeCase)
42
+ .replace(/\{\{LOWERCASE\}\}/g, names.lowerCase)
43
+ .replace(/\{\{DISPLAY_NAME\}\}/g, names.displayName)
44
+ .replace(/\{\{KEBAB_CASE\}\}/g, names.kebabCase)
45
+ .replace(/\{\{INSTALL_COMMAND\}\}/g, pmConfig.install)
46
+ .replace(/\{\{RUNNER\}\}/g, pmConfig.runner);
47
+ }
48
+
49
+ /**
50
+ * 템플릿을 대상 디렉토리에 스케폴딩
51
+ * @param {string} templateDir - 템플릿 디렉토리 경로
52
+ * @param {string} targetDir - 생성할 프로젝트 디렉토리 경로
53
+ * @param {object} names - 이름 변환 결과
54
+ * @param {object} pmConfig - 패키지 매니저 설정
55
+ */
56
+ export async function scaffold(templateDir, targetDir, names, pmConfig) {
57
+ const files = await walkDir(templateDir);
58
+ const created = [];
59
+
60
+ for (const srcPath of files) {
61
+ // 템플릿 기준 상대 경로
62
+ const relPath = relative(templateDir, srcPath);
63
+
64
+ // 경로의 각 부분에서 파일명 치환
65
+ const destRelPath = relPath
66
+ .split(/[\\/]/)
67
+ .map((part) => replaceFileName(part, names))
68
+ .join("/");
69
+
70
+ const destPath = join(targetDir, destRelPath);
71
+ const ext = srcPath.substring(srcPath.lastIndexOf(".")).toLowerCase();
72
+
73
+ // 디렉토리 생성
74
+ await mkdir(join(destPath, ".."), { recursive: true });
75
+
76
+ if (BINARY_EXTS.has(ext)) {
77
+ // 바이너리 파일은 그대로 복사
78
+ await copyFile(srcPath, destPath);
79
+ } else {
80
+ // 텍스트 파일은 내용 치환
81
+ const content = await readFile(srcPath, "utf-8");
82
+ const replaced = replaceContent(content, names, pmConfig);
83
+ await writeFile(destPath, replaced, "utf-8");
84
+ }
85
+
86
+ created.push(destRelPath);
87
+ }
88
+
89
+ return created;
90
+ }
@@ -0,0 +1,21 @@
1
+ # Set the default behavior, in case people don't have core.autocrlf set.
2
+ * text=auto
3
+
4
+ # Explicitly declare text files you want to always be normalized and converted
5
+ # to native line endings on checkout.
6
+ *.ts text eol=lf
7
+ *.tsx text eol=lf
8
+ *.js text eol=lf
9
+ *.jsx text eol=lf
10
+ *.css text eol=lf
11
+ *.scss text eol=lf
12
+ *.json text eol=lf
13
+ *.xml text eol=lf
14
+ *.md text eol=lf
15
+ *.gitattributes eol=lf
16
+ *.gitignore eol=lf
17
+
18
+ # Denote all files that are truly binary and should not be modified.
19
+ *.png binary
20
+ *.jpg binary
21
+ *.gif binary
@@ -0,0 +1 @@
1
+ tests/testProject/
@@ -0,0 +1,15 @@
1
+ The Apache License v2.0
2
+
3
+ Copyright © Mendix Technology BV 2026. All rights reserved.
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
@@ -0,0 +1,48 @@
1
+ # 의존성
2
+ node_modules/
3
+ .yarn/
4
+ .pnp.*
5
+
6
+ # 패키지 매니저 lock 파일
7
+ package-lock.json
8
+ yarn.lock
9
+ pnpm-lock.yaml
10
+ bun.lockb
11
+ bun.lock
12
+
13
+ # 패키지 매니저 캐시/설정
14
+ .npmrc
15
+ .yarnrc
16
+ .yarnrc.yml
17
+ .pnpmfile.cjs
18
+ .npmignore
19
+
20
+ # Gleam 컴파일 출력
21
+ build/
22
+
23
+ # Mendix 위젯 빌드 출력
24
+ dist/
25
+
26
+ # 환경 변수
27
+ .env
28
+
29
+ # 로그
30
+ *.log
31
+
32
+ # OS
33
+ .DS_Store
34
+
35
+ # IDE
36
+ .idea/
37
+ .vscode/
38
+ *.launch
39
+
40
+ # Mendix 테스트 프로젝트
41
+ tests/testProject/
42
+
43
+ # 테스트 산출물
44
+ coverage/
45
+ **/e2e/diffs/
46
+ **/screenshot/
47
+ **/screenshot-results/
48
+ **/artifacts/