create-fuzionx 0.1.43 → 0.1.44

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.
Files changed (126) hide show
  1. package/fx.js +80 -80
  2. package/index.js +206 -206
  3. package/package.json +1 -1
  4. package/templates/common/.env.example.tpl +16 -16
  5. package/templates/common/.gitignore.tpl +7 -7
  6. package/templates/common/app.js.tpl +6 -6
  7. package/templates/common/database/models/Attachment.js +41 -41
  8. package/templates/common/database/models/Post.js +39 -39
  9. package/templates/common/database/models/Thumbnail.js +53 -53
  10. package/templates/common/database/models/User.js +35 -35
  11. package/templates/common/fuzionx.yaml.tpl +212 -212
  12. package/templates/common/locales/en.json +250 -250
  13. package/templates/common/locales/ko.json +250 -250
  14. package/templates/common/package.json.tpl +20 -20
  15. package/templates/common/shared/events/userEvents.js +10 -10
  16. package/templates/common/shared/jobs/CleanupJob.js +18 -18
  17. package/templates/common/shared/jobs/EmailTask.js +17 -17
  18. package/templates/common/shared/jobs/ProcessVideoThumbnailTask.js +110 -110
  19. package/templates/common/shared/jobs/SampleQueuedTask.js +84 -84
  20. package/templates/common/shared/jobs/SampleScheduledJob.js +75 -75
  21. package/templates/common/shared/jobs/VideoPreviewTask.js +47 -47
  22. package/templates/common/shared/workers/heavy.js +18 -18
  23. package/templates/common/shared/workers/video-worker.js +69 -69
  24. package/templates/common/tester/controllers/FileController.js +288 -288
  25. package/templates/common/tester/controllers/HomeController.js +36 -36
  26. package/templates/common/tester/controllers/UserController.js +43 -43
  27. package/templates/common/tester/middleware/RequestLogger.js +13 -13
  28. package/templates/common/tester/routes/api.js +397 -397
  29. package/templates/common/tester/routes/web.js +8 -8
  30. package/templates/common/tester/services/UserService.js +52 -52
  31. package/templates/common/tester/views/default/errors/404.html +15 -15
  32. package/templates/common/tester/views/default/errors/500.html +14 -14
  33. package/templates/common/tester/views/default/layouts/main.html +82 -82
  34. package/templates/common/tester/views/default/pages/home.html +56 -56
  35. package/templates/common/tester/views/default/pages/i18n.html +104 -104
  36. package/templates/common/tester/views/default/pages/upload.html +149 -149
  37. package/templates/common/tester/views/default/pages/websocket.html +239 -239
  38. package/templates/common/tester/views/default/partials/footer.html +8 -8
  39. package/templates/common/tester/views/default/partials/header.html +20 -20
  40. package/templates/common/tester/ws/ChatHandler.js +98 -98
  41. package/templates/spa/controllers/AuthController.js +114 -114
  42. package/templates/spa/controllers/HomeController.js +68 -68
  43. package/templates/spa/controllers/PostController.js +191 -191
  44. package/templates/spa/controllers/UserController.js +43 -43
  45. package/templates/spa/meta.json +24 -24
  46. package/templates/spa/public/css/style.css +1011 -1011
  47. package/templates/spa/routes/api.js +31 -31
  48. package/templates/spa/routes/web.js +19 -19
  49. package/templates/spa/services/AuthService.js +48 -48
  50. package/templates/spa/services/PostService.js +372 -372
  51. package/templates/spa/services/UserService.js +48 -48
  52. package/templates/spa/views/default/errors/404.html +11 -11
  53. package/templates/spa/views/default/errors/500.html +11 -11
  54. package/templates/spa/views/default/layouts/main.html +34 -34
  55. package/templates/spa/views/default/pages/home.html +22 -22
  56. package/templates/spa/views/default/spa/index.html +13 -13
  57. package/templates/spa/views/default/spa/package.json +20 -20
  58. package/templates/spa/views/default/spa/src/App.vue +41 -41
  59. package/templates/spa/views/default/spa/src/assets/landing.css +220 -220
  60. package/templates/spa/views/default/spa/src/assets/style.css +1156 -1156
  61. package/templates/spa/views/default/spa/src/components/AlertDialog.vue +179 -179
  62. package/templates/spa/views/default/spa/src/components/CodeBlock.vue +33 -33
  63. package/templates/spa/views/default/spa/src/components/EditorToolbar.vue +54 -54
  64. package/templates/spa/views/default/spa/src/components/FileUpload.vue +161 -161
  65. package/templates/spa/views/default/spa/src/components/FlashMessage.vue +39 -39
  66. package/templates/spa/views/default/spa/src/components/LanguageSwitcher.vue +108 -108
  67. package/templates/spa/views/default/spa/src/components/Lightbox.vue +62 -62
  68. package/templates/spa/views/default/spa/src/components/Navbar.vue +68 -68
  69. package/templates/spa/views/default/spa/src/components/Pagination.vue +166 -166
  70. package/templates/spa/views/default/spa/src/components/ToastContainer.vue +135 -135
  71. package/templates/spa/views/default/spa/src/composables/useApi.js +129 -129
  72. package/templates/spa/views/default/spa/src/composables/useClipboard.js +44 -44
  73. package/templates/spa/views/default/spa/src/composables/useDate.js +73 -73
  74. package/templates/spa/views/default/spa/src/composables/useDebounce.js +59 -59
  75. package/templates/spa/views/default/spa/src/composables/useFlash.js +46 -46
  76. package/templates/spa/views/default/spa/src/composables/useHeartbeat.js +45 -45
  77. package/templates/spa/views/default/spa/src/composables/useLocalStorage.js +43 -43
  78. package/templates/spa/views/default/spa/src/composables/useLocale.js +79 -79
  79. package/templates/spa/views/default/spa/src/composables/useWebSocket.js +93 -93
  80. package/templates/spa/views/default/spa/src/main.js +108 -108
  81. package/templates/spa/views/default/spa/src/plugins/alert.js +96 -96
  82. package/templates/spa/views/default/spa/src/plugins/toast.js +79 -79
  83. package/templates/spa/views/default/spa/src/router/index.js +29 -29
  84. package/templates/spa/views/default/spa/src/stores/auth.js +58 -58
  85. package/templates/spa/views/default/spa/src/views/BoardDetail.vue +169 -169
  86. package/templates/spa/views/default/spa/src/views/BoardForm.vue +192 -192
  87. package/templates/spa/views/default/spa/src/views/BoardList.vue +129 -129
  88. package/templates/spa/views/default/spa/src/views/ChatView.vue +327 -327
  89. package/templates/spa/views/default/spa/src/views/FeaturesView.vue +242 -242
  90. package/templates/spa/views/default/spa/src/views/HomeView.vue +215 -215
  91. package/templates/spa/views/default/spa/src/views/Login.vue +82 -82
  92. package/templates/spa/views/default/spa/src/views/Profile.vue +85 -85
  93. package/templates/spa/views/default/spa/src/views/Register.vue +84 -84
  94. package/templates/spa/views/default/spa/vite.config.js +28 -28
  95. package/templates/spa/views/default/spa/yarn.lock +633 -633
  96. package/templates/spa/ws/ChatHandler.js +138 -138
  97. package/templates/ssr/controllers/AuthController.js +119 -119
  98. package/templates/ssr/controllers/ChatController.js +15 -15
  99. package/templates/ssr/controllers/FeaturesController.js +15 -15
  100. package/templates/ssr/controllers/HomeController.js +21 -21
  101. package/templates/ssr/controllers/PostController.js +214 -214
  102. package/templates/ssr/controllers/UserController.js +48 -48
  103. package/templates/ssr/meta.json +11 -11
  104. package/templates/ssr/public/css/fx-ui.css +43 -43
  105. package/templates/ssr/public/css/landing.css +220 -220
  106. package/templates/ssr/public/css/style.css +1011 -1011
  107. package/templates/ssr/public/js/fx-client.js +107 -107
  108. package/templates/ssr/public/js/fx-ui.js +124 -124
  109. package/templates/ssr/routes/web.js +46 -46
  110. package/templates/ssr/services/AuthService.js +48 -48
  111. package/templates/ssr/services/PostService.js +372 -372
  112. package/templates/ssr/services/UserService.js +48 -48
  113. package/templates/ssr/views/default/errors/404.html +11 -11
  114. package/templates/ssr/views/default/errors/500.html +48 -48
  115. package/templates/ssr/views/default/layouts/main.html +93 -93
  116. package/templates/ssr/views/default/pages/board/form.html +240 -240
  117. package/templates/ssr/views/default/pages/board/index.html +73 -73
  118. package/templates/ssr/views/default/pages/board/show.html +148 -148
  119. package/templates/ssr/views/default/pages/chat.html +288 -288
  120. package/templates/ssr/views/default/pages/features.html +373 -373
  121. package/templates/ssr/views/default/pages/home.html +258 -258
  122. package/templates/ssr/views/default/pages/login.html +27 -27
  123. package/templates/ssr/views/default/pages/profile.html +36 -36
  124. package/templates/ssr/views/default/pages/register.html +35 -35
  125. package/templates/ssr/views/default/partials/pagination.html +75 -75
  126. package/templates/ssr/ws/ChatHandler.js +138 -138
