create-mendix-widget-gleam 2.0.12 → 2.0.13

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/src/index.mjs CHANGED
@@ -1,652 +1,211 @@
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
- // Gleam → JS 컴파일
106
- console.log(`\n${BOLD}Gleam 컴파일 중...${RESET}\n`);
107
- try {
108
- execSync("gleam build --target javascript", {
109
- cwd: targetDir,
110
- stdio: "inherit",
111
- });
112
- console.log(`\n${GREEN}✓${RESET} Gleam 컴파일 완료`);
113
- } catch {
114
- console.error(
115
- `\n${YELLOW}⚠ Gleam 컴파일 실패. 프로젝트 디렉토리에서 직접 실행하세요:${RESET}`,
116
- );
117
- console.error(` ${CYAN}gleam build --target javascript${RESET}\n`);
118
- }
119
-
120
- // 의존성 설치 (사용자가 선택한 패키지 매니저 사용)
121
- console.log(`\n${BOLD}의존성 설치 중... (${pm})${RESET}\n`);
122
- try {
123
- execSync(pmConfig.install, {
124
- cwd: targetDir,
125
- stdio: "inherit",
126
- });
127
- console.log(`\n${GREEN}✓${RESET} 의존성 설치 완료`);
128
- } catch {
129
- console.error(
130
- `\n${YELLOW}⚠ 의존성 설치 실패. 프로젝트 디렉토리에서 직접 실행하세요:${RESET}`,
131
- );
132
- console.error(` ${CYAN}${pmConfig.install}${RESET}\n`);
133
- }
134
-
135
- // Playwright Chromium 브라우저 설치 (미설치 시에만)
136
- try {
137
- const chromiumExists =
138
- execSync(
139
- `node -e "const fs=require('fs'),pw=require('playwright');process.stdout.write(String(fs.existsSync(pw.chromium.executablePath())))"`,
140
- { cwd: targetDir, encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] },
141
- ).trim() === "true";
142
-
143
- if (!chromiumExists) {
144
- console.log(`\n${BOLD}Playwright Chromium 설치 중...${RESET}\n`);
145
- try {
146
- execSync("npx playwright install chromium", {
147
- cwd: targetDir,
148
- stdio: "inherit",
149
- });
150
- console.log(`\n${GREEN}✓${RESET} Playwright Chromium 설치 완료`);
151
- } catch {
152
- console.error(
153
- `\n${YELLOW}⚠ Playwright 브라우저 설치 실패. 프로젝트 디렉토리에서 직접 실행하세요:${RESET}`,
154
- );
155
- console.error(
156
- ` ${CYAN}npx playwright install chromium${RESET}\n`,
157
- );
158
- }
159
- } else {
160
- console.log(`${GREEN}✓${RESET} Playwright Chromium 이미 설치됨`);
161
- }
162
- } catch {
163
- // playwright 패키지 미설치 시 무시
164
- }
165
-
166
- // 프로덕션 빌드
167
- console.log(`\n${BOLD}위젯 빌드 중...${RESET}\n`);
168
- try {
169
- execSync("gleam run -m glendix/build", {
170
- cwd: targetDir,
171
- stdio: "inherit",
172
- });
173
- console.log(`\n${GREEN}✓${RESET} 위젯 빌드 완료`);
174
- } catch {
175
- console.error(
176
- `\n${YELLOW}⚠ 빌드 실패. 프로젝트 디렉토리에서 직접 실행하세요:${RESET}`,
177
- );
178
- console.error(` ${CYAN}gleam run -m glendix/build${RESET}\n`);
179
- }
180
-
181
- // 완료 메시지
182
- console.log(`
183
- ${GREEN}${BOLD}프로젝트가 생성되었습니다!${RESET}
184
-
185
- ${BOLD}다음 단계:${RESET}
186
-
187
- ${CYAN}cd ${names.kebabCase}${RESET}
188
- ${CYAN}gleam run -m glendix/dev${RESET} ${DIM}# 개발 서버 시작${RESET}
189
- ${CYAN}gleam run -m glendix/build${RESET} ${DIM}# 프로덕션 빌드${RESET}
190
- ${CYAN}gleam run -m glendix/marketplace${RESET} ${DIM}# Marketplace 위젯 다운로드${RESET}
191
- `);
192
- }
193
-
194
- /** CLAUDE.md 생성 */
195
- async function generateClaudeMd(targetDir, names, pm, pmConfig) {
196
- const { writeFile } = await import("node:fs/promises");
197
-
198
- const content = `# ${names.pascalCase}
199
-
200
- Gleam 언어로 Mendix Pluggable Widget을 개발하는 프로젝트.
201
-
202
- ## Goal
203
-
204
- **JSX를 사용하지 않고, 오직 Gleam으로만** 위젯을 작성한다. Gleam 코드를 JavaScript로 컴파일하고, 컴파일된 JS가 곧 Mendix Pluggable Widget의 진입점이 된다.
205
-
206
- ## Tech Stack
207
-
208
- - **Gleam** JavaScript 컴파일 (target: javascript)
209
- - **[glendix](https://hexdocs.pm/glendix/)** — React + Mendix Pluggable Widget API의 Gleam FFI 바인딩 (Hex 패키지). React 원시 함수와 Mendix 런타임 타입 접근자를 타입 안전하게 제공
210
- - **Mendix Pluggable Widget** (React 19)
211
- - **Package Manager**: ${pm} (기본 npm 의존성은 \`gleam run -m glendix/install\`로 설치, \`bindings.json\` 외부 React 패키지는 수동 설치 필요)
212
- - **Build**: \`@mendix/pluggable-widgets-tools\` (Rollup 기반)
213
- - **Package**: \`.mpk\` (ZIP 아카이브) → Mendix Studio Pro에 배포
214
-
215
- ## Architecture
216
-
217
- \`\`\`
218
- src/
219
- ${names.snakeCase}.gleam # 위젯 메인 모듈
220
- editor_config.gleam # Studio Pro 속성 패널 설정
221
- editor_preview.gleam # Studio Pro 디자인 뷰 미리보기
222
- components/
223
- hello_world.gleam # Hello World 공유 컴포넌트
224
- ${names.pascalCase}.xml # 위젯 속성 정의 (Mendix Studio Pro용)
225
- package.xml # Mendix 패키지 매니페스트
226
- ui/
227
- ${names.pascalCase}.css # 위젯 스타일시트
228
- widgets/ # .mpk 위젯 파일 (glendix/widget로 바인딩)
229
- bindings.json # 외부 React 컴포넌트 바인딩 설정
230
- package.json # npm 의존성 (React, 빌드 도구 등)
231
- gleam.toml # Gleam 프로젝트 설정
232
- docs/
233
- gleam_language_tour.md # Gleam 언어 레퍼런스 (문법 전체)
234
- glendix_guide.md # glendix 사용 가이드 (React/Mendix 바인딩 전체)
235
- \`\`\`
236
-
237
- ## glendix — React + Mendix 바인딩 패키지
238
-
239
- React FFI와 Mendix API 바인딩은 별도 Hex 패키지 [glendix](https://hexdocs.pm/glendix/)로 분리되어 있다. 이 프로젝트는 glendix를 의존성으로 사용한다.
240
-
241
- glendix가 제공하는 모듈:
242
-
243
- React:
244
- - \`glendix/react\` — 핵심 타입(\`ReactElement\`, \`JsProps\`, \`Component\`, \`Context\`, \`Ref\`) + \`element\`/\`element_\`/\`void_element\`/\`component_el\`/\`component_el_\`/\`void_component_el\`/\`fragment\`/\`text\`/\`none\` + 조건부 렌더링(\`when\`, \`when_some\`) + \`define_component\`/\`memo\`/\`StrictMode\`/\`Suspense\`/\`Profiler\`/\`portal\`/\`forwardRef\`
245
- - \`glendix/react/attribute\` — Attribute 리스트 API (\`[attribute.class("x"), event.on_click(handler)]\`) + 90+ HTML 속성 함수 + \`attribute.none()\` 조건부 속성
246
- - \`glendix/react/hook\` — React Hooks (\`useState\`, \`useEffect\`, \`useLayoutEffect\`, \`useInsertionEffect\`, \`useMemo\`, \`useCallback\`, \`useRef\`, \`useReducer\`, \`useContext\`, \`useId\`, \`useTransition\`, \`useDeferredValue\`, \`useOptimistic\`, \`useImperativeHandle\`, \`useLazyState\`, \`useSyncExternalStore\`, \`useDebugValue\`)
247
- - \`glendix/react/event\` — 15개 이벤트 타입 + 148+ 핸들러 Attribute (캡처 단계 포함) + 67+ 접근자
248
- - \`glendix/react/html\` — 75+ HTML 태그 편의 함수 (순수 Gleam, FFI 없음)
249
- - \`glendix/react/svg\` — 57 SVG 요소 편의 함수 (순수 Gleam, FFI 없음)
250
- - \`glendix/react/svg_attribute\` — 97+ SVG 전용 속성 함수 (순수 Gleam, FFI 없음)
251
- - \`glendix/binding\` — 외부 React 컴포넌트 바인딩 (\`bindings.json\` + \`binding.module\`/\`binding.resolve\`)
252
- - \`glendix/widget\` — .mpk 위젯 컴포넌트 바인딩 (\`widget.component\`)
253
-
254
- Mendix:
255
- - \`glendix/mendix\` — \`ValueStatus\`, \`ObjectItem\`, JsProps 접근자
256
- - \`glendix/mendix/editable_value\` — 편집 가능한 값
257
- - \`glendix/mendix/action\` — 액션 실행
258
- - \`glendix/mendix/dynamic_value\` — 동적 읽기 전용 값
259
- - \`glendix/mendix/list_value\` — 리스트 데이터 + 정렬/필터
260
- - \`glendix/mendix/list_attribute\` — 리스트 아이템별 접근
261
- - \`glendix/mendix/selection\` — 단일/다중 선택
262
- - \`glendix/mendix/reference\` — 단일 연관 참조 (ReferenceValue)
263
- - \`glendix/mendix/reference_set\` — 다중 연관 참조 (ReferenceSetValue)
264
- - \`glendix/mendix/date\` — JS Date 래퍼 (월 1-based)
265
- - \`glendix/mendix/big\` — Big.js 고정밀 십진수 래퍼
266
- - \`glendix/mendix/file\` — 파일/이미지
267
- - \`glendix/mendix/icon\` — 아이콘
268
- - \`glendix/mendix/formatter\` — 값 포맷팅/파싱
269
- - \`glendix/mendix/filter\` — 필터 조건 빌더
270
-
271
- 빌드 스크립트:
272
- - \`glendix/cmd\` — 셸 명령어 실행 + 패키지 매니저 자동 감지 (\`exec\`, \`detect_runner\`, \`run_tool\`)
273
- - \`glendix/build\` — 프로덕션 빌드 (\`gleam run -m glendix/build\`)
274
- - \`glendix/dev\` — 개발 서버 (\`gleam run -m glendix/dev\`)
275
- - \`glendix/start\` — Mendix 연동 (\`gleam run -m glendix/start\`)
276
- - \`glendix/install\` — 의존성 설치 (\`gleam run -m glendix/install\`)
277
- - \`glendix/release\` — 릴리즈 빌드 (\`gleam run -m glendix/release\`)
278
- - \`glendix/lint\` — ESLint 실행 (\`gleam run -m glendix/lint\`)
279
- - \`glendix/lint_fix\` — ESLint 자동 수정 (\`gleam run -m glendix/lint_fix\`)
280
- - \`glendix/marketplace\` — Mendix Marketplace 위젯 검색/다운로드 (\`gleam run -m glendix/marketplace\`)
281
-
282
- ## Integration Strategy: Gleam + glendix → Mendix Widget
283
-
284
- JSX 파일 없이 Gleam + glendix로 위젯을 구현한다. glendix가 React 원시 함수와 Mendix 런타임 타입 접근을 타입 안전하게 제공하므로, 위젯 프로젝트에서는 비즈니스 로직에만 집중한다.
285
-
286
- 핵심 원리:
287
- - Gleam 함수 \`fn(JsProps) -> ReactElement\`는 React 함수형 컴포넌트와 동일한 시그니처
288
- - glendix의 FFI 레이어(\`react_ffi.mjs\`, \`mendix_ffi.mjs\`)는 얇은 어댑터일 뿐, 위젯 로직과 UI 구조는 전부 Gleam 코드
289
- - Mendix가 전달하는 props(순수 JS 객체)를 \`mendix.get_prop\`/\`mendix.get_string_prop\` 등으로 접근
290
- - Mendix 복합 타입(\`EditableValue\`, \`ActionValue\`, \`ListValue\` 등)은 opaque type + FFI 접근자로 타입 안전하게 다룸
291
- - JS \`undefined\` ↔ Gleam \`Option\` 변환은 FFI 경계에서 자동 처리
292
- - HTML 속성은 Attribute 리스트 기반 선언적 API로 구성 (\`[attribute.class("x"), event.on_click(handler)]\`)
293
- - Gleam List는 linked list이므로 FFI에서 \`.toArray()\` 호출 후 React.createElement에 spread
294
- - Gleam 튜플 \`#(a, b)\` = JS \`[a, b]\` — useState 반환값과 직접 호환
295
-
296
- 핵심 제약사항:
297
- - Mendix 위젯의 진입점은 MUST React 컴포넌트여야 한다 (\`pluginWidget="true"\`)
298
- - Gleam 컴파일 출력은 ES 모듈 형식이므로 Rollup 번들링과 호환된다
299
- - 위젯 ID 형식: \`mendix.${names.lowerCase}.${names.pascalCase}\`
300
- - JSX 파일을 작성하지 않는다. 모든 React 로직은 Gleam + glendix로 구현한다
301
- - Gleam 컴파일 출력이 Mendix 빌드 도구의 진입점으로 연결되도록 빌드 설정 커스터마이징이 필요하다
302
-
303
- ## Build Pipeline
304
-
305
- \`\`\`
306
- [src/*.gleam] + [glendix 패키지 (Hex)]
307
- ↓ gleam run -m glendix/build (내부적으로 gleam build 자동 수행)
308
- [build/dev/javascript/${names.snakeCase}/*.mjs] (gleam.toml name 기준)
309
- [build/dev/javascript/glendix/glendix/*.mjs] (glendix 컴파일 출력)
310
- [build/dev/javascript/glendix/glendix/react_ffi.mjs]
311
- [build/dev/javascript/glendix/glendix/mendix_ffi.mjs]
312
- ↓ 브릿지 JS (자동 생성)가 import
313
- ↓ Rollup (pluggable-widgets-tools build:web)
314
- [dist/1.0.0/mendix.${names.lowerCase}.${names.pascalCase}.mpk]
315
- \`\`\`
316
-
317
- ## Commands
318
-
319
- 모든 명령어는 \`gleam\`으로 통일. \`gleam run -m\`은 Gleam 컴파일을 자동 수행한 뒤 스크립트를 실행한다.
320
-
321
- \`\`\`bash
322
- gleam run -m glendix/install # 의존성 설치 (Gleam deps 자동 + PM 자동 감지, bindings.json 바인딩 코드 생성. 단, 외부 React 패키지는 사전에 수동 설치 필요)
323
- gleam run -m glendix/build # 위젯 프로덕션 빌드 (.mpk 생성)
324
- gleam run -m glendix/dev # 개발 서버 (HMR, port 3000)
325
- gleam run -m glendix/start # Mendix 테스트 프로젝트와 연동 개발
326
- gleam run -m glendix/lint # ESLint 실행
327
- gleam run -m glendix/lint_fix # ESLint 자동 수정
328
- gleam run -m glendix/release # 릴리즈 빌드
329
- gleam run -m glendix/marketplace # Mendix Marketplace 위젯 검색/다운로드
330
- gleam build --target javascript # Gleam → JS 컴파일만 (스크립트 없이)
331
- gleam test # Gleam 테스트 실행
332
- gleam format # Gleam 코드 포맷팅
333
- \`\`\`
334
-
335
- ## glendix Guide
336
-
337
- \`docs/glendix_guide.md\` 파일에 glendix 패키지의 사용법이 수록되어 있다. 주요 내용:
338
- - 프로젝트 설정 및 첫 번째 위젯 만들기
339
- - 핵심 개념: opaque 타입, undefined ↔ Option 변환, Attribute 리스트 API
340
- - React 바인딩: 엘리먼트 생성(\`element\`/\`element_\`/\`void_element\`/\`component_el\`), Attribute 리스트, HTML 태그 함수, Hooks(\`useState\`/\`useEffect\`/\`useMemo\`/\`useCallback\`/\`useRef\` 등), 이벤트 처리, 조건부/리스트 렌더링, 스타일, 외부 React 컴포넌트 바인딩(\`bindings.json\`), .mpk 위젯 바인딩(\`widgets/\`)
341
- - Mendix 바인딩: Props 접근, ValueStatus, EditableValue, ActionValue, DynamicValue, ListValue(페이지네이션/정렬), ListAttribute, Selection, Reference, Filter 빌더, JsDate, Big, FileValue/WebIcon/ValueFormatter
342
- - Marketplace 연동: Mendix Marketplace에서 위젯 검색/다운로드 (\`glendix/marketplace\`)
343
- - 실전 패턴: 폼 입력 위젯, 데이터 테이블, 검색 가능 리스트, 컴포넌트 합성
344
- - 트러블슈팅: 빌드/런타임 에러, Hook 규칙
345
-
346
- ## Gleam Language Reference
347
-
348
- \`docs/gleam_language_tour.md\` 파일에 Gleam 문법 전체가 수록되어 있다. 주요 특징:
349
- - 정적 타입, 불변 데이터, 패턴 매칭
350
- - \`pub fn\` 으로 외부 공개 함수 선언
351
- - \`import gleam/모듈\` 로 표준 라이브러리 사용
352
- - JavaScript 타겟 시 \`@external(javascript, "모듈", "함수")\` 로 JS 함수 호출 가능
353
- - Result 타입(\`Ok\`, \`Error\`)으로 에러 처리
354
- - \`use\` 키워드로 콜백 체이닝 간소화
355
-
356
- ## Mendix Widget Conventions
357
-
358
- - \`src/${names.pascalCase}.xml\`: 위젯 속성 정의. \`<property>\` 추가 시 빌드 도구가 자동으로 타입 생성
359
- - \`src/package.xml\`: 패키지 매니페스트. \`widgetFile\` 경로와 컴파일 출력 경로 지정
360
- - 위젯 컴포넌트는 \`default export\` 또는 \`named export\` 함수형 React 컴포넌트
361
- - \`needsEntityContext="true"\` → 위젯이 Mendix 데이터 컨텍스트 필요
362
- - \`offlineCapable="true"\` → 오프라인 지원
363
- - editorConfig도 Gleam으로 작성. 브릿지 JS는 glendix가 빌드 시 자동 생성/삭제한다
364
-
365
- ## Mendix Documentation
366
-
367
- Mendix 공식 문서 사이트(docs.mendix.com)는 접근 불가. 대신 GitHub raw 소스를 사용:
368
- - Base: \`https://github.com/mendix/docs/blob/development/content/en/docs\`
369
- - Pluggable Widgets API: \`apidocs-mxsdk/apidocs/pluggable-widgets/\`
370
- - How-to: \`howto/extensibility/\`
371
- - Widget 빌드 도구 소스: \`https://github.com/mendix/widgets-tools\`
372
- - 공식 위젯 예제: \`https://github.com/mendix/web-widgets\`
373
-
374
- ## Important Notes
375
-
376
- - Gleam 컴파일 출력 경로와 Rollup 번들링 입력 경로가 MUST 일치해야 한다
377
- - \`package.json\`의 \`packagePath: "mendix"\`가 위젯 배포 경로를 결정한다
378
- - Mendix 위젯 이름은 영문자(a-zA-Z)만 허용된다
379
- - \`.mpk\` 파일은 \`dist/\` 디렉토리에 생성된다
380
- - 테스트 프로젝트 경로: \`./tests/testProject\`
381
- - Gleam→JS→Mendix Widget 파이프라인은 공식 지원되지 않는 조합이므로, 빌드 설정 커스터마이징이 필요할 수 있다
382
- - **JSX/JS 파일을 직접 작성하지 않는다.** 모든 위젯 로직과 UI는 Gleam으로 작성하고 JS로 컴파일한다
383
- - Mendix 빌드 도구가 요구하는 브릿지 JS 파일(진입점, editorConfig, editorPreview)은 glendix가 빌드 시 자동 생성/삭제한다. 수동 관리 불필요
384
- - Redraw 등 외부 Gleam React 라이브러리는 사용하지 않는다. glendix가 Gleam FFI로 React API를 직접 바인딩한다
385
- - React/Mendix FFI 바인딩은 glendix 패키지에서 제공한다. 위젯 프로젝트에 FFI 파일을 직접 작성하지 않는다
386
-
387
- ## Code Style
388
-
389
- - Gleam 파일: \`gleam format\` 사용
390
- - Gleam 컴파일 출력 JS: 수동 편집하지 않는다
391
- - 한국어 주석 사용
392
- `;
393
-
394
- await writeFile(join(targetDir, "CLAUDE.md"), content, "utf-8");
395
- }
396
-
397
- /** README.md 생성 */
398
- async function generateReadme(targetDir, names, pm, pmConfig) {
399
- const { writeFile } = await import("node:fs/promises");
400
-
401
- const runCmd = pm === "npm" ? "npm run" : pm;
402
-
403
- const content = `# ${names.pascalCase}
404
-
405
- Gleam 언어로 작성된 Mendix Pluggable Widget.
406
-
407
- ## 핵심 원리
408
-
409
- Gleam 함수 \`fn(JsProps) -> ReactElement\`는 React 함수형 컴포넌트와 동일한 시그니처다. glendix가 React 원시 함수와 Mendix 런타임 타입 접근자를 타입 안전하게 제공하므로, 위젯 프로젝트에서는 비즈니스 로직에만 집중하면 된다.
410
-
411
- \`\`\`gleam
412
- // src/${names.snakeCase}.gleam
413
- import glendix/mendix
414
- import glendix/react.{type JsProps, type ReactElement}
415
- import glendix/react/attribute
416
- import glendix/react/html
417
-
418
- pub fn widget(props: JsProps) -> ReactElement {
419
- let sample_text = mendix.get_string_prop(props, "sampleText")
420
- html.div([attribute.class("widget-hello-world")], [
421
- react.text("Hello " <> sample_text),
422
- ])
423
- }
424
- \`\`\`
425
-
426
- Mendix 복합 타입도 Gleam에서 타입 안전하게 사용할 수 있다:
427
-
428
- \`\`\`gleam
429
- import glendix/mendix
430
- import glendix/mendix/editable_value
431
- import glendix/mendix/action
432
-
433
- pub fn widget(props: JsProps) -> ReactElement {
434
- // EditableValue 접근
435
- let name_attr: EditableValue = mendix.get_prop_required(props, "name")
436
- let display = editable_value.display_value(name_attr)
437
-
438
- // ActionValue 실행
439
- let on_save: Option(ActionValue) = mendix.get_prop(props, "onSave")
440
- action.execute_action(on_save)
441
- // ...
442
- }
443
- \`\`\`
444
-
445
- ## 시작하기
446
-
447
- ### 사전 요구사항
448
-
449
- - [Gleam](https://gleam.run/getting-started/installing/) (최신 버전)
450
- - [Node.js](https://nodejs.org/) (v18+)
451
- - ${pm}
452
-
453
- ### 설치
454
-
455
- \`\`\`bash
456
- gleam run -m glendix/install
457
- \`\`\`
458
-
459
- ### 개발
460
-
461
- \`\`\`bash
462
- gleam run -m glendix/dev
463
- \`\`\`
464
-
465
- ### 빌드
466
-
467
- \`\`\`bash
468
- gleam run -m glendix/build
469
- \`\`\`
470
-
471
- 빌드 결과물(\`.mpk\`)은 \`dist/\` 디렉토리에 생성됩니다.
472
-
473
- ### 기타 명령어
474
-
475
- \`\`\`bash
476
- gleam run -m glendix/start # Mendix 테스트 프로젝트 연동
477
- gleam run -m glendix/lint # ESLint 실행
478
- gleam run -m glendix/lint_fix # ESLint 자동 수정
479
- gleam run -m glendix/release # 릴리즈 빌드
480
- gleam run -m glendix/marketplace # Marketplace 위젯 검색/다운로드
481
- gleam build --target javascript # Gleam → JS 컴파일만
482
- gleam test # 테스트 실행
483
- gleam format # 코드 포맷팅
484
- \`\`\`
485
-
486
- ## 프로젝트 구조
487
-
488
- \`\`\`
489
- src/
490
- ${names.snakeCase}.gleam # 메인 위젯 모듈
491
- editor_config.gleam # Studio Pro 속성 패널
492
- editor_preview.gleam # Studio Pro 디자인 뷰 미리보기
493
- components/
494
- hello_world.gleam # Hello World 공유 컴포넌트
495
- ${names.pascalCase}.xml # 위젯 속성 정의
496
- widgets/ # .mpk 위젯 파일 (glendix/widget로 바인딩)
497
- bindings.json # 외부 React 컴포넌트 바인딩 설정
498
- package.json # npm 의존성 (React, 외부 라이브러리 등)
499
- \`\`\`
500
-
501
- React/Mendix FFI 바인딩은 [glendix](https://hexdocs.pm/glendix/) Hex 패키지로 제공됩니다.
502
-
503
- ## 외부 React 컴포넌트 사용
504
-
505
- npm 패키지로 제공되는 React 컴포넌트 라이브러리를 \`.mjs\` FFI 파일 작성 없이 순수 Gleam에서 사용할 수 있다.
506
-
507
- ### 1단계: npm 패키지 설치
508
-
509
- \`\`\`bash
510
- ${pm === "npm" ? "npm install" : pm === "yarn" ? "yarn add" : pm === "pnpm" ? "pnpm add" : "bun add"} recharts
511
- \`\`\`
512
-
513
- ### 2단계: \`bindings.json\` 작성
514
-
515
- 프로젝트 루트에 \`bindings.json\`을 생성하고, 사용할 컴포넌트를 등록한다:
516
-
517
- \`\`\`json
518
- {
519
- "recharts": {
520
- "components": ["PieChart", "Pie", "Cell", "Tooltip", "ResponsiveContainer"]
521
- }
522
- }
523
- \`\`\`
524
-
525
- ### 3단계: 바인딩 생성
526
-
527
- \`\`\`bash
528
- gleam run -m glendix/install
529
- \`\`\`
530
-
531
- \`binding_ffi.mjs\`가 자동 생성된다. 이후 \`gleam run -m glendix/build\` 등 빌드 시에도 자동 갱신된다.
532
-
533
- ### 4단계: Gleam에서 사용
534
-
535
- \`\`\`gleam
536
- import glendix/binding
537
- import glendix/react.{type ReactElement}
538
- import glendix/react/attribute.{type Attribute}
539
-
540
- fn m() { binding.module("recharts") }
541
-
542
- pub fn pie_chart(attrs: List(Attribute), children: List(ReactElement)) -> ReactElement {
543
- react.component_el(binding.resolve(m(), "PieChart"), attrs, children)
544
- }
545
-
546
- pub fn tooltip(attrs: List(Attribute)) -> ReactElement {
547
- react.void_component_el(binding.resolve(m(), "Tooltip"), attrs)
548
- }
549
- \`\`\`
550
-
551
- \`html.div\`와 동일한 호출 패턴으로 외부 React 컴포넌트를 사용할 수 있다.
552
-
553
- ## Mendix Marketplace 위젯 다운로드
554
-
555
- Mendix Marketplace에서 위젯(.mpk)을 인터랙티브하게 검색하고 다운로드할 수 있다. 다운로드 완료 후 바인딩 \`.gleam\` 파일이 자동 생성되어 바로 사용 가능하다.
556
-
557
- ### 사전 준비
558
-
559
- \`.env\` 파일에 Mendix Personal Access Token을 설정한다:
560
-
561
- \`\`\`
562
- MENDIX_PAT=your_personal_access_token
563
- \`\`\`
564
-
565
- > PAT는 [Mendix Developer Settings](https://user-settings.mendix.com/link/developersettings)에서 **Personal Access Tokens** 섹션의 **New Token**을 클릭하여 발급. 필요한 scope: \`mx:marketplace-content:read\`
566
-
567
- ### 실행
568
-
569
- \`\`\`bash
570
- gleam run -m glendix/marketplace
571
- \`\`\`
572
-
573
- 인터랙티브 TUI에서 위젯을 검색/선택하면 \`widgets/\` 디렉토리에 \`.mpk\`가 다운로드되고, \`src/widgets/\`에 바인딩 \`.gleam\` 파일이 자동 생성된다.
574
-
575
- ## .mpk 위젯 컴포넌트 사용
576
-
577
- \`widgets/\` 디렉토리에 \`.mpk\` 파일(Mendix 위젯 빌드 결과물)을 배치하면, 다른 위젯 안에서 기존 Mendix 위젯을 React 컴포넌트로 렌더링할 수 있다.
578
-
579
- ### 1단계: \`.mpk\` 파일 배치
580
-
581
- \`\`\`
582
- 프로젝트 루트/
583
- ├── widgets/
584
- │ ├── Switch.mpk
585
- │ └── Badge.mpk
586
- ├── src/
587
- └── gleam.toml
588
- \`\`\`
589
-
590
- ### 2단계: 바인딩 생성
591
-
592
- \`\`\`bash
593
- gleam run -m glendix/install
594
- \`\`\`
595
-
596
- 실행 시 다음이 자동 처리된다:
597
- - \`.mpk\`에서 \`.mjs\`/\`.css\`를 추출하고 \`widget_ffi.mjs\`가 생성된다
598
- - \`.mpk\` XML의 \`<property>\` 정의를 파싱하여 \`src/widgets/\`에 바인딩 \`.gleam\` 파일이 자동 생성된다 (이미 존재하면 건너뜀)
599
-
600
- ### 3단계: 자동 생성된 \`src/widgets/*.gleam\` 파일 확인
601
-
602
- \`\`\`gleam
603
- // src/widgets/switch.gleam (자동 생성)
604
- import glendix/mendix
605
- import glendix/react.{type JsProps, type ReactElement}
606
- import glendix/react/attribute
607
- import glendix/widget
608
-
609
- /// Switch 위젯 렌더링 - props에서 속성을 읽어 위젯에 전달
610
- pub fn render(props: JsProps) -> ReactElement {
611
- let boolean_attribute = mendix.get_prop_required(props, "booleanAttribute")
612
- let action = mendix.get_prop_required(props, "action")
613
-
614
- let comp = widget.component("Switch")
615
- react.component_el(
616
- comp,
617
- [
618
- attribute.attribute("booleanAttribute", boolean_attribute),
619
- attribute.attribute("action", action),
620
- ],
621
- [],
622
- )
623
- }
624
- \`\`\`
625
-
626
- required/optional 속성이 자동 구분되며, 필요에 따라 생성된 파일을 자유롭게 수정할 수 있다.
627
-
628
- ### 4단계: 위젯에서 사용
629
-
630
- \`\`\`gleam
631
- import widgets/switch
632
-
633
- // 컴포넌트 내부에서
634
- switch.render(props)
635
- \`\`\`
636
-
637
- 위젯 이름은 \`.mpk\` 내부 XML의 \`<name>\` 값을, property key는 \`.mpk\` XML의 원본 key를 그대로 사용한다.
638
-
639
- ## 기술 스택
640
-
641
- - **Gleam** → JavaScript 컴파일
642
- - **[glendix](https://hexdocs.pm/glendix/)** — React + Mendix API Gleam 바인딩
643
- - **Mendix Pluggable Widget** (React 19)
644
- - **${pm}** — 패키지 매니저
645
-
646
- ## 라이센스
647
-
648
- Apache-2.0
649
- `;
650
-
651
- await writeFile(join(targetDir, "README.md"), content, "utf-8");
652
- }
1
+ /**
2
+ * create-mendix-widget-gleam main orchestration
3
+ */
4
+
5
+ import { resolve, dirname, join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { mkdir, writeFile } 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
+ import { t, getTemplateComments, getLangLabel } from "./i18n.mjs";
14
+ import { generateClaudeMdContent } from "./templates/claude_md.mjs";
15
+ import { generateReadmeContent } from "./templates/readme_md.mjs";
16
+ import { generateWidgetsReadmeContent } from "./templates/widgets_readme.mjs";
17
+
18
+ const __dirname = dirname(fileURLToPath(import.meta.url));
19
+ const TEMPLATE_DIR = resolve(__dirname, "..", "template");
20
+
21
+ const BOLD = "\x1b[1m";
22
+ const RESET = "\x1b[0m";
23
+ const GREEN = "\x1b[32m";
24
+ const CYAN = "\x1b[36m";
25
+ const DIM = "\x1b[2m";
26
+ const YELLOW = "\x1b[33m";
27
+
28
+ const VERSION = "1.0.0";
29
+
30
+ const HELP = `
31
+ ${BOLD}create-mendix-widget-gleam${RESET} — Create Gleam + Mendix Pluggable Widget projects
32
+
33
+ ${BOLD}Usage:${RESET}
34
+ npx create-mendix-widget-gleam [project-name]
35
+
36
+ ${BOLD}Options:${RESET}
37
+ --help, -h Show help
38
+ --version, -v Show version
39
+
40
+ ${BOLD}Examples:${RESET}
41
+ npx create-mendix-widget-gleam my-cool-widget
42
+ npx create-mendix-widget-gleam MyCoolWidget
43
+ `;
44
+
45
+ export async function main(args) {
46
+ // Flag handling
47
+ if (args.includes("--help") || args.includes("-h")) {
48
+ console.log(HELP);
49
+ return;
50
+ }
51
+ if (args.includes("--version") || args.includes("-v")) {
52
+ console.log(VERSION);
53
+ return;
54
+ }
55
+
56
+ console.log(
57
+ `\n${BOLD}${CYAN}create-mendix-widget-gleam${RESET} ${DIM}v${VERSION}${RESET}\n`,
58
+ );
59
+
60
+ // Extract project name from CLI args (excluding flags)
61
+ const positional = args.filter((a) => !a.startsWith("-"));
62
+ const cliProjectName = positional[0] || null;
63
+
64
+ // Collect options via prompts
65
+ const { projectName, pm, lang } = await collectOptions(cliProjectName);
66
+
67
+ // Name transformations
68
+ const names = generateNames(projectName);
69
+ if (!names) {
70
+ console.error(`${YELLOW}${t(lang, "error.invalidName")}${RESET}`);
71
+ process.exit(1);
72
+ }
73
+
74
+ const pmConfig = getPmConfig(pm);
75
+ const targetDir = resolve(process.cwd(), names.kebabCase);
76
+
77
+ // Summary
78
+ console.log(`\n${BOLD}${t(lang, "summary.title")}${RESET}`);
79
+ console.log(` ${t(lang, "summary.directory")} ${CYAN}${names.kebabCase}/${RESET}`);
80
+ console.log(` ${t(lang, "summary.widgetName")} ${names.pascalCase}`);
81
+ console.log(` ${t(lang, "summary.gleamModule")} ${names.snakeCase}`);
82
+ console.log(` ${t(lang, "summary.packageManager")} ${pm}`);
83
+ console.log(` ${t(lang, "summary.language")} ${getLangLabel(lang)}`);
84
+ console.log();
85
+
86
+ // Create directory
87
+ await mkdir(targetDir, { recursive: true });
88
+
89
+ // Build template comments (i18n for template placeholders)
90
+ const templateComments = {
91
+ ...getTemplateComments(lang),
92
+ widgets_readme: generateWidgetsReadmeContent(lang),
93
+ };
94
+
95
+ // Scaffold templates
96
+ console.log(`${DIM}${t(lang, "progress.generatingFiles")}${RESET}`);
97
+ const created = await scaffold(TEMPLATE_DIR, targetDir, names, pmConfig, templateComments);
98
+ console.log(`${GREEN}✓${RESET} ${t(lang, "progress.filesCreated", { count: created.length })}`);
99
+
100
+ // Generate CLAUDE.md (always English, comment lang instruction varies)
101
+ await writeFile(
102
+ join(targetDir, "CLAUDE.md"),
103
+ generateClaudeMdContent(lang, names, pm, pmConfig),
104
+ "utf-8",
105
+ );
106
+ console.log(`${GREEN}✓${RESET} ${t(lang, "progress.claudeMdCreated")}`);
107
+
108
+ // Generate README.md
109
+ await writeFile(
110
+ join(targetDir, "README.md"),
111
+ generateReadmeContent(lang, names, pm, pmConfig),
112
+ "utf-8",
113
+ );
114
+ console.log(`${GREEN}✓${RESET} ${t(lang, "progress.readmeCreated")}`);
115
+
116
+ // git init
117
+ try {
118
+ execSync("git init", { cwd: targetDir, stdio: "ignore" });
119
+ console.log(`${GREEN}✓${RESET} ${t(lang, "progress.gitInit")}`);
120
+ } catch {
121
+ // git not available — continue
122
+ }
123
+
124
+ // Gleam → JS compilation
125
+ console.log(`\n${BOLD}${t(lang, "progress.gleamCompiling")}${RESET}\n`);
126
+ try {
127
+ execSync("gleam build --target javascript", {
128
+ cwd: targetDir,
129
+ stdio: "inherit",
130
+ });
131
+ console.log(`\n${GREEN}✓${RESET} ${t(lang, "progress.gleamCompiled")}`);
132
+ } catch {
133
+ console.error(
134
+ `\n${YELLOW}${t(lang, "error.gleamCompileFail")}${RESET}`,
135
+ );
136
+ console.error(` ${CYAN}gleam build --target javascript${RESET}\n`);
137
+ }
138
+
139
+ // Install dependencies
140
+ console.log(`\n${BOLD}${t(lang, "progress.depsInstalling", { pm })}${RESET}\n`);
141
+ try {
142
+ execSync(pmConfig.install, {
143
+ cwd: targetDir,
144
+ stdio: "inherit",
145
+ });
146
+ console.log(`\n${GREEN}✓${RESET} ${t(lang, "progress.depsInstalled")}`);
147
+ } catch {
148
+ console.error(
149
+ `\n${YELLOW}${t(lang, "error.depsInstallFail")}${RESET}`,
150
+ );
151
+ console.error(` ${CYAN}${pmConfig.install}${RESET}\n`);
152
+ }
153
+
154
+ // Install Playwright Chromium (only if not already installed)
155
+ try {
156
+ const chromiumExists =
157
+ execSync(
158
+ `node -e "const fs=require('fs'),pw=require('playwright');process.stdout.write(String(fs.existsSync(pw.chromium.executablePath())))"`,
159
+ { cwd: targetDir, encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] },
160
+ ).trim() === "true";
161
+
162
+ if (!chromiumExists) {
163
+ console.log(`\n${BOLD}${t(lang, "progress.playwrightInstalling")}${RESET}\n`);
164
+ try {
165
+ execSync("npx playwright install chromium", {
166
+ cwd: targetDir,
167
+ stdio: "inherit",
168
+ });
169
+ console.log(`\n${GREEN}✓${RESET} ${t(lang, "progress.playwrightInstalled")}`);
170
+ } catch {
171
+ console.error(
172
+ `\n${YELLOW}${t(lang, "error.playwrightFail")}${RESET}`,
173
+ );
174
+ console.error(
175
+ ` ${CYAN}npx playwright install chromium${RESET}\n`,
176
+ );
177
+ }
178
+ } else {
179
+ console.log(`${GREEN}✓${RESET} ${t(lang, "progress.playwrightExists")}`);
180
+ }
181
+ } catch {
182
+ // playwright package not installed — ignore
183
+ }
184
+
185
+ // Production build
186
+ console.log(`\n${BOLD}${t(lang, "progress.buildingWidget")}${RESET}\n`);
187
+ try {
188
+ execSync("gleam run -m glendix/build", {
189
+ cwd: targetDir,
190
+ stdio: "inherit",
191
+ });
192
+ console.log(`\n${GREEN}✓${RESET} ${t(lang, "progress.widgetBuilt")}`);
193
+ } catch {
194
+ console.error(
195
+ `\n${YELLOW}${t(lang, "error.buildFail")}${RESET}`,
196
+ );
197
+ console.error(` ${CYAN}gleam run -m glendix/build${RESET}\n`);
198
+ }
199
+
200
+ // Done
201
+ console.log(`
202
+ ${GREEN}${BOLD}${t(lang, "done.title")}${RESET}
203
+
204
+ ${BOLD}${t(lang, "done.nextSteps")}${RESET}
205
+
206
+ ${CYAN}cd ${names.kebabCase}${RESET}
207
+ ${CYAN}gleam run -m glendix/dev${RESET} ${DIM}${t(lang, "done.devServer")}${RESET}
208
+ ${CYAN}gleam run -m glendix/build${RESET} ${DIM}${t(lang, "done.prodBuild")}${RESET}
209
+ ${CYAN}gleam run -m glendix/marketplace${RESET} ${DIM}${t(lang, "done.marketplace")}${RESET}
210
+ `);
211
+ }