@fuzionx/framework 0.1.41 → 0.1.43
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 +2 -1
- package/cli/index.js +3 -2
- package/lib/core/AutoLoader.js +9 -8
- package/lib/core/Config.js +112 -9
- package/package.json +2 -2
package/cli/db-sync.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { promises as fs } from 'node:fs';
|
|
13
13
|
import path from 'node:path';
|
|
14
|
+
import { pathToFileURL } from 'node:url';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* 모델 클래스에서 CREATE TABLE SQL 생성
|
|
@@ -59,7 +60,7 @@ export async function generateSchema(modelsDir) {
|
|
|
59
60
|
.map(e => path.join(modelsDir, e.name));
|
|
60
61
|
|
|
61
62
|
for (const file of files) {
|
|
62
|
-
const mod = await import(file);
|
|
63
|
+
const mod = await import(pathToFileURL(file).href);
|
|
63
64
|
const ModelClass = mod.default;
|
|
64
65
|
if (ModelClass?.table) {
|
|
65
66
|
sqls.push(generateCreateTable(ModelClass));
|
package/cli/index.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { promises as fs } from 'node:fs';
|
|
21
21
|
import path from 'node:path';
|
|
22
22
|
import { fileURLToPath } from 'node:url';
|
|
23
|
+
import { pathToFileURL } from 'node:url';
|
|
23
24
|
|
|
24
25
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
25
26
|
const TPL_DIR = path.join(__dirname, 'templates');
|
|
@@ -335,7 +336,7 @@ export async function run(args) {
|
|
|
335
336
|
// ── fx routes — 라우트 테이블 출력 ──
|
|
336
337
|
if (command === 'routes') {
|
|
337
338
|
try {
|
|
338
|
-
const appMod = await import(path.resolve('app.js'));
|
|
339
|
+
const appMod = await import(pathToFileURL(path.resolve('app.js')).href);
|
|
339
340
|
const app = appMod.default || appMod.app;
|
|
340
341
|
if (!app?._appRegistry) { console.error('Cannot load app routes'); return; }
|
|
341
342
|
console.log('');
|
|
@@ -390,7 +391,7 @@ export async function run(args) {
|
|
|
390
391
|
const models = [];
|
|
391
392
|
|
|
392
393
|
for (const file of jsFiles) {
|
|
393
|
-
const mod = await import(path.join(modelsDir, file));
|
|
394
|
+
const mod = await import(pathToFileURL(path.join(modelsDir, file)).href);
|
|
394
395
|
const Model = mod.default;
|
|
395
396
|
if (!Model) continue;
|
|
396
397
|
const name = path.basename(file, '.js');
|
package/lib/core/AutoLoader.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { promises as fs } from 'node:fs';
|
|
12
12
|
import path from 'node:path';
|
|
13
|
+
import { pathToFileURL } from 'node:url';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* 디렉토리에서 .js 파일 목록 반환
|
|
@@ -74,7 +75,7 @@ export default class AutoLoader {
|
|
|
74
75
|
async loadModels(subDir = 'models') {
|
|
75
76
|
const files = await scanDir(path.join(this.baseDir, subDir));
|
|
76
77
|
for (const file of files) {
|
|
77
|
-
const mod = await import(file);
|
|
78
|
+
const mod = await import(pathToFileURL(file).href);
|
|
78
79
|
const ModelClass = mod.default;
|
|
79
80
|
if (!ModelClass) continue;
|
|
80
81
|
const name = extractName(file);
|
|
@@ -98,7 +99,7 @@ export default class AutoLoader {
|
|
|
98
99
|
if (!registry) return;
|
|
99
100
|
|
|
100
101
|
for (const file of files) {
|
|
101
|
-
const mod = await import(file);
|
|
102
|
+
const mod = await import(pathToFileURL(file).href);
|
|
102
103
|
const ControllerClass = mod.default;
|
|
103
104
|
if (!ControllerClass) continue;
|
|
104
105
|
|
|
@@ -138,7 +139,7 @@ export default class AutoLoader {
|
|
|
138
139
|
async loadServices() {
|
|
139
140
|
const files = await scanDir(path.join(this.baseDir, 'services'));
|
|
140
141
|
for (const file of files) {
|
|
141
|
-
const mod = await import(file);
|
|
142
|
+
const mod = await import(pathToFileURL(file).href);
|
|
142
143
|
const ServiceClass = mod.default;
|
|
143
144
|
if (!ServiceClass) continue;
|
|
144
145
|
const name = extractName(file, 'Service');
|
|
@@ -153,7 +154,7 @@ export default class AutoLoader {
|
|
|
153
154
|
|
|
154
155
|
const files = await scanDir(path.join(this.baseDir, 'middleware'));
|
|
155
156
|
for (const file of files) {
|
|
156
|
-
const mod = await import(file);
|
|
157
|
+
const mod = await import(pathToFileURL(file).href);
|
|
157
158
|
const MwClass = mod.default;
|
|
158
159
|
if (!MwClass) continue;
|
|
159
160
|
const mwName = MwClass.alias || extractName(file, 'Middleware').toLowerCase();
|
|
@@ -165,7 +166,7 @@ export default class AutoLoader {
|
|
|
165
166
|
async loadEvents(subDir = 'events') {
|
|
166
167
|
const files = await scanDir(path.join(this.baseDir, subDir));
|
|
167
168
|
for (const file of files) {
|
|
168
|
-
const mod = await import(file);
|
|
169
|
+
const mod = await import(pathToFileURL(file).href);
|
|
169
170
|
// events/*.js 는 export default (app) => { app.on('...', handler) }
|
|
170
171
|
if (typeof mod.default === 'function') {
|
|
171
172
|
mod.default(this.app);
|
|
@@ -177,7 +178,7 @@ export default class AutoLoader {
|
|
|
177
178
|
async loadJobs(subDir = 'jobs') {
|
|
178
179
|
const files = await scanDir(path.join(this.baseDir, subDir));
|
|
179
180
|
for (const file of files) {
|
|
180
|
-
const mod = await import(file);
|
|
181
|
+
const mod = await import(pathToFileURL(file).href);
|
|
181
182
|
const JobClass = mod.default;
|
|
182
183
|
if (!JobClass) continue;
|
|
183
184
|
|
|
@@ -200,7 +201,7 @@ export default class AutoLoader {
|
|
|
200
201
|
|
|
201
202
|
const files = await scanDir(path.join(this.baseDir, 'ws'));
|
|
202
203
|
for (const file of files) {
|
|
203
|
-
const mod = await import(file);
|
|
204
|
+
const mod = await import(pathToFileURL(file).href);
|
|
204
205
|
const HandlerClass = mod.default;
|
|
205
206
|
if (!HandlerClass) continue;
|
|
206
207
|
const ns = HandlerClass.namespace || '/';
|
|
@@ -215,7 +216,7 @@ export default class AutoLoader {
|
|
|
215
216
|
|
|
216
217
|
const files = await scanDir(path.join(this.baseDir, 'routes'));
|
|
217
218
|
for (const file of files) {
|
|
218
|
-
const mod = await import(file);
|
|
219
|
+
const mod = await import(pathToFileURL(file).href);
|
|
219
220
|
if (typeof mod.default === 'function') {
|
|
220
221
|
router.load(mod.default);
|
|
221
222
|
}
|
package/lib/core/Config.js
CHANGED
|
@@ -62,21 +62,94 @@ export default class Config {
|
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
64
|
* 간이 YAML 파서 — 외부 의존 없이 fuzionx.yaml 구조 파싱.
|
|
65
|
-
* 지원: 중첩 객체, 문자열, 숫자, boolean, 배열(
|
|
65
|
+
* 지원: 중첩 객체, 문자열, 숫자, boolean, 배열(인라인/블록), 인용 문자열.
|
|
66
|
+
* Windows(\r\n) / Unix(\n) 줄바꿈 모두 지원.
|
|
66
67
|
* @param {string} content
|
|
67
68
|
* @returns {object}
|
|
68
69
|
*/
|
|
69
70
|
static parseYaml(content) {
|
|
70
71
|
const result = {};
|
|
71
|
-
|
|
72
|
+
// Windows \r\n → \n 정규화 (정규식 $, . 호환)
|
|
73
|
+
const lines = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n');
|
|
74
|
+
// stack: { indent, obj, lastKey? }
|
|
72
75
|
const stack = [{ indent: -1, obj: result }];
|
|
73
76
|
|
|
74
77
|
for (const rawLine of lines) {
|
|
75
|
-
//
|
|
76
|
-
const line =
|
|
78
|
+
// 인용부호 바깥의 # 주석만 제거 (문자열 안의 # 보존)
|
|
79
|
+
const line = Config._stripInlineComment(rawLine);
|
|
77
80
|
if (!line.trim()) continue;
|
|
78
81
|
|
|
79
82
|
const indent = line.search(/\S/);
|
|
83
|
+
|
|
84
|
+
// ── YAML 블록 배열 항목: "- value" 또는 "- key: value" ──
|
|
85
|
+
const listMatch = line.match(/^(\s*)-\s+(.+)$/);
|
|
86
|
+
if (listMatch) {
|
|
87
|
+
const listIndent = listMatch[1].length;
|
|
88
|
+
|
|
89
|
+
// 스택에서 현재 indent보다 깊은 레벨 제거 (배열은 유지)
|
|
90
|
+
while (stack.length > 1) {
|
|
91
|
+
const top = stack[stack.length - 1];
|
|
92
|
+
if (Array.isArray(top.obj) && top.indent === listIndent) break;
|
|
93
|
+
if (top.indent >= listIndent) {
|
|
94
|
+
stack.pop();
|
|
95
|
+
} else {
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 부모 찾기 — 배열이면 직접 사용, 아니면 lastKey를 배열로 변환
|
|
101
|
+
const top = stack[stack.length - 1];
|
|
102
|
+
let arr;
|
|
103
|
+
if (Array.isArray(top.obj)) {
|
|
104
|
+
arr = top.obj;
|
|
105
|
+
} else {
|
|
106
|
+
// lastKey 탐색: 현재 top → 상위 스택 순서로 찾기
|
|
107
|
+
// (빈 값 키 origins: → {} 가 스택에 있으면 lastKey=undefined,
|
|
108
|
+
// 이 경우 부모의 lastKey가 "origins")
|
|
109
|
+
let foundIdx = -1;
|
|
110
|
+
let foundKey = null;
|
|
111
|
+
let foundObj = null;
|
|
112
|
+
for (let si = stack.length - 1; si >= 0; si--) {
|
|
113
|
+
if (stack[si].lastKey !== undefined) {
|
|
114
|
+
foundIdx = si;
|
|
115
|
+
foundKey = stack[si].lastKey;
|
|
116
|
+
foundObj = stack[si].obj;
|
|
117
|
+
// 배열이 아닌 빈 객체가 이 키 아래 있으면 그게 배열로 바뀌어야 함
|
|
118
|
+
const target = Array.isArray(foundObj)
|
|
119
|
+
? foundObj[foundObj.length - 1]
|
|
120
|
+
: foundObj;
|
|
121
|
+
if (target && typeof target[foundKey] !== undefined) {
|
|
122
|
+
foundObj = target;
|
|
123
|
+
}
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (foundKey === null) continue;
|
|
128
|
+
if (!Array.isArray(foundObj[foundKey])) {
|
|
129
|
+
foundObj[foundKey] = [];
|
|
130
|
+
}
|
|
131
|
+
arr = foundObj[foundKey];
|
|
132
|
+
// 기존 빈 객체 스택 제거 후 배열로 교체
|
|
133
|
+
while (stack.length > foundIdx + 1) stack.pop();
|
|
134
|
+
stack.push({ indent: listIndent, obj: arr });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const itemContent = listMatch[2].trim();
|
|
138
|
+
// "- key: value" 형태인지 확인
|
|
139
|
+
const kvMatch = itemContent.match(/^(?:(["'])([^"']+)\1|([-\w.]+))\s*:\s*(.+)$/);
|
|
140
|
+
if (kvMatch) {
|
|
141
|
+
const k = kvMatch[2] || kvMatch[3];
|
|
142
|
+
const v = Config._parseYamlValue(kvMatch[4].trim());
|
|
143
|
+
const obj = { [k]: v };
|
|
144
|
+
arr.push(obj);
|
|
145
|
+
stack.push({ indent: listIndent + 2, obj, lastKey: k });
|
|
146
|
+
} else {
|
|
147
|
+
arr.push(Config._parseYamlValue(itemContent));
|
|
148
|
+
}
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── 키: 값 파싱 ──
|
|
80
153
|
// 키: unquoted ([-\w.]+) 또는 quoted ("..." / '...')
|
|
81
154
|
const match = line.match(/^(\s*)(?:(["'])([^"']+)\2|([-\w.]+))\s*:\s*(.*)$/);
|
|
82
155
|
if (!match) continue;
|
|
@@ -89,22 +162,50 @@ export default class Config {
|
|
|
89
162
|
stack.pop();
|
|
90
163
|
}
|
|
91
164
|
|
|
92
|
-
const
|
|
165
|
+
const top = stack[stack.length - 1];
|
|
166
|
+
// 배열 안의 마지막 객체에 키 추가 (- key: value 후속 키)
|
|
167
|
+
const target = Array.isArray(top.obj) ? top.obj[top.obj.length - 1] : top.obj;
|
|
93
168
|
const value = rawValue.trim();
|
|
94
169
|
|
|
95
170
|
if (!value) {
|
|
96
171
|
// 하위 객체 시작
|
|
97
|
-
|
|
98
|
-
stack.push({ indent, obj:
|
|
172
|
+
target[key] = {};
|
|
173
|
+
stack.push({ indent, obj: target[key], lastKey: undefined });
|
|
174
|
+
// lastKey를 부모에 설정 (배열 전환 시 필요)
|
|
175
|
+
const parentIdx = stack.length - 2;
|
|
176
|
+
if (parentIdx >= 0) {
|
|
177
|
+
stack[parentIdx].lastKey = key;
|
|
178
|
+
}
|
|
99
179
|
} else {
|
|
100
180
|
// 값 파싱
|
|
101
|
-
|
|
181
|
+
target[key] = Config._parseYamlValue(value);
|
|
182
|
+
top.lastKey = key;
|
|
102
183
|
}
|
|
103
184
|
}
|
|
104
185
|
|
|
105
186
|
return result;
|
|
106
187
|
}
|
|
107
188
|
|
|
189
|
+
/**
|
|
190
|
+
* 인라인 주석 제거 — 인용부호 안의 # 보존
|
|
191
|
+
* @param {string} line
|
|
192
|
+
* @returns {string}
|
|
193
|
+
* @private
|
|
194
|
+
*/
|
|
195
|
+
static _stripInlineComment(line) {
|
|
196
|
+
let inSingle = false;
|
|
197
|
+
let inDouble = false;
|
|
198
|
+
for (let i = 0; i < line.length; i++) {
|
|
199
|
+
const ch = line[i];
|
|
200
|
+
if (ch === "'" && !inDouble) inSingle = !inSingle;
|
|
201
|
+
else if (ch === '"' && !inSingle) inDouble = !inDouble;
|
|
202
|
+
else if (ch === '#' && !inSingle && !inDouble) {
|
|
203
|
+
return line.slice(0, i);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return line;
|
|
207
|
+
}
|
|
208
|
+
|
|
108
209
|
/**
|
|
109
210
|
* YAML 값 파싱 (문자열, 숫자, boolean, 배열)
|
|
110
211
|
* @private
|
|
@@ -121,7 +222,9 @@ export default class Config {
|
|
|
121
222
|
|
|
122
223
|
// 인라인 배열 [a, b, c]
|
|
123
224
|
if (raw.startsWith('[') && raw.endsWith(']')) {
|
|
124
|
-
|
|
225
|
+
const inner = raw.slice(1, -1).trim();
|
|
226
|
+
if (!inner) return [];
|
|
227
|
+
return inner.split(',').map(s => Config._parseYamlValue(s.trim()));
|
|
125
228
|
}
|
|
126
229
|
|
|
127
230
|
// 숫자
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fuzionx/framework",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.43",
|
|
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.43",
|
|
38
38
|
"better-sqlite3": "^12.8.0",
|
|
39
39
|
"knex": "^3.2.5",
|
|
40
40
|
"mongoose": "^9.3.2",
|