@fuzionx/framework 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/db-sync.js +99 -0
- package/cli/fx.js +3 -0
- package/cli/index.js +329 -0
- package/cli/templates/app/.env.example.tpl +14 -0
- package/cli/templates/app/.gitignore.tpl +4 -0
- package/cli/templates/app/app.js.tpl +14 -0
- package/cli/templates/app/controllers/HomeController.js +13 -0
- package/cli/templates/app/fuzionx.yaml.tpl +30 -0
- package/cli/templates/app/package.json.tpl +15 -0
- package/cli/templates/app/routes/api.js.tpl +7 -0
- package/cli/templates/app/routes/web.js.tpl +5 -0
- package/cli/templates/app/views/default/errors/404.html +15 -0
- package/cli/templates/app/views/default/errors/500.html +14 -0
- package/cli/templates/app/views/default/layouts/main.html +22 -0
- package/cli/templates/app/views/default/pages/home.html +188 -0
- package/cli/templates/make/controller.js.tpl +40 -0
- package/cli/templates/make/event.js.tpl +8 -0
- package/cli/templates/make/job.js.tpl +10 -0
- package/cli/templates/make/middleware.js.tpl +10 -0
- package/cli/templates/make/model.js.tpl +15 -0
- package/cli/templates/make/service.js.tpl +15 -0
- package/cli/templates/make/task.js.tpl +15 -0
- package/cli/templates/make/test.js.tpl +7 -0
- package/cli/templates/make/worker.js.tpl +14 -0
- package/cli/templates/make/ws.js.tpl +18 -0
- package/lib/core/Application.js +10 -7
- package/package.json +4 -2
package/cli/db-sync.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* db:sync — 모델 기반 스키마 SQL 생성
|
|
3
|
+
*
|
|
4
|
+
* 사용: fx db:sync [--dry-run]
|
|
5
|
+
*
|
|
6
|
+
* 모델의 static 멤버(table, primaryKey, timestamps, softDelete)에서
|
|
7
|
+
* CREATE TABLE SQL을 생성합니다. 실제 마이그레이션 시스템이 아닌
|
|
8
|
+
* 빠른 프로토타이핑용 도구입니다.
|
|
9
|
+
*
|
|
10
|
+
* @see docs/framework/02-database-orm.md
|
|
11
|
+
*/
|
|
12
|
+
import { promises as fs } from 'node:fs';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 모델 클래스에서 CREATE TABLE SQL 생성
|
|
17
|
+
* @param {typeof import('../lib/Model.js').default} ModelClass
|
|
18
|
+
* @returns {string}
|
|
19
|
+
*/
|
|
20
|
+
export function generateCreateTable(ModelClass) {
|
|
21
|
+
const table = ModelClass.table;
|
|
22
|
+
const pk = ModelClass.primaryKey || 'id';
|
|
23
|
+
const timestamps = ModelClass.timestamps !== false;
|
|
24
|
+
const softDelete = ModelClass.softDelete || false;
|
|
25
|
+
|
|
26
|
+
if (!table) return '';
|
|
27
|
+
|
|
28
|
+
const columns = [];
|
|
29
|
+
|
|
30
|
+
// 기본키
|
|
31
|
+
columns.push(` \`${pk}\` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY`);
|
|
32
|
+
|
|
33
|
+
// timestamps
|
|
34
|
+
if (timestamps) {
|
|
35
|
+
columns.push(` \`created_at\` DATETIME DEFAULT CURRENT_TIMESTAMP`);
|
|
36
|
+
columns.push(` \`updated_at\` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// softDelete
|
|
40
|
+
if (softDelete) {
|
|
41
|
+
columns.push(` \`deleted_at\` DATETIME DEFAULT NULL`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return `CREATE TABLE IF NOT EXISTS \`${table}\` (\n${columns.join(',\n')}\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 디렉토리의 모든 모델에서 SQL 생성
|
|
49
|
+
* @param {string} modelsDir
|
|
50
|
+
* @returns {Promise<string>}
|
|
51
|
+
*/
|
|
52
|
+
export async function generateSchema(modelsDir) {
|
|
53
|
+
const sqls = [];
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const entries = await fs.readdir(modelsDir, { withFileTypes: true });
|
|
57
|
+
const files = entries
|
|
58
|
+
.filter(e => e.isFile() && e.name.endsWith('.js') && !e.name.startsWith('.'))
|
|
59
|
+
.map(e => path.join(modelsDir, e.name));
|
|
60
|
+
|
|
61
|
+
for (const file of files) {
|
|
62
|
+
const mod = await import(file);
|
|
63
|
+
const ModelClass = mod.default;
|
|
64
|
+
if (ModelClass?.table) {
|
|
65
|
+
sqls.push(generateCreateTable(ModelClass));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
// 디렉토리 없음
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return sqls.join('\n');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* CLI 엔트리
|
|
77
|
+
*/
|
|
78
|
+
export async function run(args) {
|
|
79
|
+
const dryRun = args.includes('--dry-run');
|
|
80
|
+
const modelsDir = path.resolve('app/models');
|
|
81
|
+
|
|
82
|
+
console.log(`[db:sync] Scanning ${modelsDir}...`);
|
|
83
|
+
|
|
84
|
+
const sql = await generateSchema(modelsDir);
|
|
85
|
+
|
|
86
|
+
if (!sql) {
|
|
87
|
+
console.log('[db:sync] No models found.');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (dryRun) {
|
|
92
|
+
console.log('\n-- DRY RUN (preview only) --\n');
|
|
93
|
+
console.log(sql);
|
|
94
|
+
} else {
|
|
95
|
+
console.log('\n-- Generated SQL --\n');
|
|
96
|
+
console.log(sql);
|
|
97
|
+
console.log('-- Execute this SQL against your database to sync schema --');
|
|
98
|
+
}
|
|
99
|
+
}
|
package/cli/fx.js
ADDED
package/cli/index.js
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI — fx 명령어 핸들러
|
|
3
|
+
*
|
|
4
|
+
* fx new <name> 앱 스캐폴딩
|
|
5
|
+
* fx make:controller <Name> 컨트롤러 생성
|
|
6
|
+
* fx make:service <Name> 서비스 생성
|
|
7
|
+
* fx make:model <Name> 모델 생성
|
|
8
|
+
* fx make:middleware <Name> 미들웨어 생성
|
|
9
|
+
* fx make:job <Name> Job 생성
|
|
10
|
+
* fx make:task <Name> Task 생성
|
|
11
|
+
* fx make:ws <Name> WsHandler 생성
|
|
12
|
+
* fx make:event <Name> 이벤트 핸들러 생성
|
|
13
|
+
* fx make:worker <Name> Worker 생성
|
|
14
|
+
* fx make:test <Name> 테스트 생성
|
|
15
|
+
*
|
|
16
|
+
* @see docs/framework/16-cli.md
|
|
17
|
+
*/
|
|
18
|
+
import { promises as fs } from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
|
|
22
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const TPL_DIR = path.join(__dirname, 'templates');
|
|
24
|
+
|
|
25
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
26
|
+
// Template Engine
|
|
27
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 간단 템플릿 치환 — {{varName}} → value
|
|
31
|
+
* @param {string} template
|
|
32
|
+
* @param {object} vars - { Name, name, nameLower, tableName, dbName }
|
|
33
|
+
* @returns {string}
|
|
34
|
+
*/
|
|
35
|
+
function render(template, vars) {
|
|
36
|
+
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? '');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 템플릿 파일 읽기 + 치환
|
|
41
|
+
* @param {string} relativePath - templates/ 기준 상대 경로
|
|
42
|
+
* @param {object} vars
|
|
43
|
+
* @returns {Promise<string>}
|
|
44
|
+
*/
|
|
45
|
+
async function loadTemplate(relativePath, vars = {}) {
|
|
46
|
+
const content = await fs.readFile(path.join(TPL_DIR, relativePath), 'utf-8');
|
|
47
|
+
return render(content, vars);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
51
|
+
// fx make:* — 코드 생성
|
|
52
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
53
|
+
|
|
54
|
+
const TYPE_DIRS = {
|
|
55
|
+
controller: 'controllers',
|
|
56
|
+
service: 'services',
|
|
57
|
+
model: 'models',
|
|
58
|
+
middleware: 'middleware',
|
|
59
|
+
job: 'jobs',
|
|
60
|
+
task: 'jobs',
|
|
61
|
+
ws: 'ws',
|
|
62
|
+
event: 'events',
|
|
63
|
+
worker: 'workers',
|
|
64
|
+
test: 'tests',
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const TYPE_SUFFIXES = {
|
|
68
|
+
controller: 'Controller',
|
|
69
|
+
service: 'Service',
|
|
70
|
+
model: '',
|
|
71
|
+
middleware: 'Middleware',
|
|
72
|
+
job: 'Job',
|
|
73
|
+
task: '',
|
|
74
|
+
ws: 'Handler',
|
|
75
|
+
event: '',
|
|
76
|
+
worker: '',
|
|
77
|
+
test: '.test',
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* fx make:<type> <Name> — 코드 생성
|
|
82
|
+
* @param {string} type - 'controller', 'service', 'model', 등
|
|
83
|
+
* @param {string} name - PascalCase 이름 (e.g. 'User')
|
|
84
|
+
* @param {string} [baseDir='.'] - 프로젝트 루트
|
|
85
|
+
* @returns {Promise<string>} - 생성된 파일 경로
|
|
86
|
+
*/
|
|
87
|
+
export async function makeFile(type, name, baseDir = '.') {
|
|
88
|
+
const dir = TYPE_DIRS[type];
|
|
89
|
+
if (!dir) throw new Error(`Unknown type: ${type}`);
|
|
90
|
+
|
|
91
|
+
const suffix = TYPE_SUFFIXES[type];
|
|
92
|
+
const fileName = `${name}${suffix}.js`;
|
|
93
|
+
const filePath = path.join(baseDir, dir, fileName);
|
|
94
|
+
|
|
95
|
+
const vars = {
|
|
96
|
+
Name: name,
|
|
97
|
+
name: name,
|
|
98
|
+
nameLower: name.toLowerCase(),
|
|
99
|
+
tableName: name.toLowerCase() + 's',
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const content = await loadTemplate(`make/${type}.js.tpl`, vars);
|
|
103
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
104
|
+
await fs.writeFile(filePath, content);
|
|
105
|
+
|
|
106
|
+
return filePath;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
110
|
+
// fx new <name> — 앱 스캐폴딩
|
|
111
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
112
|
+
|
|
113
|
+
const APP_FILES = [
|
|
114
|
+
{ tpl: 'app/package.json.tpl', dest: 'package.json' },
|
|
115
|
+
{ tpl: 'app/fuzionx.yaml.tpl', dest: 'fuzionx.yaml' },
|
|
116
|
+
{ tpl: 'app/app.js.tpl', dest: 'app.js' },
|
|
117
|
+
{ tpl: 'app/routes/web.js.tpl', dest: 'routes/web.js' },
|
|
118
|
+
{ tpl: 'app/routes/api.js.tpl', dest: 'routes/api.js' },
|
|
119
|
+
{ tpl: 'app/.env.example.tpl', dest: '.env.example' },
|
|
120
|
+
{ tpl: 'app/.env.example.tpl', dest: '.env' },
|
|
121
|
+
{ tpl: 'app/.gitignore.tpl', dest: '.gitignore' },
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
/** 스캐폴딩 디렉토리 (16-cli.md) */
|
|
125
|
+
const APP_DIRS = [
|
|
126
|
+
'controllers',
|
|
127
|
+
'models',
|
|
128
|
+
'services',
|
|
129
|
+
'middleware',
|
|
130
|
+
'ws',
|
|
131
|
+
'jobs',
|
|
132
|
+
'events',
|
|
133
|
+
'workers',
|
|
134
|
+
'migrations',
|
|
135
|
+
'seeds',
|
|
136
|
+
'storage/logs',
|
|
137
|
+
'storage/uploads',
|
|
138
|
+
'public/css',
|
|
139
|
+
'public/js',
|
|
140
|
+
'locales',
|
|
141
|
+
'tests',
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* fx new <name> — 앱 스캐폴딩
|
|
146
|
+
* @param {string} name - 프로젝트 이름
|
|
147
|
+
* @param {string} [targetDir] - 기본: ./<name>
|
|
148
|
+
*/
|
|
149
|
+
export async function createApp(name, targetDir) {
|
|
150
|
+
const dir = targetDir || path.resolve(name);
|
|
151
|
+
const vars = {
|
|
152
|
+
name,
|
|
153
|
+
dbName: name.replace(/-/g, '_'),
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// 디렉토리 생성
|
|
157
|
+
for (const d of APP_DIRS) {
|
|
158
|
+
await fs.mkdir(path.join(dir, d), { recursive: true });
|
|
159
|
+
await fs.writeFile(path.join(dir, d, '.gitkeep'), '');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 템플릿 파일 생성
|
|
163
|
+
for (const { tpl, dest } of APP_FILES) {
|
|
164
|
+
const content = await loadTemplate(tpl, vars);
|
|
165
|
+
const fullPath = path.join(dir, dest);
|
|
166
|
+
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
167
|
+
await fs.writeFile(fullPath, content);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// HomeController 복사 (16-cli.md)
|
|
171
|
+
const hcSrc = path.join(TPL_DIR, 'app/controllers/HomeController.js');
|
|
172
|
+
const hcDst = path.join(dir, 'controllers/HomeController.js');
|
|
173
|
+
await fs.copyFile(hcSrc, hcDst);
|
|
174
|
+
|
|
175
|
+
// Views — views/{theme}/ 구조 (03, 16-cli.md)
|
|
176
|
+
const viewsSrc = path.join(TPL_DIR, 'app/views/default');
|
|
177
|
+
const viewsDst = path.join(dir, 'views/default');
|
|
178
|
+
|
|
179
|
+
// layouts/main.html
|
|
180
|
+
const layoutDir = path.join(viewsDst, 'layouts');
|
|
181
|
+
await fs.mkdir(layoutDir, { recursive: true });
|
|
182
|
+
await fs.copyFile(path.join(viewsSrc, 'layouts/main.html'), path.join(layoutDir, 'main.html'));
|
|
183
|
+
|
|
184
|
+
// pages/home.html
|
|
185
|
+
const pagesDir = path.join(viewsDst, 'pages');
|
|
186
|
+
await fs.mkdir(pagesDir, { recursive: true });
|
|
187
|
+
await fs.copyFile(path.join(viewsSrc, 'pages/home.html'), path.join(pagesDir, 'home.html'));
|
|
188
|
+
|
|
189
|
+
// errors/404.html, 500.html
|
|
190
|
+
const errDir = path.join(viewsDst, 'errors');
|
|
191
|
+
await fs.mkdir(errDir, { recursive: true });
|
|
192
|
+
await fs.copyFile(path.join(viewsSrc, 'errors/404.html'), path.join(errDir, '404.html'));
|
|
193
|
+
await fs.copyFile(path.join(viewsSrc, 'errors/500.html'), path.join(errDir, '500.html'));
|
|
194
|
+
|
|
195
|
+
return dir;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
199
|
+
// CLI 엔트리
|
|
200
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* CLI 엔트리 (fx 명령어)
|
|
204
|
+
* @param {string[]} args - process.argv.slice(2)
|
|
205
|
+
*/
|
|
206
|
+
export async function run(args) {
|
|
207
|
+
const [command, ...rest] = args;
|
|
208
|
+
|
|
209
|
+
if (command === 'new') {
|
|
210
|
+
const name = rest[0];
|
|
211
|
+
if (!name) { console.error('Usage: fx new <name>'); process.exit(1); }
|
|
212
|
+
const dir = await createApp(name);
|
|
213
|
+
console.log(`✅ Created ${name} at ${dir}`);
|
|
214
|
+
console.log(`\n cd ${name}`);
|
|
215
|
+
console.log(` npm install`);
|
|
216
|
+
console.log(` npm run dev\n`);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (command?.startsWith('make:')) {
|
|
221
|
+
const type = command.replace('make:', '');
|
|
222
|
+
const name = rest[0];
|
|
223
|
+
if (!name) { console.error(`Usage: fx ${command} <Name>`); process.exit(1); }
|
|
224
|
+
const file = await makeFile(type, name);
|
|
225
|
+
console.log(`✅ Created ${file}`);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ── fx dev — 개발 서버 (Node --watch) ──
|
|
230
|
+
if (command === 'dev') {
|
|
231
|
+
const { execSync } = await import('node:child_process');
|
|
232
|
+
const port = rest.find(a => a.startsWith('--port='))?.split('=')[1] || '';
|
|
233
|
+
const entry = 'app.js';
|
|
234
|
+
const env = port ? `PORT=${port} ` : '';
|
|
235
|
+
console.log(`🚀 Starting dev server...`);
|
|
236
|
+
try {
|
|
237
|
+
execSync(`${env}node --watch ${entry}`, { stdio: 'inherit', cwd: process.cwd() });
|
|
238
|
+
} catch {}
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ── fx test — 테스트 실행 ──
|
|
243
|
+
if (command === 'test') {
|
|
244
|
+
const { execSync } = await import('node:child_process');
|
|
245
|
+
try {
|
|
246
|
+
execSync('npx vitest run', { stdio: 'inherit', cwd: process.cwd() });
|
|
247
|
+
} catch { process.exit(1); }
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ── fx routes — 라우트 테이블 출력 ──
|
|
252
|
+
if (command === 'routes') {
|
|
253
|
+
try {
|
|
254
|
+
const appMod = await import(path.resolve('app.js'));
|
|
255
|
+
const app = appMod.default || appMod.app;
|
|
256
|
+
if (!app?._router) { console.error('Cannot load app routes'); return; }
|
|
257
|
+
const routes = app._router.getRoutes();
|
|
258
|
+
console.log('');
|
|
259
|
+
console.log(' METHOD PATH HANDLER MIDDLEWARE');
|
|
260
|
+
console.log(' ────── ───── ──────── ──────────');
|
|
261
|
+
for (const r of routes) {
|
|
262
|
+
const method = r.method.padEnd(8);
|
|
263
|
+
const rPath = r.path.padEnd(22);
|
|
264
|
+
const handler = r.handler?.__handler__
|
|
265
|
+
? `${r.handler.controller?.name || ''}.${r.handler.method}`
|
|
266
|
+
: (typeof r.handler === 'function' ? r.handler.name || 'anonymous' : '');
|
|
267
|
+
const mw = (r.middleware || []).join(', ');
|
|
268
|
+
console.log(` ${method} ${rPath} ${handler.padEnd(26)} ${mw}`);
|
|
269
|
+
}
|
|
270
|
+
console.log('');
|
|
271
|
+
} catch (err) { console.error('Failed to load routes:', err.message); }
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ── fx config — fuzionx.yaml 출력 ──
|
|
276
|
+
if (command === 'config') {
|
|
277
|
+
try {
|
|
278
|
+
const configPath = path.resolve('fuzionx.yaml');
|
|
279
|
+
const content = await fs.readFile(configPath, 'utf-8');
|
|
280
|
+
console.log('\n📄 fuzionx.yaml:\n');
|
|
281
|
+
console.log(content);
|
|
282
|
+
} catch (err) { console.error('Cannot read fuzionx.yaml:', err.message); }
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ── fx db:sync — 모델 ↔ DB 스키마 diff ──
|
|
287
|
+
if (command === 'db:sync') {
|
|
288
|
+
try {
|
|
289
|
+
const modelsDir = path.resolve('models');
|
|
290
|
+
const files = await fs.readdir(modelsDir);
|
|
291
|
+
const jsFiles = files.filter(f => f.endsWith('.js'));
|
|
292
|
+
console.log('\n📊 Model Schema Status:\n');
|
|
293
|
+
for (const file of jsFiles) {
|
|
294
|
+
const mod = await import(path.join(modelsDir, file));
|
|
295
|
+
const Model = mod.default;
|
|
296
|
+
if (!Model) continue;
|
|
297
|
+
const name = path.basename(file, '.js');
|
|
298
|
+
const table = Model.table || name.toLowerCase() + 's';
|
|
299
|
+
const cols = Object.keys(Model.columns || {});
|
|
300
|
+
const status = rest.includes('--apply') ? '✅ synced' : '⏳ pending';
|
|
301
|
+
console.log(` ${name.padEnd(20)} table: ${table.padEnd(20)} columns: ${cols.length} ${status}`);
|
|
302
|
+
}
|
|
303
|
+
if (!rest.includes('--apply')) {
|
|
304
|
+
console.log('\n Run with --apply to sync changes to database.');
|
|
305
|
+
}
|
|
306
|
+
console.log('');
|
|
307
|
+
} catch (err) { console.error('db:sync error:', err.message); }
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
console.log(`
|
|
312
|
+
fx new <name> Create new app
|
|
313
|
+
fx make:controller <Name> Create controller
|
|
314
|
+
fx make:service <Name> Create service
|
|
315
|
+
fx make:model <Name> Create model
|
|
316
|
+
fx make:middleware <Name> Create middleware
|
|
317
|
+
fx make:job <Name> Create job
|
|
318
|
+
fx make:task <Name> Create task
|
|
319
|
+
fx make:ws <Name> Create WsHandler
|
|
320
|
+
fx make:event <Name> Create event handler
|
|
321
|
+
fx make:worker <Name> Create worker (worker_threads)
|
|
322
|
+
fx make:test <Name> Create test
|
|
323
|
+
fx dev Start dev server (--watch)
|
|
324
|
+
fx test Run tests
|
|
325
|
+
fx routes Print route table
|
|
326
|
+
fx config Print fuzionx.yaml
|
|
327
|
+
fx db:sync Sync models → DB (--apply)
|
|
328
|
+
`);
|
|
329
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Application } from '@fuzionx/framework';
|
|
2
|
+
import webRoutes from './routes/web.js';
|
|
3
|
+
import apiRoutes from './routes/api.js';
|
|
4
|
+
|
|
5
|
+
const app = new Application({ configPath: './fuzionx.yaml' });
|
|
6
|
+
|
|
7
|
+
app.routes(webRoutes);
|
|
8
|
+
app.routes(apiRoutes);
|
|
9
|
+
|
|
10
|
+
await app.boot();
|
|
11
|
+
|
|
12
|
+
app.listen(49080, () => {
|
|
13
|
+
console.log('🚀 FuzionX running on http://localhost:49080');
|
|
14
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# FuzionX Configuration
|
|
2
|
+
bridge:
|
|
3
|
+
port: 49080
|
|
4
|
+
workers: 4
|
|
5
|
+
worker_timeout: 30
|
|
6
|
+
|
|
7
|
+
rate_limit:
|
|
8
|
+
enabled: true
|
|
9
|
+
per_ip: 1000
|
|
10
|
+
|
|
11
|
+
database:
|
|
12
|
+
main:
|
|
13
|
+
driver: sqlite
|
|
14
|
+
path: ./storage/database.sqlite
|
|
15
|
+
|
|
16
|
+
app:
|
|
17
|
+
name: '{{name}}'
|
|
18
|
+
environment: development
|
|
19
|
+
auth:
|
|
20
|
+
secret: 'change-me-in-production'
|
|
21
|
+
accessTtl: '15m'
|
|
22
|
+
i18n:
|
|
23
|
+
default_locale: 'ko'
|
|
24
|
+
fallback: 'en'
|
|
25
|
+
docs:
|
|
26
|
+
enabled: true
|
|
27
|
+
path: '/docs'
|
|
28
|
+
|
|
29
|
+
themes:
|
|
30
|
+
default: 'default'
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{% extends "layouts/main.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}404 — 찾을 수 없습니다{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block content %}
|
|
6
|
+
<div style="text-align:center;padding:80px 20px;">
|
|
7
|
+
<h1 style="font-size:72px;color:#e74c3c;">{{ error.code }}</h1>
|
|
8
|
+
<p style="font-size:20px;margin:16px 0;">{{ error.message }}</p>
|
|
9
|
+
<p style="color:#888;">요청: {{ request.url }}</p>
|
|
10
|
+
{% if config.debug %}
|
|
11
|
+
<pre style="text-align:left;max-width:600px;margin:24px auto;background:#f5f5f5;padding:16px;border-radius:8px;overflow:auto;">{{ error.stack }}</pre>
|
|
12
|
+
{% endif %}
|
|
13
|
+
<a href="/" style="display:inline-block;margin-top:24px;padding:12px 24px;background:#e74c3c;color:#fff;text-decoration:none;border-radius:6px;">홈으로 돌아가기</a>
|
|
14
|
+
</div>
|
|
15
|
+
{% endblock %}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{% extends "layouts/main.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}500 — 서버 오류{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block content %}
|
|
6
|
+
<div style="text-align:center;padding:80px 20px;">
|
|
7
|
+
<h1 style="font-size:72px;color:#e74c3c;">500</h1>
|
|
8
|
+
<p style="font-size:20px;margin:16px 0;">{{ error.message | default(value='Internal Server Error') }}</p>
|
|
9
|
+
{% if config.debug and error.stack %}
|
|
10
|
+
<pre style="text-align:left;max-width:600px;margin:24px auto;background:#f5f5f5;padding:16px;border-radius:8px;overflow:auto;">{{ error.stack }}</pre>
|
|
11
|
+
{% endif %}
|
|
12
|
+
<a href="/" style="display:inline-block;margin-top:24px;padding:12px 24px;background:#e74c3c;color:#fff;text-decoration:none;border-radius:6px;">홈으로 돌아가기</a>
|
|
13
|
+
</div>
|
|
14
|
+
{% endblock %}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="{{ locale | default(value='ko') }}">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>{% block title %}{{ config.app.name | default(value='FuzionX') }}{% endblock %}</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
body { font-family: system-ui, -apple-system, sans-serif; line-height: 1.6; color: #333; }
|
|
10
|
+
</style>
|
|
11
|
+
{% block head %}{% endblock %}
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
{% include "partials/header.html" ignore missing %}
|
|
15
|
+
|
|
16
|
+
<main>
|
|
17
|
+
{% block content %}{% endblock %}
|
|
18
|
+
</main>
|
|
19
|
+
|
|
20
|
+
{% include "partials/footer.html" ignore missing %}
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="ko">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>FuzionX</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;800&display=swap');
|
|
10
|
+
|
|
11
|
+
body {
|
|
12
|
+
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
|
13
|
+
min-height: 100vh;
|
|
14
|
+
display: flex;
|
|
15
|
+
align-items: center;
|
|
16
|
+
justify-content: center;
|
|
17
|
+
background: linear-gradient(135deg, #0f0c29 0%, #1a1a3e 40%, #24243e 100%);
|
|
18
|
+
color: #e0e0e0;
|
|
19
|
+
overflow: hidden;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.container {
|
|
23
|
+
text-align: center;
|
|
24
|
+
z-index: 1;
|
|
25
|
+
animation: fadeInUp 0.8s ease-out;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.logo {
|
|
29
|
+
font-size: 4rem;
|
|
30
|
+
font-weight: 800;
|
|
31
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
|
|
32
|
+
-webkit-background-clip: text;
|
|
33
|
+
background-clip: text;
|
|
34
|
+
-webkit-text-fill-color: transparent;
|
|
35
|
+
letter-spacing: -2px;
|
|
36
|
+
margin-bottom: 0.5rem;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.subtitle {
|
|
40
|
+
font-size: 1.1rem;
|
|
41
|
+
font-weight: 300;
|
|
42
|
+
color: rgba(255, 255, 255, 0.5);
|
|
43
|
+
margin-bottom: 2.5rem;
|
|
44
|
+
letter-spacing: 2px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.card {
|
|
48
|
+
background: rgba(255, 255, 255, 0.05);
|
|
49
|
+
backdrop-filter: blur(20px);
|
|
50
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
51
|
+
border-radius: 16px;
|
|
52
|
+
padding: 2rem 3rem;
|
|
53
|
+
max-width: 480px;
|
|
54
|
+
margin: 0 auto 2rem;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.version {
|
|
58
|
+
display: inline-block;
|
|
59
|
+
background: linear-gradient(135deg, #667eea, #764ba2);
|
|
60
|
+
color: white;
|
|
61
|
+
padding: 4px 14px;
|
|
62
|
+
border-radius: 20px;
|
|
63
|
+
font-size: 0.75rem;
|
|
64
|
+
font-weight: 600;
|
|
65
|
+
letter-spacing: 1px;
|
|
66
|
+
margin-bottom: 1.5rem;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.features {
|
|
70
|
+
display: grid;
|
|
71
|
+
grid-template-columns: 1fr 1fr;
|
|
72
|
+
gap: 1rem;
|
|
73
|
+
text-align: left;
|
|
74
|
+
margin-top: 1.5rem;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.feature {
|
|
78
|
+
display: flex;
|
|
79
|
+
align-items: center;
|
|
80
|
+
gap: 8px;
|
|
81
|
+
font-size: 0.85rem;
|
|
82
|
+
color: rgba(255, 255, 255, 0.7);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.feature span {
|
|
86
|
+
font-size: 1.1rem;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.links {
|
|
90
|
+
display: flex;
|
|
91
|
+
gap: 1rem;
|
|
92
|
+
justify-content: center;
|
|
93
|
+
margin-top: 1rem;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.links a {
|
|
97
|
+
color: rgba(255, 255, 255, 0.6);
|
|
98
|
+
text-decoration: none;
|
|
99
|
+
font-size: 0.85rem;
|
|
100
|
+
padding: 8px 20px;
|
|
101
|
+
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
102
|
+
border-radius: 8px;
|
|
103
|
+
transition: all 0.3s ease;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.links a:hover {
|
|
107
|
+
color: #fff;
|
|
108
|
+
border-color: #667eea;
|
|
109
|
+
background: rgba(102, 126, 234, 0.1);
|
|
110
|
+
transform: translateY(-2px);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.hint {
|
|
114
|
+
margin-top: 2rem;
|
|
115
|
+
font-size: 0.75rem;
|
|
116
|
+
color: rgba(255, 255, 255, 0.3);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.hint code {
|
|
120
|
+
background: rgba(255, 255, 255, 0.08);
|
|
121
|
+
padding: 2px 8px;
|
|
122
|
+
border-radius: 4px;
|
|
123
|
+
font-family: 'JetBrains Mono', monospace;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* Background orbs */
|
|
127
|
+
.orb {
|
|
128
|
+
position: fixed;
|
|
129
|
+
border-radius: 50%;
|
|
130
|
+
filter: blur(80px);
|
|
131
|
+
opacity: 0.3;
|
|
132
|
+
animation: float 8s ease-in-out infinite;
|
|
133
|
+
}
|
|
134
|
+
.orb-1 { width: 400px; height: 400px; background: #667eea; top: -100px; right: -100px; }
|
|
135
|
+
.orb-2 { width: 300px; height: 300px; background: #764ba2; bottom: -80px; left: -80px; animation-delay: -4s; }
|
|
136
|
+
.orb-3 { width: 200px; height: 200px; background: #f093fb; top: 50%; left: 60%; animation-delay: -2s; }
|
|
137
|
+
|
|
138
|
+
@keyframes fadeInUp {
|
|
139
|
+
from { opacity: 0; transform: translateY(30px); }
|
|
140
|
+
to { opacity: 1; transform: translateY(0); }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@keyframes float {
|
|
144
|
+
0%, 100% { transform: translate(0, 0); }
|
|
145
|
+
50% { transform: translate(30px, -30px); }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@media (max-width: 600px) {
|
|
149
|
+
.logo { font-size: 2.5rem; }
|
|
150
|
+
.card { padding: 1.5rem; margin: 0 1rem; }
|
|
151
|
+
.features { grid-template-columns: 1fr; }
|
|
152
|
+
}
|
|
153
|
+
</style>
|
|
154
|
+
</head>
|
|
155
|
+
<body>
|
|
156
|
+
<div class="orb orb-1"></div>
|
|
157
|
+
<div class="orb orb-2"></div>
|
|
158
|
+
<div class="orb orb-3"></div>
|
|
159
|
+
|
|
160
|
+
<div class="container">
|
|
161
|
+
<h1 class="logo">FuzionX</h1>
|
|
162
|
+
<p class="subtitle">HIGH-PERFORMANCE NODE.JS FRAMEWORK</p>
|
|
163
|
+
|
|
164
|
+
<div class="card">
|
|
165
|
+
<div class="version">v0.1.0 · POWERED BY RUST</div>
|
|
166
|
+
|
|
167
|
+
<div class="features">
|
|
168
|
+
<div class="feature"><span>⚡</span> 500K+ RPS</div>
|
|
169
|
+
<div class="feature"><span>🦀</span> Rust N-API Bridge</div>
|
|
170
|
+
<div class="feature"><span>🎯</span> MVC Architecture</div>
|
|
171
|
+
<div class="feature"><span>🔌</span> WebSocket</div>
|
|
172
|
+
<div class="feature"><span>🗄️</span> Multi-DB ORM</div>
|
|
173
|
+
<div class="feature"><span>🔐</span> Auth & Session</div>
|
|
174
|
+
<div class="feature"><span>📡</span> Event System</div>
|
|
175
|
+
<div class="feature"><span>⏰</span> Job Scheduler</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<div class="links">
|
|
180
|
+
<a href="https://github.com/saytohenry/fuzionx">GitHub</a>
|
|
181
|
+
<a href="/docs">API Docs</a>
|
|
182
|
+
<a href="/api/health">Health Check</a>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<p class="hint">Edit <code>routes/web.js</code> to get started</p>
|
|
186
|
+
</div>
|
|
187
|
+
</body>
|
|
188
|
+
</html>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Controller } from '@fuzionx/framework';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* {{Name}} 컨트롤러.
|
|
5
|
+
* routes/ 파일에서 라우트를 등록하세요.
|
|
6
|
+
*/
|
|
7
|
+
export default class {{Name}}Controller extends Controller {
|
|
8
|
+
|
|
9
|
+
/** 목록 조회 */
|
|
10
|
+
async index(ctx) {
|
|
11
|
+
const items = await this.db.{{Name}}.paginate(ctx.query.page || 1, 20);
|
|
12
|
+
ctx.json(items);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** 상세 조회 */
|
|
16
|
+
async show(ctx) {
|
|
17
|
+
const item = await this.db.{{Name}}.findOrFail(ctx.params.id);
|
|
18
|
+
ctx.json(item);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 생성 */
|
|
22
|
+
async store(ctx) {
|
|
23
|
+
const item = await this.db.{{Name}}.create(ctx.body);
|
|
24
|
+
ctx.status(201).json(item);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 수정 */
|
|
28
|
+
async update(ctx) {
|
|
29
|
+
const item = await this.db.{{Name}}.findOrFail(ctx.params.id);
|
|
30
|
+
await item.update(ctx.body);
|
|
31
|
+
ctx.json(item);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 삭제 */
|
|
35
|
+
async destroy(ctx) {
|
|
36
|
+
const item = await this.db.{{Name}}.findOrFail(ctx.params.id);
|
|
37
|
+
await item.delete();
|
|
38
|
+
ctx.status(204).end();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { SQLiteModel } from '@fuzionx/framework';
|
|
2
|
+
|
|
3
|
+
export default class {{Name}} extends SQLiteModel {
|
|
4
|
+
static table = '{{tableName}}';
|
|
5
|
+
static timestamps = true;
|
|
6
|
+
static hidden = [];
|
|
7
|
+
|
|
8
|
+
static columns = {
|
|
9
|
+
id: { type: 'increments' },
|
|
10
|
+
name: { type: 'string', length: 100 },
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// 관계 정의
|
|
14
|
+
// posts() { return this.hasMany('Post'); }
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Service } from '@fuzionx/framework';
|
|
2
|
+
|
|
3
|
+
export default class {{Name}}Service extends Service {
|
|
4
|
+
async findAll() {
|
|
5
|
+
return this.db.{{Name}}.query().paginate();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async findById(id) {
|
|
9
|
+
return this.db.{{Name}}.findOrFail(id);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async create(data) {
|
|
13
|
+
return this.db.{{Name}}.create(data);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Task } from '@fuzionx/framework';
|
|
2
|
+
|
|
3
|
+
export default class {{Name}} extends Task {
|
|
4
|
+
static queue = 'default';
|
|
5
|
+
static retries = 3;
|
|
6
|
+
static retryDelay = 5000;
|
|
7
|
+
|
|
8
|
+
async handle(data) {
|
|
9
|
+
// 비동기 작업 로직
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async failed(data, error) {
|
|
13
|
+
this.logger.error('작업 최종 실패', { error: error.message });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* {{Name}} Worker — CPU-heavy 작업 처리
|
|
3
|
+
*
|
|
4
|
+
* 사용: this.worker.run('{{nameLower}}', data)
|
|
5
|
+
*/
|
|
6
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
7
|
+
|
|
8
|
+
async function handle(data) {
|
|
9
|
+
// TODO: CPU-heavy 로직 구현
|
|
10
|
+
return data;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const result = await handle(workerData);
|
|
14
|
+
parentPort.postMessage(result);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { WsHandler } from '@fuzionx/framework';
|
|
2
|
+
|
|
3
|
+
export default class {{Name}}Handler extends WsHandler {
|
|
4
|
+
static namespace = '/{{nameLower}}';
|
|
5
|
+
static middleware = [];
|
|
6
|
+
|
|
7
|
+
static events(e) {
|
|
8
|
+
// e.on('message', this.handleMessage);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async onConnect(socket) {
|
|
12
|
+
this.logger.info('{{Name}} connected:', socket.sessionId);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async onDisconnect(socket, code, reason) {
|
|
16
|
+
this.logger.info('{{Name}} disconnected:', socket.sessionId);
|
|
17
|
+
}
|
|
18
|
+
}
|
package/lib/core/Application.js
CHANGED
|
@@ -344,17 +344,20 @@ export default class Application {
|
|
|
344
344
|
*/
|
|
345
345
|
async _checkPort(port) {
|
|
346
346
|
const net = await import('node:net');
|
|
347
|
-
return new Promise((resolve
|
|
347
|
+
return new Promise((resolve) => {
|
|
348
348
|
const tester = net.createServer()
|
|
349
349
|
.once('error', (err) => {
|
|
350
350
|
if (err.code === 'EADDRINUSE') {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
` 확인: lsof -i :${port}
|
|
355
|
-
|
|
351
|
+
console.error(
|
|
352
|
+
`\n❌ 포트 ${port}이(가) 이미 사용 중입니다.\n` +
|
|
353
|
+
` 다른 FuzionX 인스턴스가 실행 중인지 확인하세요.\n` +
|
|
354
|
+
` 확인: lsof -i :${port}\n` +
|
|
355
|
+
` 종료: fuser -k ${port}/tcp\n`
|
|
356
|
+
);
|
|
357
|
+
process.exit(1);
|
|
356
358
|
} else {
|
|
357
|
-
|
|
359
|
+
console.error(`❌ 포트 ${port} 확인 실패:`, err.message);
|
|
360
|
+
process.exit(1);
|
|
358
361
|
}
|
|
359
362
|
})
|
|
360
363
|
.once('listening', () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fuzionx/framework",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Full-stack MVC framework built on @fuzionx/core — Controller, Service, Model, Middleware, DI, EventBus",
|
|
6
6
|
"main": "index.js",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"url": "https://github.com/saytohenry/fuzionx"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@fuzionx/core": "^0.1.
|
|
37
|
+
"@fuzionx/core": "^0.1.9",
|
|
38
38
|
"better-sqlite3": "^12.8.0",
|
|
39
39
|
"knex": "^3.2.5",
|
|
40
40
|
"mongoose": "^9.3.2",
|
|
@@ -46,6 +46,8 @@
|
|
|
46
46
|
},
|
|
47
47
|
"files": [
|
|
48
48
|
"index.js",
|
|
49
|
+
"bin/",
|
|
50
|
+
"cli/",
|
|
49
51
|
"lib/",
|
|
50
52
|
"testing/"
|
|
51
53
|
]
|