@gaonjs/cli 0.43.0 → 0.52.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/README.md +1 -1
- package/dist/commands/check.d.ts +1 -1
- package/dist/commands/check.js +1 -1
- package/dist/commands/db.js +29 -7
- package/dist/commands/new.d.ts +2 -0
- package/dist/commands/new.js +8 -3
- package/dist/commands/test.js +16 -3
- package/dist/db/journal.d.ts +4 -3
- package/dist/db/journal.js +21 -10
- package/dist/db/migrate.d.ts +3 -1
- package/dist/db/migrate.js +3 -3
- package/dist/db/replay.js +2 -2
- package/dist/db/resolve.d.ts +15 -0
- package/dist/db/resolve.js +24 -2
- package/dist/db/status.js +13 -3
- package/dist/dev.d.ts +6 -4
- package/dist/dev.js +9 -4
- package/dist/doctor/fixers/index.d.ts +1 -1
- package/dist/doctor/fixers/index.js +6 -1
- package/dist/doctor/locale-parity.js +4 -1
- package/dist/doctor/pageprops-destructure.d.ts +2 -2
- package/dist/doctor/pageprops-destructure.js +29 -23
- package/dist/doctor/render-return.d.ts +11 -0
- package/dist/doctor/render-return.js +143 -0
- package/dist/doctor/types.d.ts +1 -1
- package/dist/doctor.d.ts +3 -2
- package/dist/doctor.js +17 -6
- package/dist/generate.d.ts +20 -1
- package/dist/generate.js +120 -21
- package/dist/hub.js +2 -0
- package/dist/i18n-config.d.ts +12 -0
- package/dist/i18n-config.js +95 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +59 -14
- package/dist/mcp/tools.d.ts +1 -1
- package/dist/mcp/tools.js +9 -6
- package/dist/messages-gen.d.ts +1 -1
- package/dist/messages-gen.js +5 -2
- package/dist/scaffold/job.js +3 -1
- package/dist/templates/auth/Dashboard.vue.tpl +3 -2
- package/dist/templates/auth/Login.vue.tpl +3 -5
- package/dist/templates/auth/Signup.vue.tpl +3 -5
- package/dist/templates/auth/jwt.app.config.ts.tpl +18 -0
- package/dist/templates/auth/jwt.auth.wiring.ts.tpl +16 -0
- package/dist/templates/auth/jwt.routes.ts.tpl +7 -0
- package/dist/templates/auth/jwt.session.controller.ts.tpl +36 -0
- package/dist/templates/project/.dockerignore.tpl +3 -0
- package/dist/templates/project/.env.example.tpl +1 -1
- package/dist/templates/project/AGENTS.md.tpl +5 -3
- package/dist/templates/project/CLAUDE.md.tpl +5 -4
- package/dist/templates/project/Dockerfile.tpl +6 -1
- package/dist/templates/project/agents/async.md.tpl +75 -10
- package/dist/templates/project/agents/data.md.tpl +159 -22
- package/dist/templates/project/agents/frontend.md.tpl +51 -12
- package/dist/templates/project/agents/i18n.md.tpl +5 -2
- package/dist/templates/project/agents/mail.md.tpl +4 -2
- package/dist/templates/project/agents/realtime.md.tpl +37 -3
- package/dist/templates/project/agents/seal.md.tpl +14 -3
- package/dist/templates/project/agents/security.md.tpl +47 -11
- package/dist/templates/project/agents/storage.md.tpl +16 -8
- package/dist/templates/project/agents/web.md.tpl +78 -24
- package/dist/templates/project/apps/web/composables/useApiPing.ts.tpl +4 -3
- package/dist/templates/project/apps/web/controllers/home.ts.tpl +1 -1
- package/dist/templates/project/apps/web/main.ts.tpl +1 -1
- package/dist/templates/project/apps/web/routes.ts.tpl +1 -1
- package/dist/templates/project/docker-compose.yaml.tpl +1 -1
- package/dist/templates/project/pnpm-workspace.yaml.tpl +1 -1
- package/dist/templates/project/vite.config.ts.tpl +1 -1
- package/dist/work.d.ts +21 -0
- package/dist/work.js +45 -1
- package/package.json +12 -7
- package/dist/templates/index.ts +0 -109
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Gaon CLI — `gaon` 명령. 프로젝트 스캐폴드(`gaon new`), 개발 스택 통합(`gaon dev`),
|
|
4
4
|
제너레이터(`gaon g auth`·scaffold), 마이그레이션(`gaon db …`), 통합 검사
|
|
5
|
-
(`gaon check`), 정적 검사(`gaon doctor` ·
|
|
5
|
+
(`gaon check`), 정적 검사(`gaon doctor` · `--fix`), 운영 부팅(`gaon serve`). 모든
|
|
6
6
|
명령은 `--json` 출력을 지원합니다.
|
|
7
7
|
|
|
8
8
|
이 패키지는 [`gaonjs`](https://www.npmjs.com/package/gaonjs) 파사드에 포함됩니다 —
|
package/dist/commands/check.d.ts
CHANGED
|
@@ -52,7 +52,7 @@ export interface CheckReport {
|
|
|
52
52
|
/**
|
|
53
53
|
* `gaon check` 진입점. 검사 전에 .gaon 을 재생성(규칙 3)한 뒤 각 단계를
|
|
54
54
|
* 순서대로 실행하고, 하나라도 실패하면 exit 1. --only 지정 시 그 단계만
|
|
55
|
-
* 실행. --
|
|
55
|
+
* 실행. doctor 는 기본 포함(결정 157) — --no-doctor 로만 뺀다.
|
|
56
56
|
*
|
|
57
57
|
* §9 실 인프라 · 목업 X — 실 spawn 으로 검증한다.
|
|
58
58
|
*/
|
package/dist/commands/check.js
CHANGED
|
@@ -271,7 +271,7 @@ async function regenerateGaon(cwd) {
|
|
|
271
271
|
/**
|
|
272
272
|
* `gaon check` 진입점. 검사 전에 .gaon 을 재생성(규칙 3)한 뒤 각 단계를
|
|
273
273
|
* 순서대로 실행하고, 하나라도 실패하면 exit 1. --only 지정 시 그 단계만
|
|
274
|
-
* 실행. --
|
|
274
|
+
* 실행. doctor 는 기본 포함(결정 157) — --no-doctor 로만 뺀다.
|
|
275
275
|
*
|
|
276
276
|
* §9 실 인프라 · 목업 X — 실 spawn 으로 검증한다.
|
|
277
277
|
*/
|
package/dist/commands/db.js
CHANGED
|
@@ -11,7 +11,7 @@ import { runDbDiff } from '../db/diff.js';
|
|
|
11
11
|
import { runDbMigrate } from '../db/migrate.js';
|
|
12
12
|
import { runDbReset } from '../db/reset.js';
|
|
13
13
|
import { runDbStatus } from '../db/status.js';
|
|
14
|
-
import {
|
|
14
|
+
import { listConnectionKeysByKind } from '../db/resolve.js';
|
|
15
15
|
import { runDbSeedCommand } from '../db.js';
|
|
16
16
|
/** 전 커넥션 순회 대상 서브커맨드(결정 139 · reset 은 파괴적이라 제외 · 단일 유지). */
|
|
17
17
|
const MULTI_CONN_SUBS = new Set(['diff', 'migrate', 'status', 'seed']);
|
|
@@ -57,14 +57,29 @@ export async function runDbCommand(subcommand, opts = {}) {
|
|
|
57
57
|
// `--db <키>` 는 단일 좁힘. reset(파괴적)은 항상 단일(main 폴백).
|
|
58
58
|
const wantsAll = subcommand !== 'reset' && (opts.db === undefined || opts.db === 'all');
|
|
59
59
|
if (wantsAll && MULTI_CONN_SUBS.has(subcommand)) {
|
|
60
|
-
|
|
61
|
-
//
|
|
62
|
-
|
|
60
|
+
// 결정 292: 문서형(mongodb) 커넥션은 마이그레이션이 없어(§7.4) 순회에서 **건너뛴다** —
|
|
61
|
+
// 종전에는 mongo 키가 실패로 계상돼 mongo 커넥션이 하나라도 있으면 항상 exit 1 이었다.
|
|
62
|
+
// `--db <mongo키>` 명시 지정은 아래 단일 경로에서 여전히 fail-loud(resolveDbTarget).
|
|
63
|
+
const { sql: keys, mongo: mongoKeys } = await listConnectionKeysByKind(cwd, configPath);
|
|
64
|
+
if (keys.length === 0) {
|
|
65
|
+
// 문서형만 있는 프로젝트 — 마이그레이션 대상 없음 = 성공 no-op(수리 대상 아님).
|
|
66
|
+
const note = `문서형(mongodb) 커넥션 ${mongoKeys.join(', ')} 은 마이그레이션 대상이 아닙니다(§7.4) — SQL 커넥션 없음, ${subcommand} 할 것 없음.`;
|
|
67
|
+
emit(` ○ ${note}`, {
|
|
68
|
+
command: subcommand,
|
|
69
|
+
connections: [],
|
|
70
|
+
skipped: mongoKeys.map((db) => ({ db, adapter: 'mongodb', reason: 'documental-no-migration' })),
|
|
71
|
+
ok: true,
|
|
72
|
+
results: [],
|
|
73
|
+
});
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
// 단일 SQL 커넥션 + 문서형 없음이면 종전 출력과 100% 동일(하위 호환).
|
|
77
|
+
if (keys.length === 1 && mongoKeys.length === 0) {
|
|
63
78
|
const r = await runOne(keys[0]);
|
|
64
79
|
emit(r.text, r.json);
|
|
65
80
|
return r.exitCode;
|
|
66
81
|
}
|
|
67
|
-
return runAcrossConnections(subcommand, keys, runOne, emit, json);
|
|
82
|
+
return runAcrossConnections(subcommand, keys, runOne, emit, json, mongoKeys);
|
|
68
83
|
}
|
|
69
84
|
// 단일 커넥션 경로(--db <키> · reset · env-only 폴백) — 종전 동작 그대로.
|
|
70
85
|
if (subcommand === 'reset' && opts.db === 'all') {
|
|
@@ -97,7 +112,7 @@ export async function runDbCommand(subcommand, opts = {}) {
|
|
|
97
112
|
* 실패해도 나머지를 계속한다(부분 적용 방지가 아니라 부분 진행 후 전체 보고 — 어느
|
|
98
113
|
* 커넥션이 성공/실패했는지가 사용자에게 필요). exitCode = 하나라도 실패면 1.
|
|
99
114
|
*/
|
|
100
|
-
async function runAcrossConnections(subcommand, keys, runOne, emit, json) {
|
|
115
|
+
async function runAcrossConnections(subcommand, keys, runOne, emit, json, skippedMongo = []) {
|
|
101
116
|
const results = [];
|
|
102
117
|
for (const key of keys) {
|
|
103
118
|
try {
|
|
@@ -121,12 +136,19 @@ async function runAcrossConnections(subcommand, keys, runOne, emit, json) {
|
|
|
121
136
|
emit('', {
|
|
122
137
|
command: subcommand,
|
|
123
138
|
connections: keys,
|
|
139
|
+
// 결정 292: 문서형(mongodb) 커넥션은 순회 대상이 아니다 — skipped 로 명시 보고.
|
|
140
|
+
...(skippedMongo.length > 0
|
|
141
|
+
? { skipped: skippedMongo.map((db) => ({ db, adapter: 'mongodb', reason: 'documental-no-migration' })) }
|
|
142
|
+
: {}),
|
|
124
143
|
ok: failed.length === 0,
|
|
125
144
|
results: results.map((r) => ({ db: r.db, ok: r.ok, ...r.json })),
|
|
126
145
|
});
|
|
127
146
|
return exitCode;
|
|
128
147
|
}
|
|
129
|
-
const
|
|
148
|
+
const skippedNote = skippedMongo.length > 0
|
|
149
|
+
? `\n ○ 문서형(mongodb) 커넥션 건너뜀: ${skippedMongo.join(', ')} — 문서형은 마이그레이션이 없습니다(§7.4 · 인덱스는 Mongoose 스키마 선언).`
|
|
150
|
+
: '';
|
|
151
|
+
const header = ` gaon db ${subcommand} · 커넥션 ${keys.length}개 순회: ${keys.join(', ')}${skippedNote}`;
|
|
130
152
|
const sections = results.map((r) => ` ── [${r.db}] ${'─'.repeat(Math.max(0, 40 - r.db.length))}\n${r.text}`);
|
|
131
153
|
const footer = failed.length === 0
|
|
132
154
|
? ` ✓ 전 커넥션 완료(${keys.length}개).`
|
package/dist/commands/new.d.ts
CHANGED
|
@@ -44,6 +44,8 @@ export interface NewCommandResult {
|
|
|
44
44
|
readonly totalMs: number;
|
|
45
45
|
readonly error?: string;
|
|
46
46
|
}
|
|
47
|
+
/** 이름 유효성 — npm 패키지명 규칙(단순 부분)만 검사. */
|
|
48
|
+
export declare function validateProjectName(name: string): void;
|
|
47
49
|
/**
|
|
48
50
|
* `gaon new <name>` 실행. 파일 생성 → pnpm install → git init 순서.
|
|
49
51
|
* 각 단계는 옵션으로 스킵 가능. 반환값은 프로세스 종료 코드(0=성공).
|
package/dist/commands/new.js
CHANGED
|
@@ -26,7 +26,7 @@ import { renderProjectFiles, PACKAGE_MANAGER_PINS, } from '../templates/index.js
|
|
|
26
26
|
import { writeUiKitScaffold } from '../uikit.js';
|
|
27
27
|
import { regenerateProjectGaon } from './gen.js';
|
|
28
28
|
/** 이름 유효성 — npm 패키지명 규칙(단순 부분)만 검사. */
|
|
29
|
-
function validateProjectName(name) {
|
|
29
|
+
export function validateProjectName(name) {
|
|
30
30
|
if (!name)
|
|
31
31
|
throw new Error('프로젝트 이름이 비어 있습니다.');
|
|
32
32
|
if (name.includes('/') || name.includes('\\')) {
|
|
@@ -35,8 +35,13 @@ function validateProjectName(name) {
|
|
|
35
35
|
if (name.includes('..')) {
|
|
36
36
|
throw new Error(`프로젝트 이름에 '..' 는 사용할 수 없습니다: ${name}`);
|
|
37
37
|
}
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
// 결정 315: 소문자·숫자·하이픈만 — 이름이 compose 프로젝트명(소문자만 유효)과
|
|
39
|
+
// MinIO/S3 버킷명(`_`·대문자 불허)으로 그대로 치환되므로(docker-compose.yaml.tpl),
|
|
40
|
+
// 대문자·언더스코어를 받으면 gaon dev 인프라 기동·첫 업로드가 뒤늦게 깨진다.
|
|
41
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) {
|
|
42
|
+
throw new Error(`프로젝트 이름은 소문자 영문/숫자로 시작해 소문자/숫자/하이픈(-)만 씁니다: ${name}\n` +
|
|
43
|
+
` → 이름은 Docker Compose 프로젝트명과 스토리지 버킷명으로 그대로 쓰여\n` +
|
|
44
|
+
` 대문자·언더스코어(_)는 인프라 기동·버킷 생성에서 실패합니다.\n` +
|
|
40
45
|
` → 예: gaon new my-app · gaon new demo`);
|
|
41
46
|
}
|
|
42
47
|
}
|
package/dist/commands/test.js
CHANGED
|
@@ -106,7 +106,11 @@ async function provisionTestDatabases(cwd, json, onOutput) {
|
|
|
106
106
|
connectionOverride: testCfg,
|
|
107
107
|
});
|
|
108
108
|
if (res.exitCode !== 0) {
|
|
109
|
-
|
|
109
|
+
// 결정 360: json 모드도 구조화 에러를 낸다 — 이전엔 이 분기만 0 바이트로
|
|
110
|
+
// exit 1 이라 자동화가 실패 원인을 전혀 못 받았다(catch 분기와 비대칭).
|
|
111
|
+
if (json)
|
|
112
|
+
writeOut(JSON.stringify({ ok: false, kind: 'provision', db: key, error: res.text }) + '\n');
|
|
113
|
+
else
|
|
110
114
|
writeErr(` ✗ 테스트 DB '${key}' 마이그레이션 실패\n${res.text}\n`);
|
|
111
115
|
return false;
|
|
112
116
|
}
|
|
@@ -211,18 +215,27 @@ export async function runTestCommand(args = [], opts = {}) {
|
|
|
211
215
|
else {
|
|
212
216
|
writeOut(` gaon test · ${cmd} ${spawnArgs.join(' ')} (scope=${scope})\n`);
|
|
213
217
|
}
|
|
218
|
+
// 결정 360: --json 은 stdout 이 순수 JSON 라인이어야 한다(결정 269 check 와 대칭).
|
|
219
|
+
// 이전엔 json 모드도 stdio:'inherit' 라 vitest 사람용 출력이 JSON 라인 사이에 섞여
|
|
220
|
+
// `gaon test --json | jq` 류 자동화가 깨졌다. 자식 출력은 {kind:'output'} 라인으로 감싼다.
|
|
221
|
+
const jsonPipe = json && !capture;
|
|
214
222
|
const exitCode = await new Promise((resolvePromise) => {
|
|
215
223
|
// capture(MCP) 모드는 자식 출력을 파이프로 모아 싱크로 흘린다(도구 응답에 담기).
|
|
216
|
-
// CLI 경로(미지정)는 vitest 컬러 출력·리포터를 그대로 보이게 stdio 를 상속한다.
|
|
224
|
+
// CLI 사람 경로(미지정)는 vitest 컬러 출력·리포터를 그대로 보이게 stdio 를 상속한다.
|
|
217
225
|
const child = spawn(cmd, spawnArgs, {
|
|
218
226
|
cwd,
|
|
219
227
|
env: testEnv,
|
|
220
|
-
stdio: capture ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
|
228
|
+
stdio: capture || jsonPipe ? ['ignore', 'pipe', 'pipe'] : 'inherit',
|
|
221
229
|
});
|
|
222
230
|
if (capture) {
|
|
223
231
|
child.stdout?.on('data', (d) => opts.onOutput(d.toString('utf8')));
|
|
224
232
|
child.stderr?.on('data', (d) => opts.onOutput(d.toString('utf8')));
|
|
225
233
|
}
|
|
234
|
+
else if (jsonPipe) {
|
|
235
|
+
const emit = (stream) => (d) => writeOut(JSON.stringify({ kind: 'output', stream, chunk: d.toString('utf8') }) + '\n');
|
|
236
|
+
child.stdout?.on('data', emit('stdout'));
|
|
237
|
+
child.stderr?.on('data', emit('stderr'));
|
|
238
|
+
}
|
|
226
239
|
child.on('error', (err) => {
|
|
227
240
|
writeErr(` ✗ gaon test spawn 실패: ${String(err)}\n`);
|
|
228
241
|
resolvePromise(127);
|
package/dist/db/journal.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Kysely } from 'kysely';
|
|
2
|
+
import { type AdapterName } from '@gaonjs/data';
|
|
2
3
|
export type MigrationKind = 'file' | 'schema';
|
|
3
4
|
export interface JournalEntry {
|
|
4
5
|
readonly id: string;
|
|
@@ -17,7 +18,7 @@ export interface JournalEntry {
|
|
|
17
18
|
* pg 는 count 를 bigint(문자열)로, mysql 은 number 로 돌려주므로 `Number()` 로 통일한다
|
|
18
19
|
* (양 방언 공통 · 결정 268).
|
|
19
20
|
*/
|
|
20
|
-
export declare function journalExists(db: Kysely<any
|
|
21
|
+
export declare function journalExists(db: Kysely<any>, adapter?: AdapterName): Promise<boolean>;
|
|
21
22
|
/**
|
|
22
23
|
* _gaon_migrations 를 만든다(존재하면 no-op). id/kind/applied_at/db_key/
|
|
23
24
|
* statements/summary/down_sql — text·varchar·timestamp·integer 는 postgres·
|
|
@@ -35,7 +36,7 @@ export interface RecordArgs {
|
|
|
35
36
|
/** 이력 한 행을 남긴다. 호출자가 트랜잭션(trx)을 넘기면 그 안에서 실행된다. */
|
|
36
37
|
export declare function recordEntry(db: Kysely<any>, e: RecordArgs): Promise<void>;
|
|
37
38
|
/** 가장 최근 적용된 이력(롤백 대상). 원장 미존재 시 undefined. */
|
|
38
|
-
export declare function latestEntry(db: Kysely<any
|
|
39
|
+
export declare function latestEntry(db: Kysely<any>, adapter?: AdapterName): Promise<JournalEntry | undefined>;
|
|
39
40
|
/** kind='file' 로 적용된 파일명 집합. 원장 미존재 시 빈 집합. */
|
|
40
|
-
export declare function appliedFileMigrations(db: Kysely<any
|
|
41
|
+
export declare function appliedFileMigrations(db: Kysely<any>, adapter?: AdapterName): Promise<Set<string>>;
|
|
41
42
|
export declare function deleteEntry(db: Kysely<any>, id: string): Promise<void>;
|
package/dist/db/journal.js
CHANGED
|
@@ -17,12 +17,23 @@ import { MIGRATIONS_TABLE } from '@gaonjs/data';
|
|
|
17
17
|
* pg 는 count 를 bigint(문자열)로, mysql 은 number 로 돌려주므로 `Number()` 로 통일한다
|
|
18
18
|
* (양 방언 공통 · 결정 268).
|
|
19
19
|
*/
|
|
20
|
-
export async function journalExists(db) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
export async function journalExists(db, adapter = 'postgres') {
|
|
21
|
+
// 결정 326: mysql/mariadb 의 information_schema.tables 는 **서버 전체(모든 database)** 를
|
|
22
|
+
// 보므로 같은 서버 다른 DB 의 _gaon_migrations 에 오탐했다 — 존재 오판 뒤
|
|
23
|
+
// appliedFileMigrations 가 없는 테이블을 select 해 migrate/status 가 죽는다(공유 dev 서버
|
|
24
|
+
// 현실 시나리오) → database() 로 현재 DB 에 한정한다. pg 는 카탈로그가 database 단위라
|
|
25
|
+
// 종전대로 둔다(스키마('public') 한정은 커스텀 search_path 프로젝트를 깨서 안 한다).
|
|
26
|
+
const rows = adapter === 'mysql'
|
|
27
|
+
? await sql `
|
|
28
|
+
select count(*) as n
|
|
29
|
+
from information_schema.tables
|
|
30
|
+
where table_name = ${MIGRATIONS_TABLE} and table_schema = database()
|
|
31
|
+
`.execute(db)
|
|
32
|
+
: await sql `
|
|
33
|
+
select count(*) as n
|
|
34
|
+
from information_schema.tables
|
|
35
|
+
where table_name = ${MIGRATIONS_TABLE}
|
|
36
|
+
`.execute(db);
|
|
26
37
|
const first = rows.rows[0];
|
|
27
38
|
return first ? Number(first.n) > 0 : false;
|
|
28
39
|
}
|
|
@@ -53,8 +64,8 @@ export async function recordEntry(db, e) {
|
|
|
53
64
|
`.execute(db);
|
|
54
65
|
}
|
|
55
66
|
/** 가장 최근 적용된 이력(롤백 대상). 원장 미존재 시 undefined. */
|
|
56
|
-
export async function latestEntry(db) {
|
|
57
|
-
if (!(await journalExists(db)))
|
|
67
|
+
export async function latestEntry(db, adapter = 'postgres') {
|
|
68
|
+
if (!(await journalExists(db, adapter)))
|
|
58
69
|
return undefined;
|
|
59
70
|
const rows = await sql `
|
|
60
71
|
select id, kind, db_key, statements, summary, down_sql, applied_at
|
|
@@ -65,8 +76,8 @@ export async function latestEntry(db) {
|
|
|
65
76
|
return rows.rows[0];
|
|
66
77
|
}
|
|
67
78
|
/** kind='file' 로 적용된 파일명 집합. 원장 미존재 시 빈 집합. */
|
|
68
|
-
export async function appliedFileMigrations(db) {
|
|
69
|
-
if (!(await journalExists(db)))
|
|
79
|
+
export async function appliedFileMigrations(db, adapter = 'postgres') {
|
|
80
|
+
if (!(await journalExists(db, adapter)))
|
|
70
81
|
return new Set();
|
|
71
82
|
const rows = await sql `
|
|
72
83
|
select id from ${sql.ref(MIGRATIONS_TABLE)} where kind = 'file'
|
package/dist/db/migrate.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ConnectionConfig } from '@gaonjs/data';
|
|
1
|
+
import { type MigrationOp, type ConnectionConfig } from '@gaonjs/data';
|
|
2
2
|
import { listMigrationFiles } from './replay.js';
|
|
3
3
|
export interface DbMigrateOptions {
|
|
4
4
|
readonly cwd: string;
|
|
@@ -16,6 +16,8 @@ export interface DbMigrateResult {
|
|
|
16
16
|
readonly text: string;
|
|
17
17
|
readonly json: unknown;
|
|
18
18
|
}
|
|
19
|
+
/** drop 계열 부속 op 을 사람이 읽는 한 줄로 — 크게 알리는 note 용(status 도 재사용 · 결정 326). */
|
|
20
|
+
export declare function describeDeferred(op: MigrationOp): string;
|
|
19
21
|
/**
|
|
20
22
|
* `gaon db migrate` — 진짜 apply(합성형: replay → schema-diff). 실 DB 필요(§9).
|
|
21
23
|
*/
|
package/dist/db/migrate.js
CHANGED
|
@@ -17,8 +17,8 @@ import { computeMigration, renderUp, renderDown, summarizeOp, DEFERRED_DROP_KIND
|
|
|
17
17
|
import { resolveDbTarget } from './resolve.js';
|
|
18
18
|
import { ensureJournal, journalExists, recordEntry } from './journal.js';
|
|
19
19
|
import { listMigrationFiles, replayPending, rollbackLast } from './replay.js';
|
|
20
|
-
/** drop 계열 부속 op 을 사람이 읽는 한 줄로 — 크게 알리는 note
|
|
21
|
-
function describeDeferred(op) {
|
|
20
|
+
/** drop 계열 부속 op 을 사람이 읽는 한 줄로 — 크게 알리는 note 용(status 도 재사용 · 결정 326). */
|
|
21
|
+
export function describeDeferred(op) {
|
|
22
22
|
switch (op.kind) {
|
|
23
23
|
case 'dropUnique':
|
|
24
24
|
return `unique(${op.name}: ${op.columns.join(', ')})`;
|
|
@@ -116,7 +116,7 @@ export async function runDbMigrate(opts) {
|
|
|
116
116
|
};
|
|
117
117
|
}
|
|
118
118
|
// baseline 감지 — journal 이 없는데 createTable 이 포함 → 안내(§7.5.3).
|
|
119
|
-
const hadJournal = await journalExists(target.db);
|
|
119
|
+
const hadJournal = await journalExists(target.db, target.adapter);
|
|
120
120
|
const hasCreate = plan.ops.some((o) => o.kind === 'createTable');
|
|
121
121
|
const baselineNote = !hadJournal && hasCreate
|
|
122
122
|
? ` ℹ 최초 마이그레이션(_gaon_migrations 미존재) — 이 배치가 baseline 이 됩니다.\n` +
|
package/dist/db/replay.js
CHANGED
|
@@ -66,7 +66,7 @@ export async function replayPending(opts) {
|
|
|
66
66
|
const all = listMigrationFiles(opts.cwd);
|
|
67
67
|
if (all.length === 0)
|
|
68
68
|
return { applied: [], pending: [], all: [] };
|
|
69
|
-
const done = await appliedFileMigrations(opts.db);
|
|
69
|
+
const done = await appliedFileMigrations(opts.db, opts.adapter);
|
|
70
70
|
const pending = all.filter((f) => !done.has(f));
|
|
71
71
|
if (opts.dryRun || pending.length === 0) {
|
|
72
72
|
return { applied: [], pending, all };
|
|
@@ -104,7 +104,7 @@ export async function replayPending(opts) {
|
|
|
104
104
|
* 되돌린 뒤 이력 행을 지운다. --dry-run 은 대상만 보고 실행하지 않는다.
|
|
105
105
|
*/
|
|
106
106
|
export async function rollbackLast(opts) {
|
|
107
|
-
const entry = await latestEntry(opts.db);
|
|
107
|
+
const entry = await latestEntry(opts.db, opts.adapter);
|
|
108
108
|
if (!entry)
|
|
109
109
|
return { rolledBack: null, kind: null };
|
|
110
110
|
if (opts.dryRun)
|
package/dist/db/resolve.d.ts
CHANGED
|
@@ -33,4 +33,19 @@ export interface ResolvedDbTarget {
|
|
|
33
33
|
* 폴백(GAON_DATABASE_URL 관례) — 단일 커넥션 프로젝트의 종전 동작과 정합.
|
|
34
34
|
*/
|
|
35
35
|
export declare function listConnectionKeys(cwd: string, configPath?: string): Promise<string[]>;
|
|
36
|
+
/** SQL 키 / 문서형(mongodb) 키 분리 목록 — 전 커넥션 순회의 skip 판단용(결정 292). */
|
|
37
|
+
export interface ConnectionKeysByKind {
|
|
38
|
+
/** SQL(postgres·mysql) 커넥션 키 — diff/migrate/status/seed 순회 대상. */
|
|
39
|
+
readonly sql: string[];
|
|
40
|
+
/** 문서형(mongodb) 커넥션 키 — 마이그레이션 없음(§7.4) · 순회에서 건너뛴다. */
|
|
41
|
+
readonly mongo: string[];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 전 커넥션 순회(결정 139)용으로 커넥션 키를 SQL/문서형으로 갈라 돌려준다(결정 292).
|
|
45
|
+
* 문서형(mongodb)은 마이그레이션이 없어(§7.4) 순회에서 **건너뛰는** 대상이다 —
|
|
46
|
+
* 종전에는 순회가 mongo 키를 실패로 계상해 `gaon db migrate` 가 항상 exit 1 이었다.
|
|
47
|
+
* `--db <mongo키>` 명시 지정은 여전히 resolveDbTarget 이 fail-loud 로 안내한다.
|
|
48
|
+
* config 선언이 없으면(env-only) SQL ['main'] 폴백 — listConnectionKeys 와 정합.
|
|
49
|
+
*/
|
|
50
|
+
export declare function listConnectionKeysByKind(cwd: string, configPath?: string): Promise<ConnectionKeysByKind>;
|
|
36
51
|
export declare function resolveDbTarget(opts: ResolveDbOptions): Promise<ResolvedDbTarget>;
|
package/dist/db/resolve.js
CHANGED
|
@@ -97,6 +97,28 @@ export async function listConnectionKeys(cwd, configPath) {
|
|
|
97
97
|
const keys = Object.keys(config.db ?? {});
|
|
98
98
|
return keys.length ? keys : ['main'];
|
|
99
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* 전 커넥션 순회(결정 139)용으로 커넥션 키를 SQL/문서형으로 갈라 돌려준다(결정 292).
|
|
102
|
+
* 문서형(mongodb)은 마이그레이션이 없어(§7.4) 순회에서 **건너뛰는** 대상이다 —
|
|
103
|
+
* 종전에는 순회가 mongo 키를 실패로 계상해 `gaon db migrate` 가 항상 exit 1 이었다.
|
|
104
|
+
* `--db <mongo키>` 명시 지정은 여전히 resolveDbTarget 이 fail-loud 로 안내한다.
|
|
105
|
+
* config 선언이 없으면(env-only) SQL ['main'] 폴백 — listConnectionKeys 와 정합.
|
|
106
|
+
*/
|
|
107
|
+
export async function listConnectionKeysByKind(cwd, configPath) {
|
|
108
|
+
const { config } = await loadDbConfig(resolvePath(cwd), configPath);
|
|
109
|
+
const entries = Object.entries(config.db ?? {});
|
|
110
|
+
if (entries.length === 0)
|
|
111
|
+
return { sql: ['main'], mongo: [] };
|
|
112
|
+
const sql = [];
|
|
113
|
+
const mongo = [];
|
|
114
|
+
for (const [key, cfg] of entries) {
|
|
115
|
+
if (cfg?.adapter === 'mongodb')
|
|
116
|
+
mongo.push(key);
|
|
117
|
+
else
|
|
118
|
+
sql.push(key);
|
|
119
|
+
}
|
|
120
|
+
return { sql, mongo };
|
|
121
|
+
}
|
|
100
122
|
export async function resolveDbTarget(opts) {
|
|
101
123
|
const cwd = resolvePath(opts.cwd);
|
|
102
124
|
// 사용자가 --config <path> 를 준 경우 그 경로에서 default export 를 로드한다.
|
|
@@ -132,8 +154,8 @@ export async function resolveDbTarget(opts) {
|
|
|
132
154
|
// introspect)로 흘리지 않고 명확히 안내한다. 몽고 인덱스는 Mongoose 스키마 선언으로 관리한다.
|
|
133
155
|
if (connCfg.adapter === 'mongodb') {
|
|
134
156
|
throw new Error(`[gaon db] 커넥션 '${dbKey}' 은 문서형(mongodb)입니다 — 문서형은 마이그레이션이 없습니다(§7.4).\n` +
|
|
135
|
-
` → 몽고 인덱스는 Mongoose 스키마 선언(index:true / schema.index())으로 관리합니다(
|
|
136
|
-
` → gaon db diff/migrate 는 SQL 커넥션(postgres·mysql)에만
|
|
157
|
+
` → 몽고 인덱스는 Mongoose 스키마 선언(index:true / schema.index())으로 관리합니다(mongoose autoIndex 기본값이 부팅 시 보장).\n` +
|
|
158
|
+
` → gaon db diff/migrate 는 SQL 커넥션(postgres·mysql)에만 실행하세요(키 생략 시 순회가 문서형을 자동으로 건너뜁니다 · 결정 292).`);
|
|
137
159
|
}
|
|
138
160
|
const db = createDb(connCfg);
|
|
139
161
|
registerConnection(dbKey, db, connCfg.adapter);
|
package/dist/db/status.js
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
//
|
|
3
3
|
// db/migrations/*.ts 각각의 적용/미적용과, 스키마↔DB 사이에 아직 반영 안 된
|
|
4
4
|
// 변경(schema drift) 수를 함께 보여준다. 아무것도 실행하지 않는다.
|
|
5
|
-
import { computeMigration } from '@gaonjs/data';
|
|
5
|
+
import { computeMigration, summarizeOp, DEFERRED_DROP_KINDS } from '@gaonjs/data';
|
|
6
6
|
import { resolveDbTarget } from './resolve.js';
|
|
7
7
|
import { appliedFileMigrations } from './journal.js';
|
|
8
|
+
import { describeDeferred } from './migrate.js';
|
|
8
9
|
import { listMigrationFiles } from './replay.js';
|
|
9
10
|
export async function runDbStatus(opts) {
|
|
10
11
|
const target = await resolveDbTarget({
|
|
@@ -14,7 +15,7 @@ export async function runDbStatus(opts) {
|
|
|
14
15
|
});
|
|
15
16
|
try {
|
|
16
17
|
const files = listMigrationFiles(opts.cwd);
|
|
17
|
-
const done = await appliedFileMigrations(target.db);
|
|
18
|
+
const done = await appliedFileMigrations(target.db, target.adapter);
|
|
18
19
|
const migrations = files.map((f) => ({ file: `db/migrations/${f}`, applied: done.has(f) }));
|
|
19
20
|
const pendingFiles = migrations.filter((m) => !m.applied).length;
|
|
20
21
|
// migrate 는 스키마에 없는 테이블을 자동 DROP 하지 않으므로(결정 39 · §4.8)
|
|
@@ -23,7 +24,11 @@ export async function runDbStatus(opts) {
|
|
|
23
24
|
const extraTables = plan.ops
|
|
24
25
|
.filter((o) => o.kind === 'dropTable')
|
|
25
26
|
.map((o) => o.name);
|
|
26
|
-
|
|
27
|
+
// 결정 326: deferred aux drop(제약·인덱스·기본값 제거)도 migrate 가 자동 적용하지
|
|
28
|
+
// 않는다(결정 220) — drift 로 세면 "'gaon db migrate' 로 반영하세요" 가 영원히 안
|
|
29
|
+
// 지워지는 거짓 안내가 된다. dropTable 과 같은 축의 참고 항목으로 분리한다.
|
|
30
|
+
const deferredDrops = plan.ops.filter((o) => o.kind !== 'dropTable' && DEFERRED_DROP_KINDS.has(o.kind));
|
|
31
|
+
const schemaChanges = plan.ops.length - extraTables.length - deferredDrops.length;
|
|
27
32
|
const lines = [];
|
|
28
33
|
if (migrations.length === 0) {
|
|
29
34
|
lines.push(` [${opts.dbKey}] 마이그레이션 파일 없음 (db/migrations/).`);
|
|
@@ -42,6 +47,10 @@ export async function runDbStatus(opts) {
|
|
|
42
47
|
if (extraTables.length > 0) {
|
|
43
48
|
lines.push(` 참고: 스키마에 없는 테이블 ${extraTables.length}개(자동 DROP 안 함): ${extraTables.join(', ')}`);
|
|
44
49
|
}
|
|
50
|
+
if (deferredDrops.length > 0) {
|
|
51
|
+
lines.push(` 참고: 스키마에서 사라진 제약·인덱스·기본값 ${deferredDrops.length}개(자동 제거 안 함 — ` +
|
|
52
|
+
`'gaon db diff' 의 down SQL 로 손작성 마이그): ${deferredDrops.map(describeDeferred).join(', ')}`);
|
|
53
|
+
}
|
|
45
54
|
return {
|
|
46
55
|
exitCode: 0,
|
|
47
56
|
text: lines.join('\n'),
|
|
@@ -52,6 +61,7 @@ export async function runDbStatus(opts) {
|
|
|
52
61
|
pendingFiles,
|
|
53
62
|
schemaChanges,
|
|
54
63
|
extraTables,
|
|
64
|
+
deferredDrops: deferredDrops.map(summarizeOp),
|
|
55
65
|
},
|
|
56
66
|
};
|
|
57
67
|
}
|
package/dist/dev.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export interface DevLayout {
|
|
|
13
13
|
readonly apps: readonly DevApp[];
|
|
14
14
|
/** locales/ (없으면 undefined — messages 축 생략 · 결정 158 · W2). */
|
|
15
15
|
readonly localesDir?: string;
|
|
16
|
+
/** 기준 로케일 = config i18n.fallbackLng (정적 분석 · 결정 352 · 못 읽으면 undefined). */
|
|
17
|
+
readonly messagesBaseLng?: string;
|
|
16
18
|
/** .gaon/messages.d.ts */
|
|
17
19
|
readonly messagesOut: string;
|
|
18
20
|
/** 프론트 진입(index.html)이 있는 앱이 하나라도 있는가 — env 축 적용 조건(결정 198). */
|
|
@@ -26,8 +28,8 @@ export interface DevDeps {
|
|
|
26
28
|
readonly layout: DevLayout;
|
|
27
29
|
regenerateTables(schemaDir: string, out: string): Promise<unknown>;
|
|
28
30
|
regenerateRoutes(appDir: string, out: string): Promise<unknown>;
|
|
29
|
-
/** locales/ → .gaon/messages.d.ts (결정 158 · W2). i18n 축을 쓰는 호출자만 준다. */
|
|
30
|
-
regenerateMessages?(localesDir: string, out: string): unknown;
|
|
31
|
+
/** locales/ → .gaon/messages.d.ts (결정 158 · W2 · 기준 로케일 = 결정 352). i18n 축을 쓰는 호출자만 준다. */
|
|
32
|
+
regenerateMessages?(localesDir: string, out: string, baseLng?: string): unknown;
|
|
31
33
|
/** `.env` → .gaon/env.d.ts (결정 198 · F-9 ②). env 축을 쓰는 호출자만 준다. */
|
|
32
34
|
regenerateEnv?(envFile: string, out: string): unknown;
|
|
33
35
|
watch(dir: string, opts: WatchOptions): WatchHandle;
|
|
@@ -58,8 +60,8 @@ export declare function startDev(deps: DevDeps): Promise<DevHandle>;
|
|
|
58
60
|
export interface RegenDeps {
|
|
59
61
|
regenerateTables(schemaDir: string, out: string): Promise<unknown>;
|
|
60
62
|
regenerateRoutes(appDir: string, out: string): Promise<unknown>;
|
|
61
|
-
/** locales/ → .gaon/messages.d.ts (결정 158 · W2). i18n 축을 쓰는 호출자만 준다. */
|
|
62
|
-
regenerateMessages?(localesDir: string, out: string): unknown;
|
|
63
|
+
/** locales/ → .gaon/messages.d.ts (결정 158 · W2 · 기준 로케일 = 결정 352). i18n 축을 쓰는 호출자만 준다. */
|
|
64
|
+
regenerateMessages?(localesDir: string, out: string, baseLng?: string): unknown;
|
|
63
65
|
/** `.env` → .gaon/env.d.ts (결정 198 · F-9 ②). env 축을 쓰는 호출자만 준다. */
|
|
64
66
|
regenerateEnv?(envFile: string, out: string): unknown;
|
|
65
67
|
}
|
package/dist/dev.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { readdirSync, existsSync } from 'node:fs';
|
|
15
15
|
import { join, resolve } from 'node:path';
|
|
16
|
+
import { analyzeProjectI18n } from './i18n-config.js';
|
|
16
17
|
/** locales/*.json 변경(메시지 카탈로그) — messages.d.ts 재생성 트리거. */
|
|
17
18
|
const isMessagesChange = (f) => !f.includes('.gaon') && f.endsWith('.json');
|
|
18
19
|
const isSchemaChange = (f) => f.endsWith('.ts') && !f.endsWith('.d.ts') && !f.endsWith('.test.ts') && !f.includes('.gaon');
|
|
@@ -39,7 +40,7 @@ export async function startDev(deps) {
|
|
|
39
40
|
}
|
|
40
41
|
// 결정 158(W2): locales/ → messages.d.ts.
|
|
41
42
|
if (layout.localesDir && deps.regenerateMessages) {
|
|
42
|
-
await deps.regenerateMessages(layout.localesDir, layout.messagesOut);
|
|
43
|
+
await deps.regenerateMessages(layout.localesDir, layout.messagesOut, layout.messagesBaseLng);
|
|
43
44
|
deps.log({ kind: 'regen', target: 'messages' });
|
|
44
45
|
}
|
|
45
46
|
// 결정 198(F-9 ②): 프론트 앱이 있으면 .env → env.d.ts(VITE_* 타입 브리지). `.env` 부재는
|
|
@@ -79,7 +80,7 @@ export async function startDev(deps) {
|
|
|
79
80
|
handles.push(deps.watch(localesDir, {
|
|
80
81
|
filter: isMessagesChange,
|
|
81
82
|
onChange: async () => {
|
|
82
|
-
await regenerateMessages(localesDir, layout.messagesOut);
|
|
83
|
+
await regenerateMessages(localesDir, layout.messagesOut, layout.messagesBaseLng);
|
|
83
84
|
deps.log({ kind: 'regen', target: 'messages' });
|
|
84
85
|
},
|
|
85
86
|
onError: deps.onError,
|
|
@@ -110,7 +111,7 @@ export async function regenerateGaonOnce(layout, deps) {
|
|
|
110
111
|
// 결정 158(W2): locales/ 가 있으면 messages.d.ts 도 재생성한다(i18n 키 타입 브리지).
|
|
111
112
|
let messages = false;
|
|
112
113
|
if (layout.localesDir && deps.regenerateMessages) {
|
|
113
|
-
messages = (await deps.regenerateMessages(layout.localesDir, layout.messagesOut)) === true;
|
|
114
|
+
messages = (await deps.regenerateMessages(layout.localesDir, layout.messagesOut, layout.messagesBaseLng)) === true;
|
|
114
115
|
}
|
|
115
116
|
// 결정 198(F-9 ②): 프론트 앱이 있으면 .env → env.d.ts. `.env` 부재는 throw(수리 안내).
|
|
116
117
|
let env = false;
|
|
@@ -144,12 +145,16 @@ export function resolveDevLayout(cwd) {
|
|
|
144
145
|
});
|
|
145
146
|
}
|
|
146
147
|
}
|
|
147
|
-
|
|
148
|
+
// 결정 352: 카탈로그 위치·기준 로케일은 gaon.config.ts 의 i18n 블록을 따른다
|
|
149
|
+
// (정적 분석 · 하드코딩 'locales' 는 dir 커스텀 프로젝트에서 타입 축을 무소음으로 껐다).
|
|
150
|
+
const i18nCfg = analyzeProjectI18n(root);
|
|
151
|
+
const localesDirPath = join(root, i18nCfg.dir ?? 'locales');
|
|
148
152
|
return {
|
|
149
153
|
schemaDir: existsSync(schemaDirPath) ? schemaDirPath : undefined,
|
|
150
154
|
tablesOut: join(root, '.gaon', 'tables.d.ts'),
|
|
151
155
|
apps: apps.sort((a, b) => a.name.localeCompare(b.name)),
|
|
152
156
|
localesDir: existsSync(localesDirPath) ? localesDirPath : undefined,
|
|
157
|
+
messagesBaseLng: i18nCfg.fallbackLng,
|
|
153
158
|
messagesOut: join(root, '.gaon', 'messages.d.ts'),
|
|
154
159
|
hasFrontendApps,
|
|
155
160
|
envFile: join(root, '.env'),
|
|
@@ -14,7 +14,7 @@ export declare const FIXERS: Partial<Record<DoctorRule, Fixer>>;
|
|
|
14
14
|
* 규칙별 fix 지원 여부 카탈로그. 리포트가 사용자에게 무엇이 자동 · 무엇이
|
|
15
15
|
* 수동 · 이유는 무엇인지 표시하는 데 쓴다(진단 = 수리 안내서 · §7.5.3).
|
|
16
16
|
*
|
|
17
|
-
* **정직성 규약(결정 241)**: 이 배열은 `ALL_RULES`
|
|
17
|
+
* **정직성 규약(결정 241)**: 이 배열은 `ALL_RULES` 28종을 **빠짐없이** 담는다 —
|
|
18
18
|
* fixer 가 없는 규칙도 `hasFixer:false` + 구체적 수동 안내로 명시한다. 항목이
|
|
19
19
|
* 빠지면 --fix 리포트가 그 규칙 위반에 대해 일반 문구("수동 수정 필요")만 내
|
|
20
20
|
* 사용자가 왜 자동이 안 되는지 알 수 없다. 전수성은 테스트가 고정한다
|
|
@@ -28,7 +28,7 @@ export const FIXERS = {
|
|
|
28
28
|
* 규칙별 fix 지원 여부 카탈로그. 리포트가 사용자에게 무엇이 자동 · 무엇이
|
|
29
29
|
* 수동 · 이유는 무엇인지 표시하는 데 쓴다(진단 = 수리 안내서 · §7.5.3).
|
|
30
30
|
*
|
|
31
|
-
* **정직성 규약(결정 241)**: 이 배열은 `ALL_RULES`
|
|
31
|
+
* **정직성 규약(결정 241)**: 이 배열은 `ALL_RULES` 28종을 **빠짐없이** 담는다 —
|
|
32
32
|
* fixer 가 없는 규칙도 `hasFixer:false` + 구체적 수동 안내로 명시한다. 항목이
|
|
33
33
|
* 빠지면 --fix 리포트가 그 규칙 위반에 대해 일반 문구("수동 수정 필요")만 내
|
|
34
34
|
* 사용자가 왜 자동이 안 되는지 알 수 없다. 전수성은 테스트가 고정한다
|
|
@@ -165,6 +165,11 @@ export const FIXER_CAPABILITIES = [
|
|
|
165
165
|
hasFixer: false,
|
|
166
166
|
note: '수동 · .vue 의 import.meta.env(TS1470)는 env 접근자로 전환하세요 — 접근자 도입은 코드 편집이 필요합니다(결정 198).',
|
|
167
167
|
},
|
|
168
|
+
{
|
|
169
|
+
rule: 'render-return',
|
|
170
|
+
hasFixer: false,
|
|
171
|
+
note: '수동 · this.render/redirect/json 호출 앞에 return 을 붙이세요 — 반환값이 곧 응답입니다(결정 340 · 의도된 204 는 응답 호출 없이 return).',
|
|
172
|
+
},
|
|
168
173
|
{
|
|
169
174
|
rule: 'locale-parity',
|
|
170
175
|
hasFixer: false,
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { existsSync } from 'node:fs';
|
|
12
12
|
import { join, relative } from 'node:path';
|
|
13
13
|
import { loadLocales, flattenKeys } from '@gaonjs/i18n';
|
|
14
|
+
import { analyzeProjectI18n } from '../i18n-config.js';
|
|
14
15
|
// 경고 한 줄이 폭주하지 않게 나열 상한 — 넘으면 "…외 N개" 로 접는다(detail 에는 전량).
|
|
15
16
|
const MAX_KEYS_SHOWN = 20;
|
|
16
17
|
// i18next 복수형 접미사(결정 181 · generator.ts 와 동일 규약). 로케일마다 필요한 접미사만
|
|
@@ -26,7 +27,9 @@ function pluralBase(key) {
|
|
|
26
27
|
* 비교 대상이 없어 건너뛴다(i18n 미사용·단일 로케일 프로젝트는 무소음).
|
|
27
28
|
*/
|
|
28
29
|
export async function checkLocaleParity(cwd) {
|
|
29
|
-
|
|
30
|
+
// 결정 352: 카탈로그 위치는 config i18n.dir 을 따른다(정적 분석 · 하드코딩이던
|
|
31
|
+
// 'locales' 는 dir 커스텀 프로젝트에서 이 검사를 무소음으로 껐다).
|
|
32
|
+
const localesDir = join(cwd, analyzeProjectI18n(cwd).dir ?? 'locales');
|
|
30
33
|
if (!existsSync(localesDir))
|
|
31
34
|
return { rule: 'locale-parity', issues: [] };
|
|
32
35
|
const resources = loadLocales(localesDir);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RuleReport } from './types.js';
|
|
2
|
-
/** 소스에 pageProps() 구조분해가 있는지(단위 테스트 진입점 · 주석은 제외). */
|
|
2
|
+
/** 소스에 pageProps()/useShared() 구조분해가 있는지(단위 테스트 진입점 · 주석은 제외). */
|
|
3
3
|
export declare function usesPagePropsDestructure(source: string): boolean;
|
|
4
|
-
/** apps/ 의 .vue 를 훑어 pageProps() 구조분해를 경고로 낸다. */
|
|
4
|
+
/** apps/ 의 .vue + .ts(컴포저블 등)를 훑어 pageProps()/useShared() 구조분해를 경고로 낸다. */
|
|
5
5
|
export declare function checkPagePropsDestructure(cwd: string): Promise<RuleReport>;
|