create-fuzionx 0.1.43 → 0.1.45
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/fx.js +80 -80
- package/index.js +206 -206
- package/package.json +1 -1
- package/templates/common/.env.example.tpl +16 -16
- package/templates/common/.gitignore.tpl +7 -7
- package/templates/common/app.js.tpl +6 -6
- package/templates/common/database/models/Attachment.js +41 -41
- package/templates/common/database/models/Post.js +39 -39
- package/templates/common/database/models/Thumbnail.js +53 -53
- package/templates/common/database/models/User.js +35 -35
- package/templates/common/fuzionx.yaml.tpl +212 -212
- package/templates/common/locales/en.json +250 -250
- package/templates/common/locales/ko.json +250 -250
- package/templates/common/package.json.tpl +20 -20
- package/templates/common/shared/events/userEvents.js +10 -10
- package/templates/common/shared/jobs/CleanupJob.js +18 -18
- package/templates/common/shared/jobs/EmailTask.js +17 -17
- package/templates/common/shared/jobs/ProcessVideoThumbnailTask.js +110 -110
- package/templates/common/shared/jobs/SampleQueuedTask.js +84 -84
- package/templates/common/shared/jobs/SampleScheduledJob.js +75 -75
- package/templates/common/shared/jobs/VideoPreviewTask.js +47 -47
- package/templates/common/shared/workers/heavy.js +18 -18
- package/templates/common/shared/workers/video-worker.js +69 -69
- package/templates/common/tester/controllers/FileController.js +288 -288
- package/templates/common/tester/controllers/HomeController.js +36 -36
- package/templates/common/tester/controllers/UserController.js +43 -43
- package/templates/common/tester/middleware/RequestLogger.js +13 -13
- package/templates/common/tester/routes/api.js +397 -397
- package/templates/common/tester/routes/web.js +8 -8
- package/templates/common/tester/services/UserService.js +52 -52
- package/templates/common/tester/views/default/errors/404.html +15 -15
- package/templates/common/tester/views/default/errors/500.html +14 -14
- package/templates/common/tester/views/default/layouts/main.html +82 -82
- package/templates/common/tester/views/default/pages/home.html +56 -56
- package/templates/common/tester/views/default/pages/i18n.html +104 -104
- package/templates/common/tester/views/default/pages/upload.html +149 -149
- package/templates/common/tester/views/default/pages/websocket.html +239 -239
- package/templates/common/tester/views/default/partials/footer.html +8 -8
- package/templates/common/tester/views/default/partials/header.html +20 -20
- package/templates/common/tester/ws/ChatHandler.js +98 -98
- package/templates/spa/controllers/AuthController.js +114 -114
- package/templates/spa/controllers/HomeController.js +68 -68
- package/templates/spa/controllers/PostController.js +191 -191
- package/templates/spa/controllers/UserController.js +43 -43
- package/templates/spa/meta.json +24 -24
- package/templates/spa/public/css/style.css +1011 -1011
- package/templates/spa/routes/api.js +31 -31
- package/templates/spa/routes/web.js +19 -19
- package/templates/spa/services/AuthService.js +48 -48
- package/templates/spa/services/PostService.js +372 -372
- package/templates/spa/services/UserService.js +48 -48
- package/templates/spa/views/default/errors/404.html +11 -11
- package/templates/spa/views/default/errors/500.html +11 -11
- package/templates/spa/views/default/layouts/main.html +34 -34
- package/templates/spa/views/default/pages/home.html +22 -22
- package/templates/spa/views/default/spa/index.html +13 -13
- package/templates/spa/views/default/spa/package.json +20 -20
- package/templates/spa/views/default/spa/src/App.vue +41 -41
- package/templates/spa/views/default/spa/src/assets/landing.css +220 -220
- package/templates/spa/views/default/spa/src/assets/style.css +1156 -1156
- package/templates/spa/views/default/spa/src/components/AlertDialog.vue +179 -179
- package/templates/spa/views/default/spa/src/components/CodeBlock.vue +33 -33
- package/templates/spa/views/default/spa/src/components/EditorToolbar.vue +54 -54
- package/templates/spa/views/default/spa/src/components/FileUpload.vue +161 -161
- package/templates/spa/views/default/spa/src/components/FlashMessage.vue +39 -39
- package/templates/spa/views/default/spa/src/components/LanguageSwitcher.vue +108 -108
- package/templates/spa/views/default/spa/src/components/Lightbox.vue +62 -62
- package/templates/spa/views/default/spa/src/components/Navbar.vue +68 -68
- package/templates/spa/views/default/spa/src/components/Pagination.vue +166 -166
- package/templates/spa/views/default/spa/src/components/ToastContainer.vue +135 -135
- package/templates/spa/views/default/spa/src/composables/useApi.js +129 -129
- package/templates/spa/views/default/spa/src/composables/useClipboard.js +44 -44
- package/templates/spa/views/default/spa/src/composables/useDate.js +73 -73
- package/templates/spa/views/default/spa/src/composables/useDebounce.js +59 -59
- package/templates/spa/views/default/spa/src/composables/useFlash.js +46 -46
- package/templates/spa/views/default/spa/src/composables/useHeartbeat.js +45 -45
- package/templates/spa/views/default/spa/src/composables/useLocalStorage.js +43 -43
- package/templates/spa/views/default/spa/src/composables/useLocale.js +79 -79
- package/templates/spa/views/default/spa/src/composables/useWebSocket.js +93 -93
- package/templates/spa/views/default/spa/src/main.js +108 -108
- package/templates/spa/views/default/spa/src/plugins/alert.js +96 -96
- package/templates/spa/views/default/spa/src/plugins/toast.js +79 -79
- package/templates/spa/views/default/spa/src/router/index.js +29 -29
- package/templates/spa/views/default/spa/src/stores/auth.js +58 -58
- package/templates/spa/views/default/spa/src/views/BoardDetail.vue +169 -169
- package/templates/spa/views/default/spa/src/views/BoardForm.vue +192 -192
- package/templates/spa/views/default/spa/src/views/BoardList.vue +129 -129
- package/templates/spa/views/default/spa/src/views/ChatView.vue +327 -327
- package/templates/spa/views/default/spa/src/views/FeaturesView.vue +242 -242
- package/templates/spa/views/default/spa/src/views/HomeView.vue +215 -215
- package/templates/spa/views/default/spa/src/views/Login.vue +82 -82
- package/templates/spa/views/default/spa/src/views/Profile.vue +85 -85
- package/templates/spa/views/default/spa/src/views/Register.vue +84 -84
- package/templates/spa/views/default/spa/vite.config.js +28 -28
- package/templates/spa/views/default/spa/yarn.lock +633 -633
- package/templates/spa/ws/ChatHandler.js +138 -138
- package/templates/ssr/controllers/AuthController.js +119 -119
- package/templates/ssr/controllers/ChatController.js +15 -15
- package/templates/ssr/controllers/FeaturesController.js +15 -15
- package/templates/ssr/controllers/HomeController.js +21 -21
- package/templates/ssr/controllers/PostController.js +214 -214
- package/templates/ssr/controllers/UserController.js +48 -48
- package/templates/ssr/meta.json +11 -11
- package/templates/ssr/public/css/fx-ui.css +43 -43
- package/templates/ssr/public/css/landing.css +220 -220
- package/templates/ssr/public/css/style.css +1011 -1011
- package/templates/ssr/public/js/fx-client.js +107 -107
- package/templates/ssr/public/js/fx-ui.js +124 -124
- package/templates/ssr/routes/web.js +46 -46
- package/templates/ssr/services/AuthService.js +48 -48
- package/templates/ssr/services/PostService.js +372 -372
- package/templates/ssr/services/UserService.js +48 -48
- package/templates/ssr/views/default/errors/404.html +11 -11
- package/templates/ssr/views/default/errors/500.html +48 -48
- package/templates/ssr/views/default/layouts/main.html +93 -93
- package/templates/ssr/views/default/pages/board/form.html +240 -240
- package/templates/ssr/views/default/pages/board/index.html +73 -73
- package/templates/ssr/views/default/pages/board/show.html +148 -148
- package/templates/ssr/views/default/pages/chat.html +288 -288
- package/templates/ssr/views/default/pages/features.html +373 -373
- package/templates/ssr/views/default/pages/home.html +258 -258
- package/templates/ssr/views/default/pages/login.html +27 -27
- package/templates/ssr/views/default/pages/profile.html +36 -36
- package/templates/ssr/views/default/pages/register.html +35 -35
- package/templates/ssr/views/default/partials/pagination.html +75 -75
- 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,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();
|