@gaonjs/cli 0.36.0 → 0.38.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 +0 -9
- package/dist/commands/check.js +6 -31
- package/dist/commands/dev.d.ts +14 -3
- package/dist/commands/dev.js +128 -10
- package/dist/commands/gen.d.ts +2 -0
- package/dist/commands/gen.js +6 -2
- package/dist/commands/new.js +7 -0
- package/dist/commands/test.d.ts +2 -1
- package/dist/commands/test.js +11 -5
- package/dist/dev/console.d.ts +1 -1
- package/dist/dev/console.js +2 -0
- package/dist/dev.d.ts +13 -1
- package/dist/dev.js +21 -1
- package/dist/doctor/no-import-meta-env.d.ts +5 -0
- package/dist/doctor/no-import-meta-env.js +98 -0
- package/dist/doctor/types.d.ts +1 -1
- package/dist/doctor.d.ts +2 -2
- package/dist/doctor.js +7 -3
- package/dist/env-gen.d.ts +12 -0
- package/dist/env-gen.js +63 -0
- package/dist/index.js +9 -6
- package/dist/pm.d.ts +13 -0
- package/dist/pm.js +59 -0
- package/dist/templates/project/.env.example.tpl +6 -0
- package/dist/templates/project/AGENTS.md.tpl +5 -4
- package/dist/templates/project/agents/async.md.tpl +64 -5
- package/dist/templates/project/agents/frontend.md.tpl +52 -1
- package/dist/templates/project/agents/i18n.md.tpl +26 -0
- package/dist/templates/project/agents/mail.md.tpl +31 -0
- package/dist/templates/project/agents/realtime.md.tpl +10 -0
- package/dist/templates/project/agents/testing.md.tpl +8 -4
- package/dist/templates/project/agents/web.md.tpl +36 -0
- package/dist/work.js +4 -1
- package/package.json +9 -9
|
@@ -32,6 +32,29 @@ t('nav.home') // 중첩은 점 표기
|
|
|
32
32
|
| 지원 언어 | `languages(): string[]` | 설정된 supportedLngs |
|
|
33
33
|
| 고정 번역 | `runWithLanguage(lng, fn)` | fn 안의 t() 가 그 언어(메일·비요청 경로 · §mail) |
|
|
34
34
|
|
|
35
|
+
### 1.5 복수형 — `count` 로 자동 선택 (i18next 규약 · 결정 181)
|
|
36
|
+
|
|
37
|
+
복수형은 카탈로그에 **접미사 키**(`<키>_one`·`<키>_other`)를 두고 `t('<키>', { count })`
|
|
38
|
+
로 부른다 — i18next 가 `count` 와 로케일의 CLDR 규칙으로 알맞은 접미사를 고른다. 타입은
|
|
39
|
+
**base 키**(`<키>`)로 검사한다(생성기가 접미사 키에서 base 키를 함께 노출 · 결정 181).
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
// locales/en.json — 영어는 단수/복수 구분(_one·_other)
|
|
43
|
+
{ "cart": { "items_one": "{{count}} item", "items_other": "{{count}} items" } }
|
|
44
|
+
// locales/ko.json — 한국어는 복수 구분 없음(_other 만)
|
|
45
|
+
{ "cart": { "items_other": "상품 {{count}}개" } }
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
t('cart.items', { count: 1 }) // en → "1 item" · ko → "상품 1개"
|
|
50
|
+
t('cart.items', { count: 5 }) // en → "5 items" · ko → "상품 5개"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
- **base 키로 부른다** — `t('cart.items', { count })`. `t('cart.items_one')` 처럼 접미사를
|
|
54
|
+
직접 부르면 복수 선택이 안 된다(안티패턴).
|
|
55
|
+
- 로케일마다 필요한 접미사만 둔다(영어 `_one`·`_other` / 한국어·일본어 `_other`). 기준
|
|
56
|
+
로케일(§4)에 있는 키가 타입 유니온이 되므로 **기준 로케일 카탈로그에 복수형 키를 둔다**.
|
|
57
|
+
|
|
35
58
|
### 2. 요청별 로케일 자동 협상 (결정 159)
|
|
36
59
|
|
|
37
60
|
`gaon.config.ts` 에 `i18n` 설정이 있으면 `gaon serve`/`gaon dev` 가 **매 요청**
|
|
@@ -97,6 +120,9 @@ export function greetLine(name: string): string {
|
|
|
97
120
|
전환은 `this.setLocale`, 특정 로케일 강제는 `runWithLanguage`.
|
|
98
121
|
- **메일은 요청 로케일이 아니라 수신자 로케일** — `deliver(data, { locale })` 로
|
|
99
122
|
명시한다(`agents/mail.md` · 결정 160).
|
|
123
|
+
- **검증 실패 문안도 로케일화된다** — 예약 namespace `validation.<code>`(예 `validation.required`)
|
|
124
|
+
를 `locales/` 에 넣으면 필드별 사유가 요청 로케일로 번역된다(`agents/web.md` §4.1 · 결정 183).
|
|
125
|
+
프레임웍은 코드만 노출하고 번역은 앱 몫이다(미제공 시 내장 fallback).
|
|
100
126
|
|
|
101
127
|
## 관련 결정 번호
|
|
102
128
|
|
|
@@ -57,6 +57,37 @@ await WelcomeMail.deliver(data, { locale: 'ja', to: 'ops@example.com' })
|
|
|
57
57
|
·`.env.example` 에 mail 블록이 이미 있어(결정 160) `cp .env.example .env` 후 바로 돈다.
|
|
58
58
|
운영은 `SMTP_HOST`·자격증명·`SMTP_SECURE=true` 로 교체(같은 코드).
|
|
59
59
|
|
|
60
|
+
### 5. `gaon.config.ts` 의 `mail` 블록 (env-gated)
|
|
61
|
+
|
|
62
|
+
메일러는 루트 `gaon.config.ts` 의 `mail` 블록으로 배선된다 — `db`·`redis`·`nats`
|
|
63
|
+
동형으로 **env-gated**(SMTP_HOST 없으면 미배선). 스캐폴드(`gaon new`)가 아래 블록을
|
|
64
|
+
이미 넣어 둔다(결정 160). 발신자 기본값은 **`defaultFrom`**(메시지 `from` 아님 · `from`
|
|
65
|
+
은 개별 메시지 override).
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
// gaon.config.ts
|
|
69
|
+
export default defineConfig({
|
|
70
|
+
mail: process.env.SMTP_HOST
|
|
71
|
+
? {
|
|
72
|
+
host: process.env.SMTP_HOST,
|
|
73
|
+
port: process.env.SMTP_PORT ? Number(process.env.SMTP_PORT) : 1025,
|
|
74
|
+
secure: process.env.SMTP_SECURE === 'true', // 운영 TLS
|
|
75
|
+
user: process.env.SMTP_USER, // 선택(인증 SMTP)
|
|
76
|
+
pass: process.env.SMTP_PASS, // 선택
|
|
77
|
+
defaultFrom: process.env.MAIL_FROM, // 발신자 기본값(from 없는 메시지)
|
|
78
|
+
}
|
|
79
|
+
: undefined,
|
|
80
|
+
})
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
| 필드 | 타입 | 비고 |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| `host` | `string` | SMTP 호스트(dev = MailPit `127.0.0.1`) |
|
|
86
|
+
| `port` | `number` | SMTP 포트(dev MailPit = 1025) |
|
|
87
|
+
| `secure?` | `boolean` | TLS(운영). 생략 = false |
|
|
88
|
+
| `user?`·`pass?` | `string` | 인증 SMTP 자격증명(생략 가능) |
|
|
89
|
+
| `defaultFrom` | `string` | 발신자 기본값 — 메시지에 `from` 이 없을 때. `configureMailer({ defaultFrom })` 와 동치 |
|
|
90
|
+
|
|
60
91
|
## 정본 예시
|
|
61
92
|
|
|
62
93
|
```ts
|
|
@@ -186,6 +186,15 @@ export function useRoom(roomId: number) {
|
|
|
186
186
|
- 허브가 죽었다 재시작하면 같은 포트로 재bind 하고 KV 에서 상태를
|
|
187
187
|
복원한다.
|
|
188
188
|
- ping 무활동 타임아웃은 네트워크 파티션 백스톱이다.
|
|
189
|
+
- **fail-fast — 리더가 됐는데 포트를 못 잡으면 즉시 종료한다(결정 207).**
|
|
190
|
+
리스는 얻었으나 TCP 포트 bind 에 실패하면(포트를 다른 허브·orphan·
|
|
191
|
+
오설정이 점유) 허브는 **좀비 리더**(리스 보유·프레즌스 서빙 불가)가 되지
|
|
192
|
+
않도록 리스를 사임하고 `process.exit(1)` 한다. 조용히 프레즌스가 죽는
|
|
193
|
+
대신 명확히 종료해 systemd/pm2 가 재시작하게 하고, 대기 인스턴스가 즉시
|
|
194
|
+
승계한다. **운영 함정**: 한 호스트에 허브를 여러 개 띄우면 포트가
|
|
195
|
+
겹친다 — 인스턴스마다 다른 `GAON_HUB_PORT` 를 주거나 호스트를 분리한다.
|
|
196
|
+
`gaon dev` 는 내장 허브를 기본 포트로 띄우므로, 같은 호스트에서 별도
|
|
197
|
+
`gaon hub` 를 돌릴 땐 포트를 바꾼다.
|
|
189
198
|
|
|
190
199
|
## 정본 예시
|
|
191
200
|
|
|
@@ -255,6 +264,7 @@ export default channel({
|
|
|
255
264
|
| §7 (v0.15) | 실시간 v1 포함 — 채널·프레즌스·허브 · KV 영속 · 리스 리더 선출 HA |
|
|
256
265
|
| 결정 126 | 서버 개시 `broadcast(name, data)`(`gaonjs/async`) — 컨트롤러·서비스·잡에서 클라 메시지 없이 채널 발화 · authorize 재실행 없음 · seal 재봉인 자동 |
|
|
257
266
|
| 결정 154 | `useChannel` 앱 프리픽스 자동 주입 — `import.meta.env.BASE_URL`(vite base·에셋 base 단일 소스) 로 `<프리픽스>/gaon/ws/<name>` · `opts.path` 는 탈출구 · 프리픽스 앱 실시간 무한 재연결 제거(§4) |
|
|
267
|
+
| 결정 207 | 허브 fail-fast(§5) — 리스는 얻고 TCP 포트 bind 실패 시 좀비 리더 대신 리스 사임 + `process.exit(1)`(F-13 fix · `onFatal` 훅으로 주입 가능) |
|
|
258
268
|
|
|
259
269
|
## `@gaonjs/seal` 켠 앱의 채널
|
|
260
270
|
|
|
@@ -24,10 +24,13 @@
|
|
|
24
24
|
매 테스트 뒤 전 테이블을 비운다(`truncateAll`). 아래 §5 참고.
|
|
25
25
|
- `gaon test` 가 잡·이벤트 NATS 스트림도 **자동 격리**한다(결정 130) —
|
|
26
26
|
테스트 프로세스에 `GAON_STREAM_PREFIX` 를 주입해 스트림·subject 가
|
|
27
|
-
`GAON_TEST_JOBS`·`test.gaon.jobs.>` 로 갈린다.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
`GAON_TEST_JOBS`·`test.gaon.jobs.>` 로 갈린다. 같은 접두가 **NATS KV 버킷**
|
|
28
|
+
(스케줄러 리스 `gaon_lease`·프레즌스·허브)에도 적용돼(결정 203) `test_gaon_lease`
|
|
29
|
+
처럼 격리된다 — 스트림뿐 아니라 KV 도 개발·운영 스택과 안 겹친다. 그래서
|
|
30
|
+
개발용 `gaon work` 가 떠 있어도 테스트 잡을 훔치지 않고 스케줄러 리더 경합도
|
|
31
|
+
안 생기며, 테스트가 남긴 잡을 개발 워커가 처리하지도 않는다 — **테스트 전에
|
|
32
|
+
워커를 내릴 필요가 없다.** (직접 `vitest` 로 돌리면 이 격리가 없어 개발 스택과
|
|
33
|
+
섞이니 `gaon test` 를 쓴다.)
|
|
31
34
|
- SQLite 는 Docker 가 불가능한 환경의 폴백으로만 남고 공식 경로가
|
|
32
35
|
아니다.
|
|
33
36
|
|
|
@@ -256,5 +259,6 @@ hidden 값은 **서버 코드에서는 여전히 읽힌다**(직렬화 경계에
|
|
|
256
259
|
| 결정 122 | 직렬화 경계 테스트는 관계 경유 hidden 을 반드시 포함(재귀 no-leak 단언) |
|
|
257
260
|
| 결정 111 | `gaon test` 테스트 DB 자동 준비 + `connectTestDatabase`·`truncateAll` 격리(truncate · service COMMIT 실측) |
|
|
258
261
|
| 결정 130 | `gaon test` 가 NATS 스트림도 자동 격리(`GAON_STREAM_PREFIX` → `GAON_TEST_JOBS`·`test.gaon.jobs.>`) — 개발 워커 병행 시 잡 누출 방지(규칙 10 이행) |
|
|
262
|
+
| 결정 203 | 같은 접두를 **NATS KV 버킷**에도 적용(`gaon_lease`·프레즌스·허브 → `test_gaon_lease` 등) — 스트림만 격리하던 결정 130 의 빈틈(KV 미격리)을 메움. 스케줄러 리스 경합·크로스-프리픽스 KV 누출 방지 |
|
|
259
263
|
| §9 (v0.15) | 실 인프라 필수 · 목업/인메모리 금지 |
|
|
260
264
|
| 결정 32 | 잡 발행 위치 자유 — publish 함수가 서비스 경유여도 검증 대상 |
|
|
@@ -239,6 +239,35 @@ async create() {
|
|
|
239
239
|
|
|
240
240
|
순수 JSON/API 앱(X-Inertia 아님·세션 없음)은 기존대로 **422 JSON** 을 받는다.
|
|
241
241
|
|
|
242
|
+
#### 검증 사유 로케일화 — 예약 namespace `validation.*` (결정 183)
|
|
243
|
+
|
|
244
|
+
검증 실패의 **필드별 사유**(`form.errors.<필드>` 로 엔드유저에 노출되는 부분)는 요청
|
|
245
|
+
로케일로 번역된다 — `i18n` 이 설정돼 있고 앱이 `locales/<lng>.json` 의 **예약 namespace
|
|
246
|
+
`validation.<code>`** 로 번역을 제공하면. 프레임웍은 **안정적 코드만** 노출하고 번역은
|
|
247
|
+
앱 몫이다(The One Way: 코드는 프레임웍, 문안은 앱). 키가 없으면 내장 fallback(한국어)로
|
|
248
|
+
떨어져 거동이 보존된다. i18n 미설정이면 항상 fallback.
|
|
249
|
+
|
|
250
|
+
| code | 파라미터 | 언제 |
|
|
251
|
+
|---|---|---|
|
|
252
|
+
| `required` | — | 필수 컬럼 누락 |
|
|
253
|
+
| `too_long` | `{ max, len }` | `.max(n)` 초과 |
|
|
254
|
+
| `not_allowed` | `{ value, allowed }` | enum 밖 값 |
|
|
255
|
+
| `not_integer` | `{ value }` | bigint 변환 실패 |
|
|
256
|
+
| `not_boolean` | `{ value }` | boolean 변환 실패 |
|
|
257
|
+
| `not_date` | `{ value }` | 날짜 변환 실패 |
|
|
258
|
+
|
|
259
|
+
```json
|
|
260
|
+
// locales/en.json — 앱이 검증 문안을 로케일별로 준다(i18next {{max}} 보간).
|
|
261
|
+
{ "validation": {
|
|
262
|
+
"required": "This field is required.",
|
|
263
|
+
"too_long": "At most {{max}} characters (got {{len}})."
|
|
264
|
+
} }
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
주의: 상단 개발자용 422 `message`(스키마 어느 파일을 고치라는 §7.5.3 **수리 안내**)는
|
|
268
|
+
로케일화하지 않는다 — AI/개발자용 고정 안내다. 로케일화 대상은 **엔드유저에 닿는 필드별
|
|
269
|
+
사유**뿐이다.
|
|
270
|
+
|
|
242
271
|
#### 세션/CSRF 실패·415 도 코어가 Inertia-네이티브로 마감한다 (결정 165)
|
|
243
272
|
|
|
244
273
|
폼 검증(결정 109)과 **대칭**으로, 컨트롤러 액션 밖(디스패처 try-catch 밖)에서 나는
|
|
@@ -423,6 +452,13 @@ const ok = await verifyPassword(plain, user.passwordDigest) // Promise<boolean>
|
|
|
423
452
|
|
|
424
453
|
API 앱(JWT)은 세션 대신 `this.jwt.issue(user)` / `this.jwt.refresh(token)` 를 쓴다.
|
|
425
454
|
|
|
455
|
+
- **API 앱은 프론트엔드가 없다(JSON 전용).** `apps/<app>/app.config.ts`(`auth: { strategy:'jwt', … }`)
|
|
456
|
+
+ `routes.ts` + `controllers/` 만 두면 된다 — `index.html`·`main.ts`·`pages/` 는 만들지 않는다.
|
|
457
|
+
앱 발견은 `routes.ts` 기준이라 `gaon serve` 가 이 앱을 `/<app>` 프리픽스로 정상 마운트하고,
|
|
458
|
+
`gaon build`·`gaon check` 는 프론트 앱(=`index.html` 보유)만 검사하므로 API 앱을 프론트로
|
|
459
|
+
오검사하지 않는다. JWT 앱의 표준 형태다("프론트엔드는 프로젝트당 하나" 규칙은 Vue 앱 한정 ·
|
|
460
|
+
API 앱은 그 규칙 밖).
|
|
461
|
+
|
|
426
462
|
- **`this.auth.user`/`requireAuth()` 사용자 타입은 앱이 증강한다** (결정 58).
|
|
427
463
|
`GaonCurrentUser` 는 빈 인터페이스라 증강 없이는 `user.id` 접근이 타입에러다.
|
|
428
464
|
`gaon g auth` 가 `apps/<app>/auth.ts` 에 심는다:
|
package/dist/work.js
CHANGED
|
@@ -104,7 +104,10 @@ export async function runWorkCommand(opts = {}) {
|
|
|
104
104
|
id,
|
|
105
105
|
db,
|
|
106
106
|
schedule: domain.schedule,
|
|
107
|
-
|
|
107
|
+
// 큐 기본 동시성: 옵션 > GAON_WORKER_CONCURRENCY > 잡별 선언 > 1. 잡에
|
|
108
|
+
// 명시 concurrency 가 있으면 그게 우선(worker.ts perQueueConc). 운영은
|
|
109
|
+
// 이 env 로 워커 처리량을 조절하고, gaon dev 는 개발 편의로 기본 상향한다.
|
|
110
|
+
concurrency: opts.concurrency ?? envInt('GAON_WORKER_CONCURRENCY'),
|
|
108
111
|
ackWaitMs: opts.ackWaitMs ?? envInt('GAON_WORKER_ACK_WAIT_MS'),
|
|
109
112
|
drainTimeoutMs: opts.drainTimeoutMs ?? envInt('GAON_WORKER_DRAIN_MS'),
|
|
110
113
|
onEvent: emit,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gaonjs/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Gaon CLI
|
|
3
|
+
"version": "0.38.0",
|
|
4
|
+
"description": "Gaon CLI — 스캐폴딩·제너레이터·마이그레이션·dev/serve/work/hub·doctor·check (bin: gaon)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"homepage": "https://gaonjs.dev",
|
|
@@ -27,13 +27,13 @@
|
|
|
27
27
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
28
28
|
"typescript": "^5.9.0",
|
|
29
29
|
"vite": "^7.0.0",
|
|
30
|
-
"@gaonjs/async": "0.
|
|
31
|
-
"@gaonjs/
|
|
32
|
-
"@gaonjs/
|
|
33
|
-
"@gaonjs/
|
|
34
|
-
"@gaonjs/i18n": "0.2.
|
|
35
|
-
"@gaonjs/web": "0.
|
|
36
|
-
"@gaonjs/
|
|
30
|
+
"@gaonjs/async": "0.12.0",
|
|
31
|
+
"@gaonjs/data": "0.16.1",
|
|
32
|
+
"@gaonjs/config": "0.15.2",
|
|
33
|
+
"@gaonjs/core": "0.2.2",
|
|
34
|
+
"@gaonjs/i18n": "0.2.1",
|
|
35
|
+
"@gaonjs/web": "0.18.1",
|
|
36
|
+
"@gaonjs/mail": "0.2.1"
|
|
37
37
|
},
|
|
38
38
|
"scripts": {
|
|
39
39
|
"build": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json && node -e \"require('fs').cpSync('src/templates','dist/templates',{recursive:true})\""
|