@gaonjs/cli 0.33.0 → 0.35.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/check.d.ts +10 -1
- package/dist/commands/check.js +10 -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.d.ts +11 -1
- package/dist/dev.js +29 -1
- package/dist/generate.d.ts +24 -2
- package/dist/generate.js +112 -34
- package/dist/index.d.ts +1 -0
- package/dist/index.js +7 -5
- package/dist/messages-gen.d.ts +6 -0
- package/dist/messages-gen.js +24 -0
- package/dist/templates/auth/Login.vue.tpl +1 -4
- package/dist/templates/auth/dashboard.secure.controller.ts.tpl +14 -0
- 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 +37 -0
- package/dist/templates/project/agents/data.md.tpl +70 -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 +56 -10
- package/dist/templates/project/agents/testing.md.tpl +43 -0
- package/dist/templates/project/agents/web.md.tpl +54 -0
- package/dist/templates/project/apps/web/layouts/Default.vue.tpl +20 -1
- package/dist/templates/project/gaon.config.ts.tpl +14 -0
- package/package.json +7 -6
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
|
* 하나도 없으면(예: 스크립트도 없고 바이너리도 없음) 스킵으로 리포트 —
|
|
@@ -34,6 +34,7 @@ import { runDoctorCommand } from '../doctor.js';
|
|
|
34
34
|
import { regenerateGaonOnce, resolveDevLayout } from '../dev.js';
|
|
35
35
|
import { registerTsResolve } from '../tsResolve.js';
|
|
36
36
|
import { listFrontendApps, verifyAppDist } from '../dev/build.js';
|
|
37
|
+
import { generateMessagesDts } from '../messages-gen.js';
|
|
37
38
|
/**
|
|
38
39
|
* 프로젝트 스크립트 존재 여부. pnpm/npm 어느 쪽이든 `scripts.<name>` 을
|
|
39
40
|
* 정의해 두면 우선 사용한다.
|
|
@@ -233,7 +234,7 @@ async function runDoctorStep(cwd) {
|
|
|
233
234
|
*/
|
|
234
235
|
async function regenerateGaon(cwd) {
|
|
235
236
|
const layout = resolveDevLayout(cwd);
|
|
236
|
-
if (!layout.schemaDir && layout.apps.length === 0) {
|
|
237
|
+
if (!layout.schemaDir && layout.apps.length === 0 && !layout.localesDir) {
|
|
237
238
|
return { status: 'skipped', tables: false, apps: [] };
|
|
238
239
|
}
|
|
239
240
|
// 생성기는 사용자 스키마·컨트롤러 .ts 를 동적 import 한다. TS-for-ESM
|
|
@@ -244,6 +245,7 @@ async function regenerateGaon(cwd) {
|
|
|
244
245
|
const result = await regenerateGaonOnce(layout, {
|
|
245
246
|
regenerateTables: generateTablesDts,
|
|
246
247
|
regenerateRoutes: generateRoutesDts,
|
|
248
|
+
regenerateMessages: generateMessagesDts,
|
|
247
249
|
});
|
|
248
250
|
return { status: 'done', tables: result.tables, apps: result.apps };
|
|
249
251
|
}
|
|
@@ -269,7 +271,9 @@ export async function runCheckCommand(opts = {}) {
|
|
|
269
271
|
const cwd = opts.cwd ?? process.cwd();
|
|
270
272
|
const json = opts.json ?? false;
|
|
271
273
|
const only = opts.only;
|
|
272
|
-
|
|
274
|
+
// 결정 157: doctor 는 기본 포함(규칙 5 등 doctor 규칙이 check 만 도는 CI 에서 새지 않게).
|
|
275
|
+
// --no-doctor 로만 뺀다. --include-doctor(구)는 이제 no-op(기본이 된 동작).
|
|
276
|
+
const runDoctor = !opts.noDoctor;
|
|
273
277
|
// 규칙 3 — 검사는 최신 타입 브리지 위에서만 신뢰할 수 있다. 재생성이
|
|
274
278
|
// 실패하면 stale .gaon 으로 검사하지 않고 즉시 중단한다.
|
|
275
279
|
const regen = await regenerateGaon(cwd);
|
|
@@ -277,7 +281,7 @@ export async function runCheckCommand(opts = {}) {
|
|
|
277
281
|
? []
|
|
278
282
|
: only
|
|
279
283
|
? [only]
|
|
280
|
-
: ['typecheck', 'vue-tsc', 'build', ...(
|
|
284
|
+
: ['typecheck', 'vue-tsc', 'build', ...(runDoctor ? ['doctor'] : [])];
|
|
281
285
|
const results = [];
|
|
282
286
|
for (const step of steps) {
|
|
283
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
|
+
}
|
package/dist/dev.d.ts
CHANGED
|
@@ -11,11 +11,17 @@ export interface DevLayout {
|
|
|
11
11
|
/** .gaon/tables.d.ts */
|
|
12
12
|
readonly tablesOut: string;
|
|
13
13
|
readonly apps: readonly DevApp[];
|
|
14
|
+
/** locales/ (없으면 undefined — messages 축 생략 · 결정 158 · W2). */
|
|
15
|
+
readonly localesDir?: string;
|
|
16
|
+
/** .gaon/messages.d.ts */
|
|
17
|
+
readonly messagesOut: string;
|
|
14
18
|
}
|
|
15
19
|
export interface DevDeps {
|
|
16
20
|
readonly layout: DevLayout;
|
|
17
21
|
regenerateTables(schemaDir: string, out: string): Promise<unknown>;
|
|
18
22
|
regenerateRoutes(appDir: string, out: string): Promise<unknown>;
|
|
23
|
+
/** locales/ → .gaon/messages.d.ts (결정 158 · W2). i18n 축을 쓰는 호출자만 준다. */
|
|
24
|
+
regenerateMessages?(localesDir: string, out: string): unknown;
|
|
19
25
|
watch(dir: string, opts: WatchOptions): WatchHandle;
|
|
20
26
|
log(event: DevEvent): void;
|
|
21
27
|
onError(err: Error): void;
|
|
@@ -26,7 +32,7 @@ export type DevEvent = {
|
|
|
26
32
|
readonly apps: string[];
|
|
27
33
|
} | {
|
|
28
34
|
readonly kind: 'regen';
|
|
29
|
-
readonly target: 'tables' | 'routes';
|
|
35
|
+
readonly target: 'tables' | 'routes' | 'messages';
|
|
30
36
|
readonly app?: string;
|
|
31
37
|
} | {
|
|
32
38
|
readonly kind: 'stopped';
|
|
@@ -44,12 +50,16 @@ export declare function startDev(deps: DevDeps): Promise<DevHandle>;
|
|
|
44
50
|
export interface RegenDeps {
|
|
45
51
|
regenerateTables(schemaDir: string, out: string): Promise<unknown>;
|
|
46
52
|
regenerateRoutes(appDir: string, out: string): Promise<unknown>;
|
|
53
|
+
/** locales/ → .gaon/messages.d.ts (결정 158 · W2). i18n 축을 쓰는 호출자만 준다. */
|
|
54
|
+
regenerateMessages?(localesDir: string, out: string): unknown;
|
|
47
55
|
}
|
|
48
56
|
export interface RegenResult {
|
|
49
57
|
/** tables.d.ts 를 재생성했는가(domain/schema 존재 시에만). */
|
|
50
58
|
readonly tables: boolean;
|
|
51
59
|
/** routes.d.ts 를 재생성한 앱 이름. */
|
|
52
60
|
readonly apps: readonly string[];
|
|
61
|
+
/** messages.d.ts 를 재생성했는가(locales/ 존재 · 결정 158 · W2). */
|
|
62
|
+
readonly messages: boolean;
|
|
53
63
|
}
|
|
54
64
|
/**
|
|
55
65
|
* 워처 없이 .gaon 타입 브리지를 1회 전체 재생성한다. `gaon check` 처럼
|
package/dist/dev.js
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { readdirSync, existsSync } from 'node:fs';
|
|
15
15
|
import { join, resolve } from 'node:path';
|
|
16
|
+
/** locales/*.json 변경(메시지 카탈로그) — messages.d.ts 재생성 트리거. */
|
|
17
|
+
const isMessagesChange = (f) => !f.includes('.gaon') && f.endsWith('.json');
|
|
16
18
|
const isSchemaChange = (f) => f.endsWith('.ts') && !f.endsWith('.d.ts') && !f.endsWith('.test.ts') && !f.includes('.gaon');
|
|
17
19
|
const isRoutesChange = (f) => !f.includes('.gaon') &&
|
|
18
20
|
f.endsWith('.ts') &&
|
|
@@ -35,6 +37,11 @@ export async function startDev(deps) {
|
|
|
35
37
|
await deps.regenerateRoutes(app.appDir, app.routesOut);
|
|
36
38
|
deps.log({ kind: 'regen', target: 'routes', app: app.name });
|
|
37
39
|
}
|
|
40
|
+
// 결정 158(W2): locales/ → messages.d.ts.
|
|
41
|
+
if (layout.localesDir && deps.regenerateMessages) {
|
|
42
|
+
await deps.regenerateMessages(layout.localesDir, layout.messagesOut);
|
|
43
|
+
deps.log({ kind: 'regen', target: 'messages' });
|
|
44
|
+
}
|
|
38
45
|
// 2) 스키마 워치 → tables.d.ts 재생성.
|
|
39
46
|
if (layout.schemaDir) {
|
|
40
47
|
const schemaDir = layout.schemaDir;
|
|
@@ -58,6 +65,19 @@ export async function startDev(deps) {
|
|
|
58
65
|
onError: deps.onError,
|
|
59
66
|
}));
|
|
60
67
|
}
|
|
68
|
+
// 4) locales 워치 → messages.d.ts 재생성(결정 158 · W2).
|
|
69
|
+
if (layout.localesDir && deps.regenerateMessages) {
|
|
70
|
+
const localesDir = layout.localesDir;
|
|
71
|
+
const regenerateMessages = deps.regenerateMessages;
|
|
72
|
+
handles.push(deps.watch(localesDir, {
|
|
73
|
+
filter: isMessagesChange,
|
|
74
|
+
onChange: async () => {
|
|
75
|
+
await regenerateMessages(localesDir, layout.messagesOut);
|
|
76
|
+
deps.log({ kind: 'regen', target: 'messages' });
|
|
77
|
+
},
|
|
78
|
+
onError: deps.onError,
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
61
81
|
deps.log({ kind: 'ready', schema: !!layout.schemaDir, apps: layout.apps.map((a) => a.name) });
|
|
62
82
|
return {
|
|
63
83
|
close() {
|
|
@@ -80,7 +100,12 @@ export async function regenerateGaonOnce(layout, deps) {
|
|
|
80
100
|
for (const app of layout.apps) {
|
|
81
101
|
await deps.regenerateRoutes(app.appDir, app.routesOut);
|
|
82
102
|
}
|
|
83
|
-
|
|
103
|
+
// 결정 158(W2): locales/ 가 있으면 messages.d.ts 도 재생성한다(i18n 키 타입 브리지).
|
|
104
|
+
let messages = false;
|
|
105
|
+
if (layout.localesDir && deps.regenerateMessages) {
|
|
106
|
+
messages = (await deps.regenerateMessages(layout.localesDir, layout.messagesOut)) === true;
|
|
107
|
+
}
|
|
108
|
+
return { tables: !!layout.schemaDir, apps: layout.apps.map((a) => a.name), messages };
|
|
84
109
|
}
|
|
85
110
|
/** cwd 관례로 프로젝트 레이아웃을 해석한다(존재하는 것만 포함). */
|
|
86
111
|
export function resolveDevLayout(cwd) {
|
|
@@ -102,10 +127,13 @@ export function resolveDevLayout(cwd) {
|
|
|
102
127
|
});
|
|
103
128
|
}
|
|
104
129
|
}
|
|
130
|
+
const localesDirPath = join(root, 'locales');
|
|
105
131
|
return {
|
|
106
132
|
schemaDir: existsSync(schemaDirPath) ? schemaDirPath : undefined,
|
|
107
133
|
tablesOut: join(root, '.gaon', 'tables.d.ts'),
|
|
108
134
|
apps: apps.sort((a, b) => a.name.localeCompare(b.name)),
|
|
135
|
+
localesDir: existsSync(localesDirPath) ? localesDirPath : undefined,
|
|
136
|
+
messagesOut: join(root, '.gaon', 'messages.d.ts'),
|
|
109
137
|
};
|
|
110
138
|
}
|
|
111
139
|
// runDevCommand · DevCommandOptions 는 M9-C 에서 commands/dev.ts 로 이동.
|
package/dist/generate.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
export interface AuthScaffoldOptions {
|
|
2
2
|
/** 대상 앱(apps/<app>). 기본 'web'. */
|
|
3
3
|
readonly app?: string;
|
|
4
|
+
/**
|
|
5
|
+
* 결정 155: 공개 회원가입(registration)을 깔지 여부. 기본은 **앱명 기반 시큐어**
|
|
6
|
+
* — web 은 공개 가입 O, 비-web(admin 등)은 공개 가입 X + 역할 게이트(authorize).
|
|
7
|
+
* `--public` 로 비-web 앱도 공개 가입을 opt-in 한다(공개 비-web 앱용 탈출구).
|
|
8
|
+
*/
|
|
9
|
+
readonly public?: boolean;
|
|
4
10
|
}
|
|
5
11
|
/** 생성할 파일 하나 — 경로는 프로젝트 루트 기준. */
|
|
6
12
|
export interface ScaffoldFile {
|
|
@@ -14,13 +20,27 @@ export interface ScaffoldResult {
|
|
|
14
20
|
/** 손 수리가 필요한 지점 — 수리 안내 문장(§7.5.3)을 그대로 담는다. */
|
|
15
21
|
readonly warnings: string[];
|
|
16
22
|
}
|
|
17
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* 결정 155·142: `g auth --app <앱>` 이 앱별 세션 secret 환경변수를 .env·.env.example
|
|
25
|
+
* 에도 시드한다. web 은 SESSION_SECRET 이 프로젝트 스캐폴드(.env.example.tpl)에 이미
|
|
26
|
+
* 있어 no-op 이지만, 비-web 앱(admin 등)은 이 키가 어디에도 없어 app.config 의
|
|
27
|
+
* `process.env.ADMIN_SESSION_SECRET ?? '공개 고정 dev secret'` 이 조용히 폴백으로
|
|
28
|
+
* 돌아 세션 위조 표면이 된다. env 파일에 키를 심어 사용자가 설정해야 함을 드러낸다
|
|
29
|
+
* (앱별 세션 완전 분리를 env 층까지 확장).
|
|
30
|
+
*
|
|
31
|
+
* 이미 그 키가 있으면 건드리지 않는다(멱등). 파일이 없으면 만들지 않는다 — 스캐폴드가
|
|
32
|
+
* .env 를 새로 만드는 관례가 없어(사용자가 cp .env.example .env), 존재하는 파일만 패치한다.
|
|
33
|
+
* 반환: 실제로 키를 추가한 상대 경로들.
|
|
34
|
+
*/
|
|
35
|
+
export declare function patchEnvFiles(root: string, app: string): string[];
|
|
36
|
+
/** 인증 스캐폴드가 생성하는 파일 목록(라우트 제외). 템플릿을 읽어 렌더링한다.
|
|
37
|
+
* 결정 155: 공개 가입 여부(includePublic)로 registration·Signup·dashboard 변형을 가른다. */
|
|
18
38
|
export declare function authScaffoldFiles(opts?: AuthScaffoldOptions): ScaffoldFile[];
|
|
19
39
|
/**
|
|
20
40
|
* 기존 routes.ts 에 세션·회원가입 리소스를 끼워 넣는다. 이미 있으면 null.
|
|
21
41
|
* `routes((r) => {` 콜백 여는 지점 뒤에 두 줄을 삽입한다.
|
|
22
42
|
*/
|
|
23
|
-
export declare function patchRoutes(existing: string): string | null;
|
|
43
|
+
export declare function patchRoutes(existing: string, includePublic?: boolean): string | null;
|
|
24
44
|
/**
|
|
25
45
|
* 기존 app.config.ts 에 auth 배선을 끼워 넣는다 (결정 93 · gaon new 기본 web 앱은
|
|
26
46
|
* 세션만 배선돼 있어 g auth 는 auth 만 추가하면 완성된다). 반환:
|
|
@@ -35,6 +55,8 @@ export interface GenerateAuthOptions {
|
|
|
35
55
|
readonly cwd?: string;
|
|
36
56
|
readonly app?: string;
|
|
37
57
|
readonly json?: boolean;
|
|
58
|
+
/** 결정 155: 비-web 앱도 공개 회원가입을 opt-in(공개 비-web 앱용 탈출구). */
|
|
59
|
+
readonly public?: boolean;
|
|
38
60
|
}
|
|
39
61
|
/** `gaon g auth` 진입점. 스캐폴드를 쓰고 결과를 사람/JSON 으로 출력한다. */
|
|
40
62
|
export declare function runGenerateAuthCommand(opts?: GenerateAuthOptions): number;
|