package/fx.js CHANGED
@@ -1,80 +1,80 @@
1
- #!/usr/bin/env node
2
- /**
3
- * fx — FuzionX CLI (글로벌)
4
- *
5
- * 글로벌 설치: npm install -g create-fuzionx
6
- * 사용법:
7
- * fx new my-app → create-fuzionx 스캐폴딩
8
- * fx make:controller User → 로컬 @fuzionx/framework CLI 위임
9
- * fx dev → 로컬 @fuzionx/framework CLI 위임
10
- */
11
- import { spawnSync } from 'node:child_process';
12
- import { existsSync } from 'node:fs';
13
- import path from 'node:path';
14
- import { fileURLToPath, pathToFileURL } from 'node:url';
15
-
16
- const [command, ...args] = process.argv.slice(2);
17
-
18
- // ── fx new → 스캐폴딩 (자체 처리) ──
19
- if (command === 'new') {
20
- // create-fuzionx index.js 재사용
21
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
22
- process.argv = ['node', 'create-fuzionx', ...args];
23
- await import(pathToFileURL(path.join(__dirname, 'index.js')).href);
24
- process.exit(0);
25
- }
26
-
27
- // ── 그 외 명령어 → 로컬 @fuzionx/framework CLI 위임 ──
28
-
29
- // 로컬 node_modules 에서 fx 찾기
30
- const localBin = path.resolve('node_modules', '.bin', 'fx');
31
- const localCli = path.resolve('node_modules', '@fuzionx', 'framework', 'bin', 'fx.js');
32
-
33
- if (existsSync(localBin) || existsSync(localCli)) {
34
- // spawnSync 배열 방식 — 쉘 인젝션 방지
35
- const useLocalBin = existsSync(localBin);
36
- const bin = useLocalBin ? localBin : process.execPath;
37
- const spawnArgs = useLocalBin
38
- ? process.argv.slice(2)
39
- : [localCli, ...process.argv.slice(2)];
40
- const result = spawnSync(bin, spawnArgs, {
41
- stdio: 'inherit',
42
- cwd: process.cwd(),
43
- });
44
- if (result.status !== 0) {
45
- process.exit(result.status ?? 1);
46
- }
47
- } else if (!command || command === 'help') {
48
- console.log(`
49
- fx — FuzionX CLI
50
-
51
- 프로젝트 생성:
52
- fx new <name> 새 프로젝트 스캐폴딩
53
-
54
- 프로젝트 내 명령어 (@fuzionx/framework 필요):
55
- fx make:app --type=ssr|spa Create app (fixed name: app/ssr or app/spa)
56
- fx make:controller <Name> --app= Create controller (app-specific)
57
- fx make:service <Name> --app= Create service (app-specific)
58
- fx make:model <Name> Create model (database/models)
59
- fx make:middleware <Name> --app= Create middleware (app-specific)
60
- fx make:job <Name> Create job (shared/jobs)
61
- fx make:task <Name> Create task (shared/jobs)
62
- fx make:ws <Name> --app= Create WsHandler (app-specific)
63
- fx make:event <Name> Create event handler (shared/events)
64
- fx make:worker <Name> Create worker (shared/workers)
65
- fx make:test <Name> Create test
66
- fx dev Start dev server (--watch)
67
- fx dev:spa Start dev server + Vite HMR
68
- fx build:spa Build SPA for production
69
- fx stop Stop server (graceful)
70
- fx restart Restart server (graceful)
71
- fx test Run tests
72
- fx routes Print route table
73
- fx config Print fuzionx.yaml
74
- fx db:sync Sync models → DB (--apply)
75
- `);
76
- } else {
77
- console.error(`❌ @fuzionx/framework가 설치되지 않았습니다.`);
78
- console.error(` 프로젝트 디렉토리에서 실행하거나, npm install @fuzionx/framework를 먼저 실행하세요.`);
79
- process.exit(1);
80
- }
1
+ #!/usr/bin/env node
2
+ /**
3
+ * fx — FuzionX CLI (글로벌)
4
+ *
5
+ * 글로벌 설치: npm install -g create-fuzionx
6
+ * 사용법:
7
+ * fx new my-app → create-fuzionx 스캐폴딩
8
+ * fx make:controller User → 로컬 @fuzionx/framework CLI 위임
9
+ * fx dev → 로컬 @fuzionx/framework CLI 위임
10
+ */
11
+ import { spawnSync } from 'node:child_process';
12
+ import { existsSync } from 'node:fs';
13
+ import path from 'node:path';
14
+ import { fileURLToPath, pathToFileURL } from 'node:url';
15
+
16
+ const [command, ...args] = process.argv.slice(2);
17
+
18
+ // ── fx new → 스캐폴딩 (자체 처리) ──
19
+ if (command === 'new') {
20
+ // create-fuzionx index.js 재사용
21
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
22
+ process.argv = ['node', 'create-fuzionx', ...args];
23
+ await import(pathToFileURL(path.join(__dirname, 'index.js')).href);
24
+ process.exit(0);
25
+ }
26
+
27
+ // ── 그 외 명령어 → 로컬 @fuzionx/framework CLI 위임 ──
28
+
29
+ // 로컬 node_modules 에서 fx 찾기
30
+ const localBin = path.resolve('node_modules', '.bin', 'fx');
31
+ const localCli = path.resolve('node_modules', '@fuzionx', 'framework', 'bin', 'fx.js');
32
+
33
+ if (existsSync(localBin) || existsSync(localCli)) {
34
+ // spawnSync 배열 방식 — 쉘 인젝션 방지
35
+ const useLocalBin = existsSync(localBin);
36
+ const bin = useLocalBin ? localBin : process.execPath;
37
+ const spawnArgs = useLocalBin
38
+ ? process.argv.slice(2)
39
+ : [localCli, ...process.argv.slice(2)];
40
+ const result = spawnSync(bin, spawnArgs, {
41
+ stdio: 'inherit',
42
+ cwd: process.cwd(),
43
+ });
44
+ if (result.status !== 0) {
45
+ process.exit(result.status ?? 1);
46
+ }
47
+ } else if (!command || command === 'help') {
48
+ console.log(`
49
+ fx — FuzionX CLI
50
+
51
+ 프로젝트 생성:
52
+ fx new <name> 새 프로젝트 스캐폴딩
53
+
54
+ 프로젝트 내 명령어 (@fuzionx/framework 필요):
55
+ fx make:app --type=ssr|spa Create app (fixed name: app/ssr or app/spa)
56
+ fx make:controller <Name> --app= Create controller (app-specific)
57
+ fx make:service <Name> --app= Create service (app-specific)
58
+ fx make:model <Name> Create model (database/models)
59
+ fx make:middleware <Name> --app= Create middleware (app-specific)
60
+ fx make:job <Name> Create job (shared/jobs)
61
+ fx make:task <Name> Create task (shared/jobs)
62
+ fx make:ws <Name> --app= Create WsHandler (app-specific)
63
+ fx make:event <Name> Create event handler (shared/events)
64
+ fx make:worker <Name> Create worker (shared/workers)
65
+ fx make:test <Name> Create test
66
+ fx dev Start dev server (--watch)
67
+ fx dev:spa Start dev server + Vite HMR
68
+ fx build:spa Build SPA for production
69
+ fx stop Stop server (graceful)
70
+ fx restart Restart server (graceful)
71
+ fx test Run tests
72
+ fx routes Print route table
73
+ fx config Print fuzionx.yaml
74
+ fx db:sync Sync models → DB (--apply)
75
+ `);
76
+ } else {
77
+ console.error(`❌ @fuzionx/framework가 설치되지 않았습니다.`);
78
+ console.error(` 프로젝트 디렉토리에서 실행하거나, npm install @fuzionx/framework를 먼저 실행하세요.`);
79
+ process.exit(1);
80
+ }
package/index.js CHANGED
@@ -1,206 +1,206 @@
1
- #!/usr/bin/env node
2
- /**
3
- * create-fuzionx — FuzionX 앱 스캐폴딩 CLI
4
- *
5
- * Usage:
6
- * npx create-fuzionx my-app
7
- * npx create-fuzionx my-app --type=spa
8
- * npx create-fuzionx my-app --type=ssr
9
- */
10
- import { promises as fs } from 'node:fs';
11
- import path from 'node:path';
12
- import { fileURLToPath } from 'node:url';
13
-
14
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
- const TPL_DIR = path.join(__dirname, 'templates');
16
-
17
- // ── 템플릿 엔진 ({{var}} → value) ──
18
-
19
- function render(template, vars) {
20
- return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? '');
21
- }
22
-
23
- async function loadTemplate(relPath, vars) {
24
- const raw = await fs.readFile(path.join(TPL_DIR, relPath), 'utf-8');
25
- return render(raw, vars);
26
- }
27
-
28
- /** 디렉토리 재귀 복사 — .tpl 파일은 변수 치환 */
29
- async function copyDirRecursive(src, dst, vars = {}) {
30
- await fs.mkdir(dst, { recursive: true });
31
- const entries = await fs.readdir(src, { withFileTypes: true });
32
- for (const entry of entries) {
33
- const srcPath = path.join(src, entry.name);
34
- const dstPath = path.join(dst, entry.name);
35
- if (entry.isDirectory()) {
36
- await copyDirRecursive(srcPath, dstPath, vars);
37
- } else if (entry.name === 'meta.json') {
38
- // meta.json은 스캐폴딩에 포함하지 않음
39
- continue;
40
- } else if (entry.name.endsWith('.tpl')) {
41
- // .tpl 파일은 변수 치환 후 확장자 제거
42
- const content = await fs.readFile(srcPath, 'utf-8');
43
- const rendered = render(content, vars);
44
- const destName = entry.name.replace(/\.tpl$/, '');
45
- await fs.writeFile(path.join(dst, destName), rendered);
46
- } else {
47
- await fs.copyFile(srcPath, dstPath);
48
- }
49
- }
50
- }
51
-
52
- // ── 빈 디렉토리 (.gitkeep 포함) ──
53
-
54
- const EMPTY_DIRS = [
55
- 'database/migrations',
56
- 'database/seeds',
57
- 'storage/logs',
58
- 'storage/uploads',
59
- 'tests',
60
- ];
61
-
62
- // ── createApp ──
63
-
64
- async function createApp(name, targetDir, type = 'spa') {
65
- const dir = targetDir || path.resolve(name);
66
- const appName = type; // 앱 디렉토리명은 타입과 동일
67
- const vars = {
68
- name,
69
- appName: type,
70
- dbName: name.replace(/-/g, '_'),
71
- };
72
-
73
- const commonDir = path.join(TPL_DIR, 'common');
74
- const typeDir = path.join(TPL_DIR, type);
75
-
76
- // meta.json 로드
77
- let meta = {};
78
- try {
79
- const metaRaw = await fs.readFile(path.join(typeDir, 'meta.json'), 'utf-8');
80
- meta = JSON.parse(metaRaw);
81
- } catch { /* meta.json 없어도 진행 */ }
82
-
83
- // 1. 빈 디렉토리 생성
84
- for (const d of EMPTY_DIRS) {
85
- const fullDir = path.join(dir, d);
86
- await fs.mkdir(fullDir, { recursive: true });
87
- const files = await fs.readdir(fullDir).catch(() => []);
88
- if (files.length === 0) {
89
- await fs.writeFile(path.join(fullDir, '.gitkeep'), '');
90
- }
91
- }
92
-
93
- // 앱 전용 빈 디렉토리
94
- for (const d of ['services', 'middleware', 'ws']) {
95
- const fullDir = path.join(dir, `app/${appName}`, d);
96
- await fs.mkdir(fullDir, { recursive: true });
97
- const files = await fs.readdir(fullDir).catch(() => []);
98
- if (files.length === 0) {
99
- await fs.writeFile(path.join(fullDir, '.gitkeep'), '');
100
- }
101
- }
102
-
103
- // 2. common/ 루트 .tpl 파일 → 프로젝트 루트
104
- const commonEntries = await fs.readdir(commonDir, { withFileTypes: true });
105
- for (const entry of commonEntries) {
106
- if (entry.isFile() && entry.name.endsWith('.tpl')) {
107
- const content = await fs.readFile(path.join(commonDir, entry.name), 'utf-8');
108
- const rendered = render(content, vars);
109
- const destName = entry.name.replace(/\.tpl$/, '');
110
- await fs.writeFile(path.join(dir, destName), rendered);
111
-
112
- // .env.example → .env 도 함께 생성
113
- if (destName === '.env.example') {
114
- await fs.writeFile(path.join(dir, '.env'), rendered);
115
- }
116
- }
117
- }
118
-
119
- // 3. common/ 하위 디렉토리 복사 (database, locales, shared)
120
- for (const entry of commonEntries) {
121
- if (!entry.isDirectory()) continue;
122
- if (entry.name === 'tester') continue; // tester는 별도 처리
123
- await copyDirRecursive(
124
- path.join(commonDir, entry.name),
125
- path.join(dir, entry.name),
126
- vars,
127
- );
128
- }
129
-
130
- // 4. tester 앱 복사 (common/tester/ → app/tester/)
131
- const testerSrc = path.join(commonDir, 'tester');
132
- try {
133
- await fs.access(testerSrc);
134
- await copyDirRecursive(testerSrc, path.join(dir, 'app/tester'), vars);
135
- } catch { /* tester 없으면 스킵 */ }
136
-
137
- // 5. 타입별 템플릿 복사 → app/{type}/
138
- // controllers, services, middleware, routes, ws, views
139
- const typeEntries = await fs.readdir(typeDir, { withFileTypes: true });
140
- for (const entry of typeEntries) {
141
- if (!entry.isDirectory()) continue;
142
- if (entry.name === 'public') continue; // public은 프로젝트 루트로 복사
143
- await copyDirRecursive(
144
- path.join(typeDir, entry.name),
145
- path.join(dir, `app/${appName}`, entry.name),
146
- vars,
147
- );
148
- }
149
-
150
- // 6. public/ 디렉토리 복사 (SSR의 경우 CSS/JS assets)
151
- const publicSrc = path.join(typeDir, 'public');
152
- try {
153
- await fs.access(publicSrc);
154
- await copyDirRecursive(publicSrc, path.join(dir, 'public'), vars);
155
- } catch { /* public 없으면 스킵 */ }
156
-
157
- return dir;
158
- }
159
-
160
- // ── CLI 엔트리 (direct 실행 시만 동작) ──
161
-
162
- const isDirectEntry = process.argv[1]?.includes('create-fuzionx') || process.argv[1]?.endsWith('index.js');
163
-
164
- if (isDirectEntry) {
165
- const allArgs = process.argv.slice(2);
166
- const name = allArgs.find(a => !a.startsWith('--'));
167
- const typeFlag = allArgs.find(a => a.startsWith('--type='));
168
- const type = typeFlag?.split('=')[1] || 'spa';
169
-
170
- if (!name) {
171
- console.error(`
172
- Usage: npx create-fuzionx <app-name> [--type=ssr|spa]
173
-
174
- Examples:
175
- npx create-fuzionx my-app # default: spa
176
- npx create-fuzionx my-app --type=ssr # MPA (SSR)
177
- npx create-fuzionx my-app --type=spa # SPA (Vue.js 3 + SSR)
178
- `);
179
- process.exit(1);
180
- }
181
-
182
- const validTypes = ['ssr', 'spa'];
183
- if (!validTypes.includes(type)) {
184
- console.error(`❌ Unknown type: ${type}. Available: ${validTypes.join(', ')}`);
185
- process.exit(1);
186
- }
187
-
188
- try {
189
- const dir = await createApp(name, null, type);
190
- const nextCmd = type === 'spa' ? 'npx fx dev:spa' : 'npx fx dev';
191
- console.log(`
192
- ✅ Created ${name} at ${dir} (type: ${type})
193
-
194
- Next steps:
195
-
196
- cd ${name}
197
- npm install
198
- ${type === 'spa' ? `cd app/spa/views/default/spa && npm install && cd ../../../../..
199
- ` : ''}npx fx db:sync --apply
200
- ${nextCmd}
201
- `);
202
- } catch (err) {
203
- console.error(`❌ Failed to create app: ${err.message}`);
204
- process.exit(1);
205
- }
206
- }
1
+ #!/usr/bin/env node
2
+ /**
3
+ * create-fuzionx — FuzionX 앱 스캐폴딩 CLI
4
+ *
5
+ * Usage:
6
+ * npx create-fuzionx my-app
7
+ * npx create-fuzionx my-app --type=spa
8
+ * npx create-fuzionx my-app --type=ssr
9
+ */
10
+ import { promises as fs } from 'node:fs';
11
+ import path from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+
14
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
+ const TPL_DIR = path.join(__dirname, 'templates');
16
+
17
+ // ── 템플릿 엔진 ({{var}} → value) ──
18
+
19
+ function render(template, vars) {
20
+ return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? '');
21
+ }
22
+
23
+ async function loadTemplate(relPath, vars) {
24
+ const raw = await fs.readFile(path.join(TPL_DIR, relPath), 'utf-8');
25
+ return render(raw, vars);
26
+ }
27
+
28
+ /** 디렉토리 재귀 복사 — .tpl 파일은 변수 치환 */
29
+ async function copyDirRecursive(src, dst, vars = {}) {
30
+ await fs.mkdir(dst, { recursive: true });
31
+ const entries = await fs.readdir(src, { withFileTypes: true });
32
+ for (const entry of entries) {
33
+ const srcPath = path.join(src, entry.name);
34
+ const dstPath = path.join(dst, entry.name);
35
+ if (entry.isDirectory()) {
36
+ await copyDirRecursive(srcPath, dstPath, vars);
37
+ } else if (entry.name === 'meta.json') {
38
+ // meta.json은 스캐폴딩에 포함하지 않음
39
+ continue;
40
+ } else if (entry.name.endsWith('.tpl')) {
41
+ // .tpl 파일은 변수 치환 후 확장자 제거
42
+ const content = await fs.readFile(srcPath, 'utf-8');
43
+ const rendered = render(content, vars);
44
+ const destName = entry.name.replace(/\.tpl$/, '');
45
+ await fs.writeFile(path.join(dst, destName), rendered);
46
+ } else {
47
+ await fs.copyFile(srcPath, dstPath);
48
+ }
49
+ }
50
+ }
51
+
52
+ // ── 빈 디렉토리 (.gitkeep 포함) ──
53
+
54
+ const EMPTY_DIRS = [
55
+ 'database/migrations',
56
+ 'database/seeds',
57
+ 'storage/logs',
58
+ 'storage/uploads',
59
+ 'tests',
60
+ ];
61
+
62
+ // ── createApp ──
63
+
64
+ async function createApp(name, targetDir, type = 'spa') {
65
+ const dir = targetDir || path.resolve(name);
66
+ const appName = type; // 앱 디렉토리명은 타입과 동일
67
+ const vars = {
68
+ name,
69
+ appName: type,
70
+ dbName: name.replace(/-/g, '_'),
71
+ };
72
+
73
+ const commonDir = path.join(TPL_DIR, 'common');
74
+ const typeDir = path.join(TPL_DIR, type);
75
+
76
+ // meta.json 로드
77
+ let meta = {};
78
+ try {
79
+ const metaRaw = await fs.readFile(path.join(typeDir, 'meta.json'), 'utf-8');
80
+ meta = JSON.parse(metaRaw);
81
+ } catch { /* meta.json 없어도 진행 */ }
82
+
83
+ // 1. 빈 디렉토리 생성
84
+ for (const d of EMPTY_DIRS) {
85
+ const fullDir = path.join(dir, d);
86
+ await fs.mkdir(fullDir, { recursive: true });
87
+ const files = await fs.readdir(fullDir).catch(() => []);
88
+ if (files.length === 0) {
89
+ await fs.writeFile(path.join(fullDir, '.gitkeep'), '');
90
+ }
91
+ }
92
+
93
+ // 앱 전용 빈 디렉토리
94
+ for (const d of ['services', 'middleware', 'ws']) {
95
+ const fullDir = path.join(dir, `app/${appName}`, d);
96
+ await fs.mkdir(fullDir, { recursive: true });
97
+ const files = await fs.readdir(fullDir).catch(() => []);
98
+ if (files.length === 0) {
99
+ await fs.writeFile(path.join(fullDir, '.gitkeep'), '');
100
+ }
101
+ }
102
+
103
+ // 2. common/ 루트 .tpl 파일 → 프로젝트 루트
104
+ const commonEntries = await fs.readdir(commonDir, { withFileTypes: true });
105
+ for (const entry of commonEntries) {
106
+ if (entry.isFile() && entry.name.endsWith('.tpl')) {
107
+ const content = await fs.readFile(path.join(commonDir, entry.name), 'utf-8');
108
+ const rendered = render(content, vars);
109
+ const destName = entry.name.replace(/\.tpl$/, '');
110
+ await fs.writeFile(path.join(dir, destName), rendered);
111
+
112
+ // .env.example → .env 도 함께 생성
113
+ if (destName === '.env.example') {
114
+ await fs.writeFile(path.join(dir, '.env'), rendered);
115
+ }
116
+ }
117
+ }
118
+
119
+ // 3. common/ 하위 디렉토리 복사 (database, locales, shared)
120
+ for (const entry of commonEntries) {
121
+ if (!entry.isDirectory()) continue;
122
+ if (entry.name === 'tester') continue; // tester는 별도 처리
123
+ await copyDirRecursive(
124
+ path.join(commonDir, entry.name),
125
+ path.join(dir, entry.name),
126
+ vars,
127
+ );
128
+ }
129
+
130
+ // 4. tester 앱 복사 (common/tester/ → app/tester/)
131
+ const testerSrc = path.join(commonDir, 'tester');
132
+ try {
133
+ await fs.access(testerSrc);
134
+ await copyDirRecursive(testerSrc, path.join(dir, 'app/tester'), vars);
135
+ } catch { /* tester 없으면 스킵 */ }
136
+
137
+ // 5. 타입별 템플릿 복사 → app/{type}/
138
+ // controllers, services, middleware, routes, ws, views
139
+ const typeEntries = await fs.readdir(typeDir, { withFileTypes: true });
140
+ for (const entry of typeEntries) {
141
+ if (!entry.isDirectory()) continue;
142
+ if (entry.name === 'public') continue; // public은 프로젝트 루트로 복사
143
+ await copyDirRecursive(
144
+ path.join(typeDir, entry.name),
145
+ path.join(dir, `app/${appName}`, entry.name),
146
+ vars,
147
+ );
148
+ }
149
+
150
+ // 6. public/ 디렉토리 복사 (SSR의 경우 CSS/JS assets)
151
+ const publicSrc = path.join(typeDir, 'public');
152
+ try {
153
+ await fs.access(publicSrc);
154
+ await copyDirRecursive(publicSrc, path.join(dir, 'public'), vars);
155
+ } catch { /* public 없으면 스킵 */ }
156
+
157
+ return dir;
158
+ }
159
+
160
+ // ── CLI 엔트리 (direct 실행 시만 동작) ──
161
+
162
+ const isDirectEntry = process.argv[1]?.includes('create-fuzionx') || process.argv[1]?.endsWith('index.js');
163
+
164
+ if (isDirectEntry) {
165
+ const allArgs = process.argv.slice(2);
166
+ const name = allArgs.find(a => !a.startsWith('--'));
167
+ const typeFlag = allArgs.find(a => a.startsWith('--type='));
168
+ const type = typeFlag?.split('=')[1] || 'spa';
169
+
170
+ if (!name) {
171
+ console.error(`
172
+ Usage: npx create-fuzionx <app-name> [--type=ssr|spa]
173
+
174
+ Examples:
175
+ npx create-fuzionx my-app # default: spa
176
+ npx create-fuzionx my-app --type=ssr # MPA (SSR)
177
+ npx create-fuzionx my-app --type=spa # SPA (Vue.js 3 + SSR)
178
+ `);
179
+ process.exit(1);
180
+ }
181
+
182
+ const validTypes = ['ssr', 'spa'];
183
+ if (!validTypes.includes(type)) {
184
+ console.error(`❌ Unknown type: ${type}. Available: ${validTypes.join(', ')}`);
185
+ process.exit(1);
186
+ }
187
+
188
+ try {
189
+ const dir = await createApp(name, null, type);
190
+ const nextCmd = type === 'spa' ? 'npx fx dev:spa' : 'npx fx dev';
191
+ console.log(`
192
+ ✅ Created ${name} at ${dir} (type: ${type})
193
+
194
+ Next steps:
195
+
196
+ cd ${name}
197
+ npm install
198
+ ${type === 'spa' ? `cd app/spa/views/default/spa && npm install && cd ../../../../..
199
+ ` : ''}npx fx db:sync --apply
200
+ ${nextCmd}
201
+ `);
202
+ } catch (err) {
203
+ console.error(`❌ Failed to create app: ${err.message}`);
204
+ process.exit(1);
205
+ }
206
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-fuzionx",
3
- "version": "0.1.43",
3
+ "version": "0.1.44",
4
4
  "description": "Create a new FuzionX application — npx create-fuzionx my-app",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,16 +1,16 @@
1
- # .env — FuzionX App 환경 변수
2
-
3
- # ASP 암호화 (Bridge + WASM 공유)
4
- ASP_SECRET=change-me-in-production
5
-
6
- # Auth (JWT/세션)
7
- JWT_SECRET=change-me-in-production
8
-
9
- # Client Secret (설정값 암호화 → SPA 주입)
10
- CLIENT_SECRET=change-me-in-production
11
-
12
- # Database
13
- DB_NAME={{dbName}}
14
-
15
- # Redis (세션/큐 — 미사용 시 빈 값)
16
- REDIS_URL=
1
+ # .env — FuzionX App 환경 변수
2
+
3
+ # ASP 암호화 (Bridge + WASM 공유)
4
+ ASP_SECRET=change-me-in-production
5
+
6
+ # Auth (JWT/세션)
7
+ JWT_SECRET=change-me-in-production
8
+
9
+ # Client Secret (설정값 암호화 → SPA 주입)
10
+ CLIENT_SECRET=change-me-in-production
11
+
12
+ # Database
13
+ DB_NAME={{dbName}}
14
+
15
+ # Redis (세션/큐 — 미사용 시 빈 값)
16
+ REDIS_URL=
@@ -1,7 +1,7 @@
1
- .env
2
- node_modules/
3
- storage/logs/
4
- storage/uploads/
5
- storage/database.sqlite
6
- fuzionx.pid
7
- public/dist/
1
+ .env
2
+ node_modules/
3
+ storage/logs/
4
+ storage/uploads/
5
+ storage/database.sqlite
6
+ fuzionx.pid
7
+ public/dist/
@@ -1,6 +1,6 @@
1
- import { Application } from '@fuzionx/framework';
2
-
3
- const app = new Application({ configPath: './fuzionx.yaml' });
4
-
5
- await app.boot();
6
- await app.listen();
1
+ import { Application } from '@fuzionx/framework';
2
+
3
+ const app = new Application({ configPath: './fuzionx.yaml' });
4
+
5
+ await app.boot();
6
+ await app.listen();