@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 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');
@@ -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
  }
@@ -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
- const lines = content.split('\n');
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 = rawLine.replace(/#.*$/, '');
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 parent = stack[stack.length - 1].obj;
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
- parent[key] = {};
98
- stack.push({ indent, obj: parent[key] });
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
- parent[key] = Config._parseYamlValue(value);
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
- return raw.slice(1, -1).split(',').map(s => Config._parseYamlValue(s.trim()));
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.41",
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.41",
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",