@xfilecom/xframe 0.1.16 → 0.1.18
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/bin/xframe.js +244 -20
- package/package.json +1 -1
- package/template/shared/config/api/application.yml +3 -3
package/bin/xframe.js
CHANGED
|
@@ -38,10 +38,22 @@ Options:
|
|
|
38
38
|
--front-core <spec> @xfilecom/front-core (default: ^0.2.1)
|
|
39
39
|
--no-install 스캐폴드만 하고 yarn/npm install 생략
|
|
40
40
|
|
|
41
|
+
MySQL (shared/config/api/application.yml 의 database.*)
|
|
42
|
+
--skip-db-prompt TTY여도 DB 질문 생략 → 아래 기본값 또는 플래그만 반영
|
|
43
|
+
--db-host <h> 기본 127.0.0.1
|
|
44
|
+
--db-port <n> 기본 3306
|
|
45
|
+
--db-user <u> 기본 root
|
|
46
|
+
--db-password <p> 기본 빈 문자열 (빈 값: --db-password "")
|
|
47
|
+
--db-name <n> 기본 app
|
|
48
|
+
--db-pull install 성공 후 yarn/npm run db:pull 실행 (MySQL 도달 가능할 때)
|
|
49
|
+
|
|
50
|
+
TTY + --skip-db-prompt 없음 → 위 항목을 순서대로 물어봅니다 (Enter = 기본값).
|
|
51
|
+
마지막에 db:pull 여부를 [y/N] 로 묻습니다 (--db-pull 이 있으면 질문 생략하고 실행).
|
|
52
|
+
|
|
41
53
|
Examples:
|
|
42
54
|
npx @xfilecom/xframe@latest my-app
|
|
43
|
-
npx @xfilecom/xframe my-app --
|
|
44
|
-
|
|
55
|
+
npx @xfilecom/xframe my-app --db-host 127.0.0.1 --db-user app --db-password secret --db-name mydb --db-pull
|
|
56
|
+
npx @xfilecom/xframe my-app --skip-db-prompt --no-install`);
|
|
45
57
|
}
|
|
46
58
|
|
|
47
59
|
function parseArgs(argv) {
|
|
@@ -50,6 +62,20 @@ function parseArgs(argv) {
|
|
|
50
62
|
let backendCore = '^1.0.0';
|
|
51
63
|
let frontCore = '^0.2.1';
|
|
52
64
|
let skipInstall = false;
|
|
65
|
+
let skipDbPrompt = false;
|
|
66
|
+
let dbPull = false;
|
|
67
|
+
/** @type {{ host?: string, port?: number, user?: string, password?: string, name?: string }} */
|
|
68
|
+
const dbFromCli = {};
|
|
69
|
+
|
|
70
|
+
function takeStringFlag(name, i) {
|
|
71
|
+
const v = rest[i + 1];
|
|
72
|
+
if (v === undefined) {
|
|
73
|
+
console.error(`xframe: ${name} requires a value`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
i += 1;
|
|
77
|
+
return { val: v, nextI: i };
|
|
78
|
+
}
|
|
53
79
|
|
|
54
80
|
for (let i = 0; i < rest.length; i += 1) {
|
|
55
81
|
const a = rest[i];
|
|
@@ -61,24 +87,58 @@ function parseArgs(argv) {
|
|
|
61
87
|
skipInstall = true;
|
|
62
88
|
continue;
|
|
63
89
|
}
|
|
90
|
+
if (a === '--skip-db-prompt') {
|
|
91
|
+
skipDbPrompt = true;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (a === '--db-pull') {
|
|
95
|
+
dbPull = true;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
64
98
|
if (a === '--backend-core') {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
process.exit(1);
|
|
69
|
-
}
|
|
70
|
-
backendCore = v;
|
|
71
|
-
i += 1;
|
|
99
|
+
const { val, nextI } = takeStringFlag('--backend-core', i);
|
|
100
|
+
backendCore = val;
|
|
101
|
+
i = nextI;
|
|
72
102
|
continue;
|
|
73
103
|
}
|
|
74
104
|
if (a === '--front-core') {
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
105
|
+
const { val, nextI } = takeStringFlag('--front-core', i);
|
|
106
|
+
frontCore = val;
|
|
107
|
+
i = nextI;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (a === '--db-host') {
|
|
111
|
+
const { val, nextI } = takeStringFlag('--db-host', i);
|
|
112
|
+
dbFromCli.host = val;
|
|
113
|
+
i = nextI;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (a === '--db-port') {
|
|
117
|
+
const { val, nextI } = takeStringFlag('--db-port', i);
|
|
118
|
+
dbFromCli.port = parseInt(val, 10);
|
|
119
|
+
if (!Number.isFinite(dbFromCli.port)) {
|
|
120
|
+
console.error('xframe: --db-port must be a number');
|
|
78
121
|
process.exit(1);
|
|
79
122
|
}
|
|
80
|
-
|
|
81
|
-
|
|
123
|
+
i = nextI;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (a === '--db-user') {
|
|
127
|
+
const { val, nextI } = takeStringFlag('--db-user', i);
|
|
128
|
+
dbFromCli.user = val;
|
|
129
|
+
i = nextI;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (a === '--db-password') {
|
|
133
|
+
const { val, nextI } = takeStringFlag('--db-password', i);
|
|
134
|
+
dbFromCli.password = val;
|
|
135
|
+
i = nextI;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (a === '--db-name') {
|
|
139
|
+
const { val, nextI } = takeStringFlag('--db-name', i);
|
|
140
|
+
dbFromCli.name = val;
|
|
141
|
+
i = nextI;
|
|
82
142
|
continue;
|
|
83
143
|
}
|
|
84
144
|
if (a.startsWith('-')) {
|
|
@@ -93,7 +153,126 @@ function parseArgs(argv) {
|
|
|
93
153
|
projectDir = a;
|
|
94
154
|
}
|
|
95
155
|
|
|
96
|
-
return {
|
|
156
|
+
return {
|
|
157
|
+
projectDir,
|
|
158
|
+
backendCore,
|
|
159
|
+
frontCore,
|
|
160
|
+
skipInstall,
|
|
161
|
+
skipDbPrompt,
|
|
162
|
+
dbPull,
|
|
163
|
+
dbFromCli,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** YAML 단일 인용 내용 이스케이프 */
|
|
168
|
+
function yamlSingleQuotedEscape(s) {
|
|
169
|
+
return String(s).replace(/'/g, "''");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** 식별자형이면 따옴표 없이, 아니면 작은따옴표 */
|
|
173
|
+
function yamlScalar(v) {
|
|
174
|
+
const s = String(v);
|
|
175
|
+
if (/^[A-Za-z0-9_.@-]+$/.test(s)) return s;
|
|
176
|
+
return `'${yamlSingleQuotedEscape(s)}'`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 최상위 `database:` 블록만 수정 (같은 들여쓰기의 `server.port` 등과 구분).
|
|
181
|
+
*
|
|
182
|
+
* @param {string} targetRoot
|
|
183
|
+
* @param {{ host: string, port: number, user: string, password: string, name: string }} db
|
|
184
|
+
*/
|
|
185
|
+
function patchApplicationYmlDatabase(targetRoot, db) {
|
|
186
|
+
const p = path.join(targetRoot, 'shared', 'config', 'api', 'application.yml');
|
|
187
|
+
if (!fs.existsSync(p)) {
|
|
188
|
+
console.warn('xframe: application.yml not found, skip DB patch');
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const portN = Number.isFinite(Number(db.port)) ? Number(db.port) : 3306;
|
|
192
|
+
const pwd = `'${yamlSingleQuotedEscape(db.password)}'`;
|
|
193
|
+
const lines = fs.readFileSync(p, 'utf8').split('\n');
|
|
194
|
+
let i = 0;
|
|
195
|
+
while (i < lines.length) {
|
|
196
|
+
if (lines[i] === 'database:') {
|
|
197
|
+
i += 1;
|
|
198
|
+
while (i < lines.length) {
|
|
199
|
+
const line = lines[i];
|
|
200
|
+
// 루트 database 직계만 (들여쓰기 2칸 + 키). 4칸 이상은 core.database 등 하위 블록.
|
|
201
|
+
if (line === '' || /^ [A-Za-z_#]/.test(line)) {
|
|
202
|
+
if (/^ host:/.test(line)) lines[i] = ` host: ${yamlScalar(db.host)}`;
|
|
203
|
+
else if (/^ port:/.test(line)) lines[i] = ` port: ${portN}`;
|
|
204
|
+
else if (/^ user:/.test(line)) lines[i] = ` user: ${yamlScalar(db.user)}`;
|
|
205
|
+
else if (/^ password:/.test(line)) lines[i] = ` password: ${pwd}`;
|
|
206
|
+
else if (/^ name:/.test(line)) lines[i] = ` name: ${yamlScalar(db.name)}`;
|
|
207
|
+
i += 1;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
i += 1;
|
|
215
|
+
}
|
|
216
|
+
fs.writeFileSync(p, lines.join('\n'), 'utf8');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const DB_DEFAULTS = {
|
|
220
|
+
host: '127.0.0.1',
|
|
221
|
+
port: 3306,
|
|
222
|
+
user: 'root',
|
|
223
|
+
password: '',
|
|
224
|
+
name: 'app',
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* @param {{ host?: string, port?: number, user?: string, password?: string, name?: string }} fromCli
|
|
229
|
+
* @param {boolean} skipDbPrompt
|
|
230
|
+
* @param {boolean} wantDbPullFlag
|
|
231
|
+
* @returns {Promise<{ db: typeof DB_DEFAULTS, runDbPull: boolean }>}
|
|
232
|
+
*/
|
|
233
|
+
async function collectDatabaseConfig(fromCli, skipDbPrompt, wantDbPullFlag) {
|
|
234
|
+
let db = { ...DB_DEFAULTS, ...fromCli };
|
|
235
|
+
const interactive = process.stdin.isTTY && process.stdout.isTTY && !skipDbPrompt;
|
|
236
|
+
const readline = require('node:readline/promises');
|
|
237
|
+
const rl = interactive ? readline.createInterface({ input: process.stdin, output: process.stdout }) : null;
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
if (interactive && rl) {
|
|
241
|
+
console.log('\nxframe: MySQL 연결 정보 (Enter = 괄호 안 기본값)\n');
|
|
242
|
+
const ask = async (label, key, def) => {
|
|
243
|
+
if (fromCli[key] !== undefined) return;
|
|
244
|
+
const hint = def === '' ? '(비워두기)' : def;
|
|
245
|
+
const line = (await rl.question(`${label} [${hint}]: `)).trim();
|
|
246
|
+
if (line !== '') {
|
|
247
|
+
if (key === 'port') {
|
|
248
|
+
const n = parseInt(line, 10);
|
|
249
|
+
if (Number.isFinite(n)) db[key] = n;
|
|
250
|
+
} else {
|
|
251
|
+
db[key] = line;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
await ask('Host', 'host', DB_DEFAULTS.host);
|
|
256
|
+
await ask('Port', 'port', String(DB_DEFAULTS.port));
|
|
257
|
+
await ask('User', 'user', DB_DEFAULTS.user);
|
|
258
|
+
await ask('Password', 'password', DB_DEFAULTS.password);
|
|
259
|
+
await ask('Database name', 'name', DB_DEFAULTS.name);
|
|
260
|
+
} else {
|
|
261
|
+
db = { ...DB_DEFAULTS, ...fromCli };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
let runDbPull = wantDbPullFlag;
|
|
265
|
+
if (interactive && rl && !wantDbPullFlag) {
|
|
266
|
+
const a = (await rl.question('\n지금 yarn/npm db:pull (스키마 역추출) 을 실행할까요? [y/N] '))
|
|
267
|
+
.trim()
|
|
268
|
+
.toLowerCase();
|
|
269
|
+
runDbPull = a === 'y' || a === 'yes';
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return { db, runDbPull };
|
|
273
|
+
} finally {
|
|
274
|
+
if (rl) await rl.close();
|
|
275
|
+
}
|
|
97
276
|
}
|
|
98
277
|
|
|
99
278
|
/** yarn 우선, 없거나 실패 시 npm (부모 셸 PATH 그대로 — shell:true 쓰면 sh라 yarn/nvm PATH가 빠지는 경우가 많음) */
|
|
@@ -137,6 +316,27 @@ function installDependencies(cwd) {
|
|
|
137
316
|
return false;
|
|
138
317
|
}
|
|
139
318
|
|
|
319
|
+
function executeDbPull(cwd) {
|
|
320
|
+
console.log('\nxframe: db:pull 실행 (drizzle-kit introspect)…');
|
|
321
|
+
const opts = { cwd, stdio: 'inherit', env: process.env };
|
|
322
|
+
const y = spawnSync('yarn', ['run', 'db:pull'], opts);
|
|
323
|
+
if (y.status === 0) {
|
|
324
|
+
console.log('xframe: db:pull 완료.');
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
if (y.error && y.error.code === 'ENOENT') {
|
|
328
|
+
console.log('xframe: yarn 없음 → npm run db:pull 시도');
|
|
329
|
+
}
|
|
330
|
+
const n = spawnSync('npm', ['run', 'db:pull'], opts);
|
|
331
|
+
if (n.status === 0) {
|
|
332
|
+
console.log('xframe: db:pull 완료.');
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
console.error('xframe: db:pull 실패. 프로젝트 루트에서 수동으로 yarn db:pull / npm run db:pull 하세요.');
|
|
336
|
+
console.error('xframe: (비밀번호가 비어 있으면 drizzle-kit 이 거절할 수 있습니다. application.yml database.password 를 확인하세요.)');
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
|
|
140
340
|
/** 모노레포 템플릿 tsconfig 의 @xfilecom/front-core 경로 → 생성 앱의 node_modules 기준 */
|
|
141
341
|
const TEMPLATE_FRONT_CORE_TS_PATH = '../../../../front-core/src/index.ts';
|
|
142
342
|
const SCAFFOLD_FRONT_CORE_TS_PATH = '../../node_modules/@xfilecom/front-core';
|
|
@@ -207,8 +407,16 @@ function readCliVersion() {
|
|
|
207
407
|
}
|
|
208
408
|
}
|
|
209
409
|
|
|
210
|
-
function main() {
|
|
211
|
-
const {
|
|
410
|
+
async function main() {
|
|
411
|
+
const {
|
|
412
|
+
projectDir,
|
|
413
|
+
backendCore,
|
|
414
|
+
frontCore,
|
|
415
|
+
skipInstall,
|
|
416
|
+
skipDbPrompt,
|
|
417
|
+
dbPull: dbPullFlag,
|
|
418
|
+
dbFromCli,
|
|
419
|
+
} = parseArgs(process.argv);
|
|
212
420
|
|
|
213
421
|
if (!projectDir) {
|
|
214
422
|
console.error('xframe: missing <project-directory>\n');
|
|
@@ -248,6 +456,10 @@ function main() {
|
|
|
248
456
|
|
|
249
457
|
fs.cpSync(templateRoot, targetRoot, { recursive: true });
|
|
250
458
|
|
|
459
|
+
const { db, runDbPull: shouldDbPull } = await collectDatabaseConfig(dbFromCli, skipDbPrompt, dbPullFlag);
|
|
460
|
+
patchApplicationYmlDatabase(targetRoot, db);
|
|
461
|
+
console.log(`xframe: wrote database.* → ${path.join('shared', 'config', 'api', 'application.yml')}`);
|
|
462
|
+
|
|
251
463
|
const vars = {
|
|
252
464
|
PACKAGE_NAME: packageName,
|
|
253
465
|
SERVICE_LABEL: `${packageName}-api`,
|
|
@@ -276,13 +488,22 @@ Included:
|
|
|
276
488
|
• shared/ — config/api·web, schema, sql, endpoint
|
|
277
489
|
• .cursor/rules — @xfilecom/backend-core / front-core 사용 가이드 (.mdc)`);
|
|
278
490
|
|
|
491
|
+
let installOk = true;
|
|
279
492
|
if (!skipInstall) {
|
|
280
|
-
|
|
281
|
-
if (!
|
|
493
|
+
installOk = installDependencies(targetRoot);
|
|
494
|
+
if (!installOk) {
|
|
282
495
|
process.exit(1);
|
|
283
496
|
}
|
|
284
497
|
}
|
|
285
498
|
|
|
499
|
+
if (shouldDbPull) {
|
|
500
|
+
if (skipInstall) {
|
|
501
|
+
console.warn('xframe: --no-install 이라 db:pull 을 건너뜁니다 (먼저 install 후 수동 실행).');
|
|
502
|
+
} else {
|
|
503
|
+
executeDbPull(targetRoot);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
286
507
|
const rel = path.relative(process.cwd(), targetRoot) || '.';
|
|
287
508
|
console.log(`
|
|
288
509
|
Next:
|
|
@@ -292,4 +513,7 @@ Next:
|
|
|
292
513
|
(설치 생략: npx @xfilecom/xframe <dir> --no-install)`);
|
|
293
514
|
}
|
|
294
515
|
|
|
295
|
-
main()
|
|
516
|
+
main().catch((err) => {
|
|
517
|
+
console.error(err);
|
|
518
|
+
process.exit(1);
|
|
519
|
+
});
|
package/package.json
CHANGED