@gaonjs/cli 0.32.0 → 0.34.0
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/dist/commands/build.d.ts +11 -0
- package/dist/commands/build.js +49 -0
- package/dist/commands/check.d.ts +10 -1
- package/dist/commands/check.js +51 -6
- package/dist/commands/dev.js +2 -0
- package/dist/commands/gen.js +4 -2
- package/dist/db/projectData.d.ts +14 -0
- package/dist/db/projectData.js +51 -0
- package/dist/db.js +38 -2
- package/dist/dev/build.d.ts +34 -0
- package/dist/dev/build.js +144 -0
- package/dist/dev/frontend-build.d.ts +3 -3
- package/dist/dev/frontend-build.js +42 -37
- package/dist/dev.d.ts +11 -1
- package/dist/dev.js +29 -1
- package/dist/doctor/async-offload.d.ts +3 -1
- package/dist/doctor/async-offload.js +5 -2
- package/dist/doctor/auth-wiring.d.ts +4 -2
- package/dist/doctor/auth-wiring.js +13 -6
- package/dist/doctor/csrf-wiring.d.ts +3 -1
- package/dist/doctor/csrf-wiring.js +9 -5
- package/dist/doctor/seal-security.js +12 -6
- package/dist/doctor/source-scan.d.ts +11 -0
- package/dist/doctor/source-scan.js +70 -0
- package/dist/generate.d.ts +12 -3
- package/dist/generate.js +113 -42
- package/dist/index.d.ts +3 -0
- package/dist/index.js +25 -5
- package/dist/messages-gen.d.ts +6 -0
- package/dist/messages-gen.js +24 -0
- package/dist/scaffold/app.js +2 -1
- package/dist/templates/auth/Dashboard.vue.tpl +2 -2
- package/dist/templates/auth/Login.vue.tpl +2 -5
- package/dist/templates/auth/Signup.vue.tpl +2 -2
- package/dist/templates/auth/app.config.ts.tpl +3 -3
- package/dist/templates/auth/dashboard.controller.ts.tpl +1 -1
- package/dist/templates/auth/dashboard.secure.controller.ts.tpl +14 -0
- package/dist/templates/auth/registration.controller.ts.tpl +3 -3
- package/dist/templates/auth/session.controller.ts.tpl +5 -5
- package/dist/templates/project/.env.example.tpl +6 -0
- package/dist/templates/project/AGENTS.md.tpl +3 -1
- package/dist/templates/project/CLAUDE.md.tpl +1 -1
- package/dist/templates/project/agents/async.md.tpl +46 -5
- package/dist/templates/project/agents/data.md.tpl +47 -2
- package/dist/templates/project/agents/frontend.md.tpl +3 -1
- package/dist/templates/project/agents/i18n.md.tpl +107 -0
- package/dist/templates/project/agents/mail.md.tpl +92 -0
- package/dist/templates/project/agents/realtime.md.tpl +8 -0
- package/dist/templates/project/agents/security.md.tpl +45 -2
- package/dist/templates/project/agents/testing.md.tpl +11 -0
- package/dist/templates/project/agents/web.md.tpl +32 -0
- package/dist/templates/project/gaon.config.ts.tpl +14 -0
- package/dist/templates/project/package.json.tpl +1 -1
- package/dist/templates/project/vite.config.ts.tpl +6 -5
- package/package.json +7 -6
- package/dist/templates/auth/app.ts.tpl +0 -29
- package/dist/templates/auth/server.ts.tpl +0 -14
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface BuildCommandOptions {
|
|
2
|
+
readonly cwd?: string;
|
|
3
|
+
readonly json?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export interface BuildCommandResult {
|
|
6
|
+
readonly ok: boolean;
|
|
7
|
+
readonly apps: string[];
|
|
8
|
+
readonly error?: string;
|
|
9
|
+
}
|
|
10
|
+
/** `gaon build` 진입점. .gaon 재생성 + 전 앱 vite build. */
|
|
11
|
+
export declare function runBuildCommand(opts?: BuildCommandOptions): Promise<number>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// @gaonjs/cli · `gaon build` — 멀티 앱 프론트 프로덕션 빌드 (결정 146 · 12차 W2)
|
|
2
|
+
//
|
|
3
|
+
// `gaon gen`(.gaon 재생성) 뒤 apps/* 를 순회해 앱마다 root=apps/<앱>·outDir=dist/<앱>·
|
|
4
|
+
// base=/<앱>/ 로 vite build 한다. 예전 스캐폴드 build 스크립트(`gaon gen && vite build`)는
|
|
5
|
+
// apps/web 하나만 빌드해 둘째 앱(admin)의 dist 가 안 생기거나 base 없이 404 였다 —
|
|
6
|
+
// gaon g app 으로 늘린 앱이 별도 손배선 없이 빌드 파이프라인에 자동 편입되게 한다.
|
|
7
|
+
import { regenerateProjectGaon } from './gen.js';
|
|
8
|
+
import { buildAllApps, listFrontendApps } from '../dev/build.js';
|
|
9
|
+
/** `gaon build` 진입점. .gaon 재생성 + 전 앱 vite build. */
|
|
10
|
+
export async function runBuildCommand(opts = {}) {
|
|
11
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
12
|
+
const json = opts.json ?? false;
|
|
13
|
+
const log = (line) => {
|
|
14
|
+
if (!json)
|
|
15
|
+
process.stdout.write(line + '\n');
|
|
16
|
+
};
|
|
17
|
+
try {
|
|
18
|
+
// 1) .gaon 재생성 — build 산출이 routes.manifest 값 import 를 타므로 fresh 여야 한다(규칙 3).
|
|
19
|
+
await regenerateProjectGaon(cwd);
|
|
20
|
+
// 2) 프론트 앱이 없으면(API 전용) 조용히 통과.
|
|
21
|
+
const apps = await listFrontendApps(cwd);
|
|
22
|
+
if (apps.length === 0) {
|
|
23
|
+
const result = { ok: true, apps: [] };
|
|
24
|
+
if (json)
|
|
25
|
+
process.stdout.write(JSON.stringify({ command: 'build', ...result }) + '\n');
|
|
26
|
+
else
|
|
27
|
+
log('빌드할 프론트 앱(apps/<앱>/index.html)이 없습니다 — 건너뜁니다.');
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
// 3) 앱별 빌드.
|
|
31
|
+
log(`gaon build — ${apps.length}개 앱: ${apps.join(', ')}`);
|
|
32
|
+
await buildAllApps(cwd, { onLog: json ? undefined : (l) => process.stdout.write(l + '\n') });
|
|
33
|
+
const result = { ok: true, apps };
|
|
34
|
+
if (json)
|
|
35
|
+
process.stdout.write(JSON.stringify({ command: 'build', ...result }) + '\n');
|
|
36
|
+
else
|
|
37
|
+
log(`✓ 빌드 완료 — ${apps.map((a) => `dist/${a}`).join(' · ')}`);
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
42
|
+
const result = { ok: false, apps: [], error: msg };
|
|
43
|
+
if (json)
|
|
44
|
+
process.stdout.write(JSON.stringify({ command: 'build', ...result }) + '\n');
|
|
45
|
+
else
|
|
46
|
+
process.stderr.write(`✗ gaon build 실패: ${msg}\n`);
|
|
47
|
+
return 1;
|
|
48
|
+
}
|
|
49
|
+
}
|
package/dist/commands/check.d.ts
CHANGED
|
@@ -4,7 +4,16 @@ export interface CheckCommandOptions {
|
|
|
4
4
|
readonly json?: boolean;
|
|
5
5
|
/** 지정 시 그 검사 하나만 실행. 나머지는 스킵. */
|
|
6
6
|
readonly only?: CheckStep;
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* 결정 157: doctor 는 이제 **기본 포함**이다(CI 가 check 하나만 돌려도 규칙 5 등
|
|
9
|
+
* doctor 규칙 위반을 잡는다). `noDoctor:true`(CLI `--no-doctor`)로 순수 typecheck/
|
|
10
|
+
* vue-tsc/build 만 돌린다.
|
|
11
|
+
*/
|
|
12
|
+
readonly noDoctor?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* @deprecated 결정 157: doctor 가 기본이 돼 의미 없음(무시). `--include-doctor` 는
|
|
15
|
+
* 하위 호환 no-op — doctor 를 빼려면 `noDoctor`/`--no-doctor` 를 쓴다.
|
|
16
|
+
*/
|
|
8
17
|
readonly includeDoctor?: boolean;
|
|
9
18
|
}
|
|
10
19
|
export type CheckStepStatus = 'passed' | 'failed' | 'skipped';
|
package/dist/commands/check.js
CHANGED
|
@@ -7,16 +7,16 @@
|
|
|
7
7
|
* (`node_modules/.bin/tsc --noEmit` · `node_modules/.bin/vue-tsc --noEmit`
|
|
8
8
|
* · `pnpm -r build` → `npm run build`).
|
|
9
9
|
*
|
|
10
|
-
* The One Way — 하나의 명령이
|
|
10
|
+
* The One Way — 하나의 명령이 4 검사를 순서대로 돌린다(결정 157 · doctor 기본 포함):
|
|
11
11
|
* 1) typecheck (pnpm typecheck 또는 tsc --noEmit)
|
|
12
12
|
* 2) vue-tsc (pnpm vue-tsc 또는 vue-tsc --noEmit)
|
|
13
13
|
* 3) build (pnpm build)
|
|
14
|
-
*
|
|
14
|
+
* 4) doctor (기본 포함 · 규칙 5 등 doctor 규칙 · --no-doctor 로 뺌 · 코어 재사용)
|
|
15
15
|
*
|
|
16
16
|
* 옵션 최소:
|
|
17
17
|
* --json 자동화 · 각 검사의 passed/failed/skipped
|
|
18
18
|
* --only <name> 단일 검사만
|
|
19
|
-
* --
|
|
19
|
+
* --no-doctor doctor 를 빼고 typecheck/vue-tsc/build 만 (결정 157)
|
|
20
20
|
*
|
|
21
21
|
* exit code: 검사가 하나라도 실패하면 1, 그 외 0. 미리 검사할 대상이
|
|
22
22
|
* 하나도 없으면(예: 스크립트도 없고 바이너리도 없음) 스킵으로 리포트 —
|
|
@@ -33,6 +33,8 @@ import { generateRoutesDts } from '@gaonjs/web';
|
|
|
33
33
|
import { runDoctorCommand } from '../doctor.js';
|
|
34
34
|
import { regenerateGaonOnce, resolveDevLayout } from '../dev.js';
|
|
35
35
|
import { registerTsResolve } from '../tsResolve.js';
|
|
36
|
+
import { listFrontendApps, verifyAppDist } from '../dev/build.js';
|
|
37
|
+
import { generateMessagesDts } from '../messages-gen.js';
|
|
36
38
|
/**
|
|
37
39
|
* 프로젝트 스크립트 존재 여부. pnpm/npm 어느 쪽이든 `scripts.<name>` 을
|
|
38
40
|
* 정의해 두면 우선 사용한다.
|
|
@@ -78,6 +80,17 @@ async function runStep(step, cwd) {
|
|
|
78
80
|
const cmd = 'pnpm';
|
|
79
81
|
const args = ['run', scriptName];
|
|
80
82
|
const { exitCode, output } = await runSubprocess(cwd, cmd, args);
|
|
83
|
+
// 결정 146(12차 W2): build 가 성공했으면 **등록된 앱마다** dist/<앱>/index.html 과
|
|
84
|
+
// 그 문서가 참조하는 에셋이 실제로 존재하는지 검증한다. build 스크립트가 통과해도
|
|
85
|
+
// 둘째 앱(admin)의 dist 가 안 생기거나 에셋이 404 면 여기서 잡는다("build passed 인데
|
|
86
|
+
// /admin 은 빈 화면" 사각 제거 · curl data-page 만으로 못 보던 지점).
|
|
87
|
+
if (step === 'build' && exitCode === 0) {
|
|
88
|
+
const verify = await verifyBuildOutput(cwd);
|
|
89
|
+
if (!verify.ok) {
|
|
90
|
+
return { step, status: 'failed', command: `${cmd} ${args.join(' ')}`, exitCode, output: output + '\n' + verify.message };
|
|
91
|
+
}
|
|
92
|
+
return { step, status: 'passed', command: `${cmd} ${args.join(' ')}`, exitCode, output: output + '\n' + verify.message };
|
|
93
|
+
}
|
|
81
94
|
return {
|
|
82
95
|
step,
|
|
83
96
|
status: exitCode === 0 ? 'passed' : 'failed',
|
|
@@ -159,6 +172,35 @@ async function runStep(step, cwd) {
|
|
|
159
172
|
'→ 프론트 프로덕션 빌드를 검사에 포함하려면 package.json 에 "build": "vite build" 를 정의하세요.',
|
|
160
173
|
};
|
|
161
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* build 산출 검증(결정 146 · 12차 W2). 등록된 프론트 앱마다 dist/<앱>/index.html 과
|
|
177
|
+
* 그 문서가 참조하는 로컬 에셋이 존재하는지 확인한다. 하나라도 빠지면 ok=false.
|
|
178
|
+
*/
|
|
179
|
+
async function verifyBuildOutput(cwd) {
|
|
180
|
+
const apps = await listFrontendApps(cwd);
|
|
181
|
+
if (apps.length === 0)
|
|
182
|
+
return { ok: true, message: '프론트 앱이 없어 dist 검증을 건너뜁니다.' };
|
|
183
|
+
const problems = [];
|
|
184
|
+
const okApps = [];
|
|
185
|
+
for (const app of apps) {
|
|
186
|
+
const r = await verifyAppDist(cwd, app);
|
|
187
|
+
if (!r.indexExists) {
|
|
188
|
+
problems.push(`✗ apps/${app}: dist/${app}/index.html 이 없습니다 — 이 앱이 빌드되지 않았습니다.\n` +
|
|
189
|
+
` → build 가 apps/${app} 를 순회 빌드하는지 확인하세요(gaon build · 결정 146).`);
|
|
190
|
+
}
|
|
191
|
+
else if (r.missing.length > 0) {
|
|
192
|
+
problems.push(`✗ apps/${app}: index.html 이 참조하는 에셋이 dist/${app} 에 없습니다: ${r.missing.join(', ')}\n` +
|
|
193
|
+
` → base(${app === 'web' ? '/' : `/${app}/`}) 주입이 빠졌을 수 있습니다(비-루트 앱은 /<앱>/ base 필요).`);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
okApps.push(app);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (problems.length > 0) {
|
|
200
|
+
return { ok: false, message: `build 산출 검증 실패(결정 146):\n${problems.join('\n')}` };
|
|
201
|
+
}
|
|
202
|
+
return { ok: true, message: `build 산출 검증 통과 — 앱별 dist: ${okApps.map((a) => `dist/${a}`).join(' · ')}` };
|
|
203
|
+
}
|
|
162
204
|
/** doctor 는 이미 있는 명령을 재사용 — cwd 만 넘긴다. json 은 상위에서. */
|
|
163
205
|
async function runDoctorStep(cwd) {
|
|
164
206
|
try {
|
|
@@ -192,7 +234,7 @@ async function runDoctorStep(cwd) {
|
|
|
192
234
|
*/
|
|
193
235
|
async function regenerateGaon(cwd) {
|
|
194
236
|
const layout = resolveDevLayout(cwd);
|
|
195
|
-
if (!layout.schemaDir && layout.apps.length === 0) {
|
|
237
|
+
if (!layout.schemaDir && layout.apps.length === 0 && !layout.localesDir) {
|
|
196
238
|
return { status: 'skipped', tables: false, apps: [] };
|
|
197
239
|
}
|
|
198
240
|
// 생성기는 사용자 스키마·컨트롤러 .ts 를 동적 import 한다. TS-for-ESM
|
|
@@ -203,6 +245,7 @@ async function regenerateGaon(cwd) {
|
|
|
203
245
|
const result = await regenerateGaonOnce(layout, {
|
|
204
246
|
regenerateTables: generateTablesDts,
|
|
205
247
|
regenerateRoutes: generateRoutesDts,
|
|
248
|
+
regenerateMessages: generateMessagesDts,
|
|
206
249
|
});
|
|
207
250
|
return { status: 'done', tables: result.tables, apps: result.apps };
|
|
208
251
|
}
|
|
@@ -228,7 +271,9 @@ export async function runCheckCommand(opts = {}) {
|
|
|
228
271
|
const cwd = opts.cwd ?? process.cwd();
|
|
229
272
|
const json = opts.json ?? false;
|
|
230
273
|
const only = opts.only;
|
|
231
|
-
|
|
274
|
+
// 결정 157: doctor 는 기본 포함(규칙 5 등 doctor 규칙이 check 만 도는 CI 에서 새지 않게).
|
|
275
|
+
// --no-doctor 로만 뺀다. --include-doctor(구)는 이제 no-op(기본이 된 동작).
|
|
276
|
+
const runDoctor = !opts.noDoctor;
|
|
232
277
|
// 규칙 3 — 검사는 최신 타입 브리지 위에서만 신뢰할 수 있다. 재생성이
|
|
233
278
|
// 실패하면 stale .gaon 으로 검사하지 않고 즉시 중단한다.
|
|
234
279
|
const regen = await regenerateGaon(cwd);
|
|
@@ -236,7 +281,7 @@ export async function runCheckCommand(opts = {}) {
|
|
|
236
281
|
? []
|
|
237
282
|
: only
|
|
238
283
|
? [only]
|
|
239
|
-
: ['typecheck', 'vue-tsc', 'build', ...(
|
|
284
|
+
: ['typecheck', 'vue-tsc', 'build', ...(runDoctor ? ['doctor'] : [])];
|
|
240
285
|
const results = [];
|
|
241
286
|
for (const step of steps) {
|
|
242
287
|
if (step === 'doctor') {
|
package/dist/commands/dev.js
CHANGED
|
@@ -26,6 +26,7 @@ import { registerTsResolve } from '../tsResolve.js';
|
|
|
26
26
|
import { startDev, resolveDevLayout } from '../dev.js';
|
|
27
27
|
import { generateTablesDts, watchDir } from '@gaonjs/data';
|
|
28
28
|
import { generateRoutesDts } from '@gaonjs/web';
|
|
29
|
+
import { generateMessagesDts } from '../messages-gen.js';
|
|
29
30
|
import { createDevConsole } from '../dev/console.js';
|
|
30
31
|
import { ensureInfra, composeDown } from '../dev/docker.js';
|
|
31
32
|
import { startTscWatchers, killChild } from '../dev/tsc.js';
|
|
@@ -86,6 +87,7 @@ async function startGaonRegen(args) {
|
|
|
86
87
|
layout,
|
|
87
88
|
regenerateTables: generateTablesDts,
|
|
88
89
|
regenerateRoutes: generateRoutesDts,
|
|
90
|
+
regenerateMessages: generateMessagesDts,
|
|
89
91
|
watch: watchDir,
|
|
90
92
|
log: (e) => {
|
|
91
93
|
if (e.kind === 'ready') {
|
package/dist/commands/gen.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { generateTablesDts } from '@gaonjs/data';
|
|
17
17
|
import { generateRoutesDts } from '@gaonjs/web';
|
|
18
|
+
import { generateMessagesDts } from '../messages-gen.js';
|
|
18
19
|
import { regenerateGaonOnce, resolveDevLayout } from '../dev.js';
|
|
19
20
|
import { registerTsResolve } from '../tsResolve.js';
|
|
20
21
|
/**
|
|
@@ -24,13 +25,14 @@ import { registerTsResolve } from '../tsResolve.js';
|
|
|
24
25
|
*/
|
|
25
26
|
export async function regenerateProjectGaon(cwd) {
|
|
26
27
|
const layout = resolveDevLayout(cwd);
|
|
27
|
-
if (!layout.schemaDir && layout.apps.length === 0) {
|
|
28
|
-
return { tables: false, apps: [], skipped: true };
|
|
28
|
+
if (!layout.schemaDir && layout.apps.length === 0 && !layout.localesDir) {
|
|
29
|
+
return { tables: false, apps: [], messages: false, skipped: true };
|
|
29
30
|
}
|
|
30
31
|
registerTsResolve();
|
|
31
32
|
const result = await regenerateGaonOnce(layout, {
|
|
32
33
|
regenerateTables: generateTablesDts,
|
|
33
34
|
regenerateRoutes: generateRoutesDts,
|
|
35
|
+
regenerateMessages: generateMessagesDts,
|
|
34
36
|
});
|
|
35
37
|
return { ...result, skipped: false };
|
|
36
38
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ConnectionConfig } from '@gaonjs/data';
|
|
2
|
+
/** 프로젝트(projDir) 관점에서 @gaonjs/data 가 해상되는 경로. 미설치면 null. */
|
|
3
|
+
export declare function resolveProjectDataPath(projDir: string): string | null;
|
|
4
|
+
/** 프로젝트 @gaonjs/data 가 CLI 것과 **다른 인스턴스**인가(전역 설치 이중 로드 신호). */
|
|
5
|
+
export declare function isSplitDataInstance(projDir: string): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* 프로젝트의 @gaonjs/data 인스턴스가 CLI 것과 다르면(전역 설치) 그 인스턴스에 커넥션을
|
|
8
|
+
* 등록해 모델 레이어가 같은 레지스트리를 보게 한다. 같은 인스턴스(로컬)면 resolveDbTarget
|
|
9
|
+
* 이 이미 등록했으므로 **no-op(null 반환)** — 로컬 경로 무회귀. 반환 핸들의 close() 는
|
|
10
|
+
* 프로젝트 인스턴스 커넥션을 정리한다(다른 인스턴스일 때만).
|
|
11
|
+
*/
|
|
12
|
+
export declare function registerInProjectData(projDir: string, dbKey: string, connCfg: ConnectionConfig): Promise<{
|
|
13
|
+
close(): Promise<void>;
|
|
14
|
+
} | null>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// @gaonjs/cli · 프로젝트 로컬 @gaonjs/data 해상 (결정 156 · 전역-로컬 레지스트리 분열 근본 수정)
|
|
2
|
+
//
|
|
3
|
+
// 전역 설치 CLI(`npm i -g gaonjs`)는 자기 번들의 @gaonjs/data 를 로드하고, 프로젝트의
|
|
4
|
+
// domain 코드(seed 가 실행하는 모델)는 **프로젝트 node_modules** 의 @gaonjs/data 를
|
|
5
|
+
// 로드한다(tsResolve 훅이 bare 지정자를 안 건드림 · dual package hazard). 두 인스턴스는
|
|
6
|
+
// 커넥션 레지스트리가 갈려, resolveDbTarget 이 CLI 인스턴스에 등록한 커넥션을 모델
|
|
7
|
+
// 레이어(프로젝트 인스턴스)가 못 봐서 seed 가 "커넥션 'main' 미등록" 으로 죽었다.
|
|
8
|
+
// db migrate/diff/status 는 모델을 안 거쳐 이 분열을 가렸다(13차 실측).
|
|
9
|
+
//
|
|
10
|
+
// 근본 수정: seed 처럼 **프로젝트 모델 레이어를 거치는** 명령은, 프로젝트 인스턴스의
|
|
11
|
+
// @gaonjs/data 에도 커넥션을 등록해 모델과 **단일 레지스트리**를 공유한다. 로컬 설치
|
|
12
|
+
// (CLI·프로젝트가 같은 @gaonjs/data)면 두 경로가 같은 모듈이라 no-op — migrate/diff/
|
|
13
|
+
// status 를 비롯한 로컬 경로는 무회귀다.
|
|
14
|
+
import { createRequire } from 'node:module';
|
|
15
|
+
import { pathToFileURL } from 'node:url';
|
|
16
|
+
import { join, resolve as resolvePath } from 'node:path';
|
|
17
|
+
/** CLI 자신이 로드한 @gaonjs/data 의 해상 경로. */
|
|
18
|
+
function cliDataPath() {
|
|
19
|
+
return createRequire(import.meta.url).resolve('@gaonjs/data');
|
|
20
|
+
}
|
|
21
|
+
/** 프로젝트(projDir) 관점에서 @gaonjs/data 가 해상되는 경로. 미설치면 null. */
|
|
22
|
+
export function resolveProjectDataPath(projDir) {
|
|
23
|
+
try {
|
|
24
|
+
// projDir/package.json 기준으로 해상 — 프로젝트 node_modules 의 @gaonjs/data.
|
|
25
|
+
const req = createRequire(join(resolvePath(projDir), 'package.json'));
|
|
26
|
+
return req.resolve('@gaonjs/data');
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** 프로젝트 @gaonjs/data 가 CLI 것과 **다른 인스턴스**인가(전역 설치 이중 로드 신호). */
|
|
33
|
+
export function isSplitDataInstance(projDir) {
|
|
34
|
+
const projPath = resolveProjectDataPath(projDir);
|
|
35
|
+
return projPath !== null && projPath !== cliDataPath();
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 프로젝트의 @gaonjs/data 인스턴스가 CLI 것과 다르면(전역 설치) 그 인스턴스에 커넥션을
|
|
39
|
+
* 등록해 모델 레이어가 같은 레지스트리를 보게 한다. 같은 인스턴스(로컬)면 resolveDbTarget
|
|
40
|
+
* 이 이미 등록했으므로 **no-op(null 반환)** — 로컬 경로 무회귀. 반환 핸들의 close() 는
|
|
41
|
+
* 프로젝트 인스턴스 커넥션을 정리한다(다른 인스턴스일 때만).
|
|
42
|
+
*/
|
|
43
|
+
export async function registerInProjectData(projDir, dbKey, connCfg) {
|
|
44
|
+
const projPath = resolveProjectDataPath(projDir);
|
|
45
|
+
if (!projPath || projPath === cliDataPath())
|
|
46
|
+
return null; // 로컬 설치 = 단일 인스턴스 · no-op
|
|
47
|
+
const mod = (await import(pathToFileURL(projPath).href));
|
|
48
|
+
const db = mod.createDb(connCfg);
|
|
49
|
+
mod.registerConnection(dbKey, db, connCfg.adapter);
|
|
50
|
+
return { close: () => mod.destroyAllConnections() };
|
|
51
|
+
}
|
package/dist/db.js
CHANGED
|
@@ -12,6 +12,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
12
12
|
import { createDb, registerConnection, destroyAllConnections, isSeedDef, } from '@gaonjs/data';
|
|
13
13
|
import { registerTsResolve } from './tsResolve.js';
|
|
14
14
|
import { resolveDbTarget } from './db/resolve.js';
|
|
15
|
+
import { registerInProjectData, isSplitDataInstance } from './db/projectData.js';
|
|
15
16
|
/** DB URL 에서 어댑터를 추정한다(gaon work 와 동일 규칙). */
|
|
16
17
|
function dbConfigFromUrl(url) {
|
|
17
18
|
if (url.startsWith('mysql://') || url.startsWith('mariadb://')) {
|
|
@@ -52,24 +53,59 @@ export async function runDbSeedCommand(opts = {}) {
|
|
|
52
53
|
if (opts.databaseUrl) {
|
|
53
54
|
const cfg = dbConfigFromUrl(opts.databaseUrl);
|
|
54
55
|
registerConnection(dbKey, createDb(cfg), cfg.adapter);
|
|
56
|
+
// 결정 156: 전역 설치면 프로젝트 인스턴스에도 등록(모델과 단일 레지스트리 공유).
|
|
57
|
+
const projData = await registerInProjectData(root, dbKey, cfg);
|
|
55
58
|
try {
|
|
56
59
|
const def = await loadSeed(root);
|
|
57
|
-
await def
|
|
60
|
+
await runSeed(def, root);
|
|
58
61
|
return { exitCode: 0, ...SEED_OK };
|
|
59
62
|
}
|
|
60
63
|
finally {
|
|
64
|
+
if (projData)
|
|
65
|
+
await projData.close();
|
|
61
66
|
await destroyAllConnections();
|
|
62
67
|
}
|
|
63
68
|
}
|
|
64
69
|
// 그 외 — migrate 와 같은 해석 경로: gaon.config.ts db.<키> → GAON_DATABASE_URL.
|
|
65
70
|
// config 도 env 도 없으면 resolveDbTarget 이 수리 안내 에러를 던진다(§7.5.3).
|
|
66
71
|
const target = await resolveDbTarget({ cwd: root, dbKey, configPath: opts.configPath });
|
|
72
|
+
// 결정 156: seed 는 모델 레이어를 거친다 — 전역 설치(CLI≠프로젝트 @gaonjs/data)면
|
|
73
|
+
// 프로젝트 인스턴스에도 커넥션을 등록해 "main 미등록" 분열을 근본에서 없앤다. 로컬
|
|
74
|
+
// 설치(단일 인스턴스)면 no-op. url 없는 config 는 seed 대상이 아니라 안내한다.
|
|
75
|
+
if (!target.url) {
|
|
76
|
+
await target.close();
|
|
77
|
+
throw new Error(`[gaon db seed] 커넥션 '${dbKey}' 에 url 이 없어 시드를 실행할 수 없습니다.\n` +
|
|
78
|
+
` → gaon.config.ts 의 db.${dbKey} 에 url 을 두거나 .env 의 GAON_DATABASE_URL 을 설정하세요.`);
|
|
79
|
+
}
|
|
80
|
+
const projData = await registerInProjectData(root, dbKey, { adapter: target.adapter, url: target.url, poolMax: 1 });
|
|
67
81
|
try {
|
|
68
82
|
const def = await loadSeed(root);
|
|
69
|
-
await def
|
|
83
|
+
await runSeed(def, root);
|
|
70
84
|
return { exitCode: 0, ...SEED_OK };
|
|
71
85
|
}
|
|
72
86
|
finally {
|
|
87
|
+
if (projData)
|
|
88
|
+
await projData.close();
|
|
73
89
|
await target.close();
|
|
74
90
|
}
|
|
75
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* 시드를 실행하되, "커넥션 미등록" 계열 실패는 전역-로컬 @gaonjs/data 분열로 진단해
|
|
94
|
+
* 실제 수리(로컬 실행)로 안내한다(결정 156 · §7.5.3 — 이미 등록된 커넥션을 또 선언하라던
|
|
95
|
+
* 오진 제거).
|
|
96
|
+
*/
|
|
97
|
+
async function runSeed(def, root) {
|
|
98
|
+
try {
|
|
99
|
+
await def.run();
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
103
|
+
if (/등록되지 않았|not registered|미등록/.test(msg) && isSplitDataInstance(root)) {
|
|
104
|
+
throw new Error(`[gaon db seed] 시드 실행 중 커넥션을 찾지 못했습니다 — 전역 설치 CLI 와 프로젝트가\n` +
|
|
105
|
+
`@gaonjs/data 를 각각 로드해 커넥션 레지스트리가 갈렸습니다(dual package hazard).\n` +
|
|
106
|
+
` → 프로젝트 로컬로 실행하세요: npx gaon db seed (또는 package.json 스크립트로 실행)\n` +
|
|
107
|
+
` (원본: ${msg})`);
|
|
108
|
+
}
|
|
109
|
+
throw err;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** 앱의 URL base — web 은 루트('/'), 그 외는 '/<app>/'(결정 146). vite build 시 주입. */
|
|
2
|
+
export declare function appBase(app: string): string;
|
|
3
|
+
/** apps/* 중 프론트 진입(index.html)이 있는 앱 이름들. web 을 먼저(루트 앱 관례). */
|
|
4
|
+
export declare function listFrontendApps(cwd: string): Promise<string[]>;
|
|
5
|
+
/** vite.config 를 프로젝트 루트·앱 루트 순으로 찾는다(configFile 명시용 · vue 플러그인 보장). */
|
|
6
|
+
export declare function findViteConfig(cwd: string, appRoot: string): string | undefined;
|
|
7
|
+
export interface BuildAppOptions {
|
|
8
|
+
/** watch 모드(dev) — RollupWatcher 를 담은 핸들을 돌려준다(close 로 정리). */
|
|
9
|
+
readonly watch?: boolean;
|
|
10
|
+
/** 통합 콘솔 로그 훅(생략 = vite 가 stdout 으로 직접 출력). */
|
|
11
|
+
readonly onLog?: (line: string, level: 'info' | 'warn' | 'error') => void;
|
|
12
|
+
}
|
|
13
|
+
export interface BuildAppHandle {
|
|
14
|
+
/** watch 모드에서 감시를 종료한다. non-watch 는 no-op. */
|
|
15
|
+
close(): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
/** 한 앱을 vite 로 빌드한다(programmatic · configFile 명시 → vue 플러그인·alias 보장). */
|
|
18
|
+
export declare function buildApp(cwd: string, app: string, opts?: BuildAppOptions): Promise<BuildAppHandle>;
|
|
19
|
+
/** 등록된 프론트 앱을 전부 빌드한다(build 커맨드·게이트 공용). 빌드한 앱 이름을 돌려준다. */
|
|
20
|
+
export declare function buildAllApps(cwd: string, opts?: BuildAppOptions): Promise<string[]>;
|
|
21
|
+
/** 한 앱의 dist 검증 결과. missing 가 비어 있지 않으면 게이트 실패(결정 146). */
|
|
22
|
+
export interface DistVerifyResult {
|
|
23
|
+
readonly app: string;
|
|
24
|
+
readonly indexExists: boolean;
|
|
25
|
+
/** index.html 이 참조하는데 실제 파일이 없는 로컬 에셋 경로(base 기준). */
|
|
26
|
+
readonly missing: string[];
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 앱의 dist/<앱>/index.html 과 그 문서가 참조하는 로컬 에셋(script src·link href)이
|
|
30
|
+
* 실제로 존재하는지 검증한다(결정 146 · gaon check build 게이트). base(/<앱>/)로 시작하는
|
|
31
|
+
* 참조만 검사한다(외부 URL 제외). curl data-page 만으로는 못 보던 "문서는 뜨는데 스크립트
|
|
32
|
+
* 404" 를 정면으로 막는다.
|
|
33
|
+
*/
|
|
34
|
+
export declare function verifyAppDist(cwd: string, app: string): Promise<DistVerifyResult>;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// @gaonjs/cli · dev/build — 멀티 앱 프론트 빌드 파이프라인 (결정 146 · 12차 W2)
|
|
2
|
+
//
|
|
3
|
+
// gaon new 프로젝트는 앱이 하나 이상일 수 있다(gaon g app admin). 각 앱은 자기
|
|
4
|
+
// prefix 아래로 서빙되고(web→'/' · admin→'/admin/'), serve 는 앱별 dist/<앱> 를
|
|
5
|
+
// 기대한다(결정 67). 그런데 예전 파이프라인은 apps/web 하나만 빌드해(vite.config
|
|
6
|
+
// root 하드코딩) 둘째 앱의 dist 가 안 생기고, 생겨도 base 가 없어 index.html 이
|
|
7
|
+
// 절대경로 `/assets/*` 를 박아 비-루트 앱에서 404 였다. 이 모듈이 apps/* 를 순회해
|
|
8
|
+
// 앱마다 root=apps/<앱>·outDir=dist/<앱>·base=/<앱>/ 로 빌드한다(결정 146).
|
|
9
|
+
//
|
|
10
|
+
// vite 는 **프로그램적 build()** 로 부른다 — configFile 을 명시로 넘겨야 vue 플러그인·
|
|
11
|
+
// @shared alias 가 로드된다(positional root 만 주면 앱 폴더에 vite.config 가 없어
|
|
12
|
+
// vue 플러그인이 빠지는 함정 · 결정 146). inline root/base/outDir 가 configFile 을
|
|
13
|
+
// 덮어써 앱별 override 가 확실하다.
|
|
14
|
+
import { existsSync } from 'node:fs';
|
|
15
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
16
|
+
import { join, resolve } from 'node:path';
|
|
17
|
+
/** 앱의 URL base — web 은 루트('/'), 그 외는 '/<app>/'(결정 146). vite build 시 주입. */
|
|
18
|
+
export function appBase(app) {
|
|
19
|
+
return app === 'web' ? '/' : `/${app}/`;
|
|
20
|
+
}
|
|
21
|
+
/** apps/* 중 프론트 진입(index.html)이 있는 앱 이름들. web 을 먼저(루트 앱 관례). */
|
|
22
|
+
export async function listFrontendApps(cwd) {
|
|
23
|
+
const appsDir = join(resolve(cwd), 'apps');
|
|
24
|
+
let entries;
|
|
25
|
+
try {
|
|
26
|
+
entries = (await readdir(appsDir, { withFileTypes: true }));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
const apps = entries
|
|
32
|
+
.filter((e) => e.isDirectory() && existsSync(join(appsDir, e.name, 'index.html')))
|
|
33
|
+
.map((e) => e.name);
|
|
34
|
+
return apps.sort((a, b) => (a === 'web' ? -1 : b === 'web' ? 1 : a.localeCompare(b)));
|
|
35
|
+
}
|
|
36
|
+
/** vite.config 를 프로젝트 루트·앱 루트 순으로 찾는다(configFile 명시용 · vue 플러그인 보장). */
|
|
37
|
+
export function findViteConfig(cwd, appRoot) {
|
|
38
|
+
for (const dir of [appRoot, resolve(cwd)]) {
|
|
39
|
+
for (const name of ['vite.config.ts', 'vite.config.js', 'vite.config.mts', 'vite.config.mjs']) {
|
|
40
|
+
const p = join(dir, name);
|
|
41
|
+
if (existsSync(p))
|
|
42
|
+
return p;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
/** 한 앱을 vite 로 빌드한다(programmatic · configFile 명시 → vue 플러그인·alias 보장). */
|
|
48
|
+
export async function buildApp(cwd, app, opts = {}) {
|
|
49
|
+
const root = resolve(cwd);
|
|
50
|
+
const appRoot = join(root, 'apps', app);
|
|
51
|
+
const vite = await loadVite();
|
|
52
|
+
const result = await vite.build({
|
|
53
|
+
configFile: findViteConfig(root, appRoot),
|
|
54
|
+
root: appRoot,
|
|
55
|
+
base: appBase(app),
|
|
56
|
+
// onLog 있으면 vite 출력을 그 훅으로 라우팅(customLogger 가 stdout 대신 훅으로).
|
|
57
|
+
// 없으면(예: `gaon build --json`) 완전히 침묵 — vite 진행 로그가 stdout 의 JSON 을
|
|
58
|
+
// 오염시키면 안 된다(클린룸이 마지막 줄을 JSON 으로 파싱 · 결정 146 실측).
|
|
59
|
+
logLevel: opts.onLog ? 'info' : 'silent',
|
|
60
|
+
customLogger: opts.onLog ? makeLogger(opts.onLog) : undefined,
|
|
61
|
+
build: {
|
|
62
|
+
outDir: join(root, 'dist', app),
|
|
63
|
+
emptyOutDir: true,
|
|
64
|
+
watch: opts.watch ? {} : null,
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
const watcher = result;
|
|
68
|
+
if (opts.watch && watcher && typeof watcher.close === 'function') {
|
|
69
|
+
return { close: async () => void (await watcher.close()) };
|
|
70
|
+
}
|
|
71
|
+
return { close: async () => { } };
|
|
72
|
+
}
|
|
73
|
+
/** 등록된 프론트 앱을 전부 빌드한다(build 커맨드·게이트 공용). 빌드한 앱 이름을 돌려준다. */
|
|
74
|
+
export async function buildAllApps(cwd, opts = {}) {
|
|
75
|
+
const apps = await listFrontendApps(cwd);
|
|
76
|
+
for (const app of apps) {
|
|
77
|
+
opts.onLog?.(`▶ vite build — apps/${app} → dist/${app} (base=${appBase(app)})`, 'info');
|
|
78
|
+
await buildApp(cwd, app, { onLog: opts.onLog });
|
|
79
|
+
}
|
|
80
|
+
return apps;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 앱의 dist/<앱>/index.html 과 그 문서가 참조하는 로컬 에셋(script src·link href)이
|
|
84
|
+
* 실제로 존재하는지 검증한다(결정 146 · gaon check build 게이트). base(/<앱>/)로 시작하는
|
|
85
|
+
* 참조만 검사한다(외부 URL 제외). curl data-page 만으로는 못 보던 "문서는 뜨는데 스크립트
|
|
86
|
+
* 404" 를 정면으로 막는다.
|
|
87
|
+
*/
|
|
88
|
+
export async function verifyAppDist(cwd, app) {
|
|
89
|
+
const distDir = join(resolve(cwd), 'dist', app);
|
|
90
|
+
const indexPath = join(distDir, 'index.html');
|
|
91
|
+
if (!existsSync(indexPath))
|
|
92
|
+
return { app, indexExists: false, missing: [] };
|
|
93
|
+
const html = await readFile(indexPath, 'utf8');
|
|
94
|
+
const base = appBase(app);
|
|
95
|
+
const missing = [];
|
|
96
|
+
const refs = new Set();
|
|
97
|
+
// <script ... src="..."> · <link ... href="..."> 의 값 추출.
|
|
98
|
+
for (const m of html.matchAll(/<(?:script|link)\b[^>]*\b(?:src|href)\s*=\s*["']([^"']+)["']/gi)) {
|
|
99
|
+
refs.add(m[1]);
|
|
100
|
+
}
|
|
101
|
+
for (const ref of refs) {
|
|
102
|
+
// 이 앱 base 로 시작하는 로컬 에셋만 검사(외부·절대 URL·data: 제외).
|
|
103
|
+
if (!ref.startsWith(base))
|
|
104
|
+
continue;
|
|
105
|
+
const rel = ref.slice(base.length); // base 를 벗겨 dist 상대 경로로.
|
|
106
|
+
const file = join(distDir, rel.split('?')[0].split('#')[0]);
|
|
107
|
+
if (!existsSync(file))
|
|
108
|
+
missing.push(ref);
|
|
109
|
+
}
|
|
110
|
+
return { app, indexExists: true, missing };
|
|
111
|
+
}
|
|
112
|
+
async function loadVite() {
|
|
113
|
+
try {
|
|
114
|
+
return await import('vite');
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
throw new Error(`[gaon build · vite] vite 모듈을 찾을 수 없습니다.\n` +
|
|
118
|
+
`→ 프로젝트 루트에서 설치하세요: pnpm add -D vite @vitejs/plugin-vue\n` +
|
|
119
|
+
`원인: ${err instanceof Error ? err.message : String(err)}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/** vite 로거 어댑터 — 라인을 통합 콘솔 훅으로 흘린다(필요 메서드만). */
|
|
123
|
+
function makeLogger(onLog) {
|
|
124
|
+
const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
125
|
+
let hasWarned = false;
|
|
126
|
+
const seen = new WeakSet();
|
|
127
|
+
return {
|
|
128
|
+
info: (m) => onLog(strip(m), 'info'),
|
|
129
|
+
warn: (m) => {
|
|
130
|
+
hasWarned = true;
|
|
131
|
+
onLog(strip(m), 'warn');
|
|
132
|
+
},
|
|
133
|
+
warnOnce: (m) => {
|
|
134
|
+
hasWarned = true;
|
|
135
|
+
onLog(strip(m), 'warn');
|
|
136
|
+
},
|
|
137
|
+
error: (m) => onLog(strip(m), 'error'),
|
|
138
|
+
clearScreen: () => { },
|
|
139
|
+
hasErrorLogged: (e) => seen.has(e),
|
|
140
|
+
get hasWarned() {
|
|
141
|
+
return hasWarned;
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export interface FrontendBuildHandle {
|
|
2
|
-
/** watch 빌드
|
|
2
|
+
/** watch 빌드(모든 앱)를 종료한다. */
|
|
3
3
|
close(): void;
|
|
4
4
|
}
|
|
5
5
|
export interface StartFrontendBuildOptions {
|
|
@@ -8,7 +8,7 @@ export interface StartFrontendBuildOptions {
|
|
|
8
8
|
readonly onLog: (line: string, level: 'info' | 'warn' | 'error') => void;
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
|
-
* vite
|
|
12
|
-
* 않고 no-op 핸들을 돌려준다(dev 는 계속 진행).
|
|
11
|
+
* apps/* 를 순회해 앱마다 vite watch 빌드를 띄운다. 프론트가 없거나 vite 가 없으면
|
|
12
|
+
* 아무 것도 하지 않고 no-op 핸들을 돌려준다(dev 는 계속 진행 · fail-open).
|
|
13
13
|
*/
|
|
14
14
|
export declare function startFrontendBuild(opts: StartFrontendBuildOptions): FrontendBuildHandle;
|