@gaonjs/cli 0.63.1 → 0.65.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.
Files changed (46) hide show
  1. package/dist/commands/check.js +34 -18
  2. package/dist/commands/g.d.ts +1 -0
  3. package/dist/commands/g.js +11 -2
  4. package/dist/commands/gen.js +1 -1
  5. package/dist/dev.d.ts +13 -7
  6. package/dist/dev.js +30 -22
  7. package/dist/doctor/fixers/i18n-layout.d.ts +2 -2
  8. package/dist/doctor/fixers/i18n-layout.js +119 -25
  9. package/dist/doctor/fixers/index.js +7 -2
  10. package/dist/doctor/fixers/types.d.ts +17 -2
  11. package/dist/doctor/i18n-app-scope.d.ts +3 -0
  12. package/dist/doctor/i18n-app-scope.js +94 -65
  13. package/dist/doctor/i18n-layout.d.ts +1 -1
  14. package/dist/doctor/i18n-layout.js +157 -32
  15. package/dist/doctor/i18n-server-scope.d.ts +2 -0
  16. package/dist/doctor/i18n-server-scope.js +124 -0
  17. package/dist/doctor/locale-parity.js +29 -68
  18. package/dist/doctor/types.d.ts +1 -1
  19. package/dist/doctor.d.ts +2 -1
  20. package/dist/doctor.js +36 -10
  21. package/dist/i18n-config.d.ts +9 -9
  22. package/dist/i18n-config.js +8 -15
  23. package/dist/messages-gen.d.ts +10 -10
  24. package/dist/messages-gen.js +42 -54
  25. package/dist/scaffold/app.d.ts +10 -1
  26. package/dist/scaffold/app.js +12 -2
  27. package/dist/templates/project/AGENTS.md.tpl +7 -6
  28. package/dist/templates/project/agents/async.md.tpl +9 -9
  29. package/dist/templates/project/agents/data.md.tpl +37 -12
  30. package/dist/templates/project/agents/frontend.md.tpl +14 -9
  31. package/dist/templates/project/agents/i18n.md.tpl +185 -74
  32. package/dist/templates/project/agents/mail.md.tpl +3 -1
  33. package/dist/templates/project/agents/realtime.md.tpl +54 -31
  34. package/dist/templates/project/agents/seal.md.tpl +22 -4
  35. package/dist/templates/project/agents/security.md.tpl +1 -1
  36. package/dist/templates/project/agents/storage.md.tpl +23 -15
  37. package/dist/templates/project/agents/testing.md.tpl +5 -5
  38. package/dist/templates/project/agents/web.md.tpl +24 -11
  39. package/dist/templates/project/{locales → apps/web/locales}/en/frontend.json.tpl +1 -0
  40. package/dist/templates/project/{locales → apps/web/locales}/ko/frontend.json.tpl +1 -0
  41. package/dist/templates/project/gaon.config.ts.tpl +6 -5
  42. package/package.json +7 -7
  43. package/dist/templates/project/apps/web/locales/en.json.tpl +0 -3
  44. package/dist/templates/project/apps/web/locales/ko.json.tpl +0 -3
  45. /package/dist/templates/project/{locales → domain/locales}/en/backend.json.tpl +0 -0
  46. /package/dist/templates/project/{locales → domain/locales}/ko/backend.json.tpl +0 -0
@@ -74,7 +74,9 @@ npm i @gaonjs/seal # pnpm add @gaonjs/seal · yarn add @gaonjs/seal 동
74
74
  2) 앱 설정에서 켠다:
75
75
 
76
76
  ```ts
77
- // apps/<앱>/app.config.ts — 앱 wire 전체 봉인(요청/응답 JSON + 최초 문서 data-page).
77
+ // apps/web/app.config.ts — 앱 wire 전체 봉인(요청/응답 JSON + 최초 문서 data-page).
78
+ import { defineAppConfig } from 'gaonjs/config'
79
+
78
80
  export default defineAppConfig({
79
81
  seal: true, // 또는 { except: ['/webhooks/*'], strictQuery: true } — except = 외부가 seal 을 모르는 경로만 평문 통과 · strictQuery = 평문 쿼리도 거부(결정 354)
80
82
  })
@@ -82,10 +84,15 @@ export default defineAppConfig({
82
84
  3) 클라이언트를 배선한다:
83
85
 
84
86
  ```ts
85
- // apps/<앱>/main.ts — seal 클라이언트를 정적 import 해 createGaonApp 에 넘긴다.
87
+ // apps/web/main.ts — seal 클라이언트를 정적 import 해 createGaonApp 에 넘긴다.
86
88
  import { createGaonApp } from 'gaonjs/vue'
87
89
  import * as sealClient from '@gaonjs/seal/client' // 정적 import (사용자 vite 가 wasm 포함 번들)
88
- void createGaonApp({ pages, layouts, /* ... */ sealClient })
90
+ import { routes } from './.gaon/routes.manifest.js'
91
+
92
+ const pages = import.meta.glob('./pages/**/*.vue')
93
+ const layouts = import.meta.glob('./layouts/*.vue', { eager: true })
94
+
95
+ void createGaonApp({ pages, layouts, routes, sealClient })
89
96
  ```
90
97
 
91
98
  - **왜 main.ts 배선이 필요한가 (결정 124 · §3.1)**: `@gaonjs/vue` 는 선택 플러그인 seal 을 **몰라야** 한다
@@ -194,16 +201,27 @@ seal 앱 응답에만 `script-src` 에 `'wasm-unsafe-eval'` 을 **자동 주입*
194
201
 
195
202
  ```ts
196
203
  // apps/web/app.config.ts — 앱 wire 전체 봉인.
204
+ import { defineAppConfig } from 'gaonjs/config'
205
+
197
206
  export default defineAppConfig({ seal: true })
198
207
  ```
199
208
  ```ts
200
209
  // apps/web/main.ts — seal 클라이언트 정적 import 를 createGaonApp 에 주입(결정 124 · doctor 가 강제·fixer 자동 배선).
201
210
  import { createGaonApp } from 'gaonjs/vue'
202
211
  import * as sealClient from '@gaonjs/seal/client'
203
- void createGaonApp({ pages, layouts, /* ... */ sealClient })
212
+ import { routes } from './.gaon/routes.manifest.js'
213
+
214
+ const pages = import.meta.glob('./pages/**/*.vue')
215
+ const layouts = import.meta.glob('./layouts/*.vue', { eager: true })
216
+
217
+ void createGaonApp({ pages, layouts, routes, sealClient })
204
218
  ```
205
219
  ```ts
220
+ // apps/web/controllers/posts.ts
206
221
  // 컨트롤러는 그대로 — seal 을 전혀 모른다(요청/응답 JSON·최초 문서 data-page·WS 프레임이 자동 봉인).
222
+ import { controller } from 'gaonjs/web'
223
+ import { Post } from '../../../domain/models/Post.js'
224
+
207
225
  export default controller({
208
226
  async index() {
209
227
  return this.render('Posts/Index', { posts: await Post.latest().all() })
@@ -249,7 +249,7 @@
249
249
 
250
250
  ## 정본 예시
251
251
 
252
- ```ts
252
+ ```ts fragment
253
253
  // 시크릿 — env 로만 (gaonjs/env)
254
254
  import { env } from 'gaonjs/env'
255
255
  const apiKey = env('PAYMENT_API_KEY') // .env / 배포 환경변수
@@ -52,21 +52,26 @@
52
52
  ### 2. 설정 (`gaon.config.ts`)
53
53
 
54
54
  ```ts
55
- storage: process.env.STORAGE_ENDPOINT
56
- ? {
57
- default: 'main',
58
- disks: {
59
- main: {
60
- driver: 's3', // 's3' | 'local'
61
- bucket: process.env.STORAGE_BUCKET ?? 'myapp',
62
- endpoint: process.env.STORAGE_ENDPOINT, // MinIO/R2 = 필수 · AWS S3 = 생략
63
- accessKeyId: process.env.STORAGE_ACCESS_KEY,
64
- secretAccessKey: process.env.STORAGE_SECRET_KEY,
65
- // publicUrl: 'https://cdn.example.com', // 있으면 url()이 공개 URL
55
+ // gaon.config.ts
56
+ import { defineConfig } from 'gaonjs/config'
57
+
58
+ export default defineConfig({
59
+ storage: process.env.STORAGE_ENDPOINT
60
+ ? {
61
+ default: 'main',
62
+ disks: {
63
+ main: {
64
+ driver: 's3', // 's3' | 'local'
65
+ bucket: process.env.STORAGE_BUCKET ?? 'myapp',
66
+ endpoint: process.env.STORAGE_ENDPOINT, // MinIO/R2 = 필수 · AWS S3 = 생략
67
+ accessKeyId: process.env.STORAGE_ACCESS_KEY,
68
+ secretAccessKey: process.env.STORAGE_SECRET_KEY,
69
+ // publicUrl: 'https://cdn.example.com', // 있으면 url()이 공개 URL
70
+ },
66
71
  },
67
- },
68
- }
69
- : undefined,
72
+ }
73
+ : undefined,
74
+ })
70
75
  ```
71
76
 
72
77
  - dev 는 compose 의 `createbuckets` 가 버킷을 만들어 **첫 업로드부터 동작**한다
@@ -86,6 +91,9 @@ storage: process.env.STORAGE_ENDPOINT
86
91
 
87
92
  ```ts
88
93
  // apps/web/controllers/profile.ts
94
+ import { controller } from 'gaonjs/web'
95
+ import { Storage } from 'gaonjs/storage'
96
+
89
97
  export default controller({
90
98
  async updateAvatar() {
91
99
  const f = this.file('avatar') // UploadedFile | undefined
@@ -139,7 +147,7 @@ export default controller({
139
147
 
140
148
  ## 정본 예시
141
149
 
142
- ```ts
150
+ ```ts fragment
143
151
  // 저장 → 공개/서명 URL 얻기(드라이버 무관 · 같은 코드). url() 은 async.
144
152
  // 화면에 표시할 파일이면 키를 public/ 아래에 둔다 — 로컬 디스크의 서빙 범위가
145
153
  // publicPrefix(기본 'public/')로 한정되기 때문이다(결정 401 · 밖이면 404).
@@ -56,7 +56,7 @@
56
56
 
57
57
  ### 3. 실 NATS 접속 관례
58
58
 
59
- ```ts
59
+ ```ts test
60
60
  import { connectNats } from 'gaonjs/async'
61
61
 
62
62
  // 인자 없이 부르면 env NATS_URL(없으면 nats://127.0.0.1:4222)에 붙는다.
@@ -147,7 +147,7 @@ describe('PostPublished (실 NATS JetStream)', () => {
147
147
  표준 하네스(`connectTestDatabase` · `config.nats` + `main` 커넥션)가 배선하므로, `gaon test`
148
148
  로 도는 통합 테스트에서 그냥 성립한다.
149
149
 
150
- ```ts
150
+ ```ts test
151
151
  // service() 가 emit 하는 흐름도 같은 한 줄로 확증된다(트리거만 서비스 호출로 바꾸면 됨).
152
152
  await expectEventProcessed(PostPublished, () => PublishPost.call({ postId: 1n }), { nats })
153
153
  ```
@@ -190,7 +190,7 @@ DB 테스트는 손으로 커넥션을 배선하지 않는다 — `gaon test`
190
190
 
191
191
  스캐폴드가 심어 주는 `test/setup.ts`(수정 불필요):
192
192
 
193
- ```ts
193
+ ```ts test
194
194
  import { afterAll, afterEach, beforeAll } from 'vitest'
195
195
  import { connectTestDatabase, truncateAllConnections, type TestDbHandle } from 'gaonjs/testing'
196
196
 
@@ -273,7 +273,7 @@ describe('AuditLog(실 mongod)', () => {
273
273
  서비스가 반환하기 **전에** 실행되므로, 테스트는 서비스 호출을 `await` 한 직후 몽고
274
274
  문서를 단언하면 된다(별도 대기·폴링 불필요).
275
275
 
276
- ```ts
276
+ ```ts test
277
277
  // SQL 커밋 → 몽고 감사 로그 순서를 한 흐름으로 확증한다.
278
278
  const user = await SignUp.call({ email: 'a@b.c', password: 'x' }) // main(SQL) 커밋 + afterCommit
279
279
  expect(await AuditLog.countDocuments({ actorId: String(user.id) })).toBe(1)
@@ -298,7 +298,7 @@ expect(await AuditLog.countDocuments({ actorId: 'dup' })).toBe(0)
298
298
  정본 = "직렬화 결과 어디에도 hidden 컬럼명이 없다" 를 **재귀로** 단언한다
299
299
  (배열·중첩 관계 전부 훑는 헬퍼). 관계를 포함한 픽스처에 적용한다:
300
300
 
301
- ```ts
301
+ ```ts test
302
302
  // 직렬화 결과(JSON-safe)에 hidden 컬럼명이 어느 깊이에도 없음을 확인.
303
303
  function assertNoHiddenLeak(value: unknown, hidden: readonly string[]): void {
304
304
  const walk = (v: unknown): void => {
@@ -231,7 +231,7 @@ handler 안에서 파싱). **결정 342 이후 헤더 부착은 프레임웍 자
231
231
  (참고: 지원 Content-Type 은 `application/json` · `multipart/form-data` 뿐이라
232
232
  `x-www-form-urlencoded` 로 폼을 보내면 415 로 거부된다 · `inertia.ts` · §아래 415.)
233
233
 
234
- ```vue
234
+ ```vue fragment
235
235
  <script setup lang="ts">
236
236
  import { useForm } from 'gaonjs/vue'
237
237
  const form = useForm({ avatar: null as File | null })
@@ -259,8 +259,10 @@ redirect 로 처리한다 — 전체 페이지 리로드도, 별도 REST 엔드
259
259
  폼 API 는 **`gaonjs/vue` 의 `useForm`·`router`** 뿐이다(결정 64) —
260
260
  `@inertiajs/vue3` 를 직접 import 하지 않는다(파사드가 The One Way).
261
261
 
262
- ```ts
262
+ ```ts fragment
263
263
  // 로그인 폼 — 제출은 Inertia SPA 방식, 서버는 redirect 로 답한다.
264
+ import { pageProps, router, useForm } from 'gaonjs/vue'
265
+
264
266
  // pageProps 는 반응형 — 변수로 받아 props.x 로 접근한다(구조분해 금지 · 결정 99).
265
267
  // CSRF 토큰은 프레임웍이 자동 부착한다(결정 342) — _csrf 바디도, 수동 헤더도 없다.
266
268
  const props = pageProps<'web:session#new'>()
@@ -282,7 +284,7 @@ Inertia 폼(`useForm(...).post()`)의 검증 실패는 컨트롤러가 손으로
282
284
  방문의 `errors` 로 `form.errors.<필드>` 를 **자동으로 채운다**. 입력값은
283
285
  `useForm` 이 그대로 보존한다(재제출 방지). 컨트롤러는 성공 경로만 쓴다:
284
286
 
285
- ```ts
287
+ ```ts controller-action
286
288
  // 컨트롤러 — 검증 실패 분기를 손으로 쓰지 않는다(결정 109).
287
289
  async create() {
288
290
  const data = this.params(Post.form.pick('title', 'body')) // 실패 시 프레임웍이 303 back
@@ -291,10 +293,18 @@ async create() {
291
293
  }
292
294
  ```
293
295
 
294
- ```vue
296
+ ```vue fragment
295
297
  <!-- 페이지 — form.errors.<필드> 는 서버 검증 실패 시 자동으로 채워진다. -->
296
- <input v-model="form.title" />
297
- <p v-if="form.errors.title">{{ form.errors.title }}</p>
298
+ <script setup lang="ts">
299
+ import { useForm } from 'gaonjs/vue'
300
+
301
+ const form = useForm({ title: '', body: '' })
302
+ </script>
303
+
304
+ <template>
305
+ <input v-model="form.title" />
306
+ <p v-if="form.errors.title">{{ form.errors.title }}</p>
307
+ </template>
298
308
  ```
299
309
 
300
310
  순수 JSON/API 앱(X-Inertia 아님·세션 없음)은 기존대로 **422 JSON** 을 받는다.
@@ -302,8 +312,9 @@ async create() {
302
312
  #### 검증 사유 로케일화 — 예약 namespace `validation.*` (결정 183)
303
313
 
304
314
  검증 실패의 **필드별 사유**(`form.errors.<필드>` 로 엔드유저에 노출되는 부분)는 요청
305
- 로케일로 번역된다 — `i18n` 이 설정돼 있고 앱이 `locales/<lng>.json` 의 **예약 namespace
306
- `validation.<code>`** 로 번역을 제공하면. 프레임웍은 **안정적 코드만** 노출하고 번역은
315
+ 로케일로 번역된다 — `i18n` 이 설정돼 있고 프로젝트가 `domain/locales/<로케일>/backend.json`
316
+ 의 **예약 namespace `validation.<code>`** 로 번역을 제공하면(결정 459 검증 문안은 화면
317
+ 밖 문구라 도메인이 소유한다). 프레임웍은 **안정적 코드만** 노출하고 번역은
307
318
  앱 몫이다(The One Way: 코드는 프레임웍, 문안은 앱). 키가 없으면 내장 fallback(한국어)로
308
319
  떨어져 거동이 보존된다. i18n 미설정이면 항상 fallback.
309
320
 
@@ -320,7 +331,7 @@ async create() {
320
331
  | `invalid` | — | 그 외 coerce 예외(비-CoerceError) |
321
332
 
322
333
  ```json
323
- // locales/en.json — 앱이 검증 문안을 로케일별로 준다(i18next {{max}} 보간).
334
+ // domain/locales/en/backend.json — 검증 문안을 로케일별로 준다(i18next {{max}} 보간).
324
335
  { "validation": {
325
336
  "required": "This field is required.",
326
337
  "too_long": "At most {{max}} characters (got {{len}})."
@@ -381,6 +392,8 @@ async create() {
381
392
 
382
393
  ```ts
383
394
  // apps/web/app.config.ts
395
+ import { defineAppConfig } from 'gaonjs/config'
396
+
384
397
  export default defineAppConfig({
385
398
  sharedProps: (ctx) => ({ locale: ctx.session?.locale ?? 'en', theme: 'dark' }),
386
399
  })
@@ -414,7 +427,7 @@ shared.locale // 로그인/로그아웃·플래시로
414
427
  메서드(스코프)**, 대등한 조합이면 `domain/services/`(`agents/data.md` §8). 컨트롤러가
415
428
  `pluck`·교집합·사이드바 질의를 인라인 조립하기 시작하면 서비스/스코프로 옮긴다.
416
429
 
417
- ```ts
430
+ ```ts fragment
418
431
  // ❌ 컨트롤러가 검색 교집합·태그 필터를 인라인 조립
419
432
  // ✅ const rows = await Post.searchPublished(term).latest().offset(o).limit(n).all()
420
433
  // ✅ 페이지네이션은 스코프 체인 종단 paginate — 컨트롤러는 여전히 한 줄(결정 119)
@@ -450,7 +463,7 @@ shared.locale // 로그인/로그아웃·플래시로
450
463
  base64·해시를 손으로 짜지 말고** `gaonjs/web` 의 헬퍼를 쓴다(The One Way · §7 인증).
451
464
  bcrypt(cost 10)로 해싱하며 상수시간 비교를 제공한다.
452
465
 
453
- ```ts
466
+ ```ts fragment
454
467
  import { hashPassword, verifyPassword } from 'gaonjs/web'
455
468
 
456
469
  // 가입 — 서비스에서 평문을 해싱해 hidden 컬럼(passwordDigest)에 저장.
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "nav": { "home": "Home" },
3
+ "home": { "tagline": "You are on Gaon." },
3
4
  "common": {
4
5
  "loading": "Checking status…",
5
6
  "items_one": "{{count}} item",
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "nav": { "home": "홈" },
3
+ "home": { "tagline": "가온에 올라탔습니다." },
3
4
  "common": {
4
5
  "loading": "상태 확인 중…",
5
6
  "items_other": "항목 {{count}}개"
@@ -54,11 +54,12 @@ export default defineConfig({
54
54
  }
55
55
  : undefined,
56
56
 
57
- // 다국어(§7 · 결정 454). 카탈로그는 언어별 폴더 + 스코프 분리다:
58
- // locales/<로케일>/frontend.json — 화면 문구. 클라 t()(gaonjs/vue) 쓰고번들 청크로 나간다.
59
- // locales/<로케일>/backend.json — 메일·잡·검증 **서버 전용**. 클라로 나가지 않는다.
60
- // apps/<앱>/locales/<로케일>.json 전용 화면 문구(그 청크에만 실린다).
61
- // 요청 로케일은 세션>쿠키>Accept-Language 로 자동 협상된다(결정 159 · this.setLocale 로 전환).
57
+ // 다국어(§7 · 결정 459). 문구의 **소유자**가 위치를 정한다 화면이면 그 앱, 화면 밖이면 domain:
58
+ // apps/<앱>/locales/<로케일>/frontend.json — 그 앱 화면 문구. 클라 t()(gaonjs/vue) · 청크로만 나간다.
59
+ // apps/<앱>/locales/<로케일>/backend.json — 컨트롤러 전용 서버 문구(선택).
60
+ // domain/locales/<로케일>/backend.json 메일·잡·검증 도메인 공통 **서버 전용**. 클라로 나가지 않는다.
61
+ // 위치는 관례로 고정이라 dir 설정이 없다. 요청 로케일은 세션>쿠키>Accept-Language 로
62
+ // 자동 협상된다(결정 159 · this.setLocale 로 전환).
62
63
  i18n: { fallbackLng: 'ko', supportedLngs: ['ko', 'en'] },
63
64
 
64
65
  // 웹 서버 리슨 옵션. --port · env PORT 로 덮을 수 있다.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gaonjs/cli",
3
- "version": "0.63.1",
3
+ "version": "0.65.0",
4
4
  "description": "Gaon CLI — 스캐폴딩·제너레이터·마이그레이션·dev/serve/work/hub·doctor·check (bin: gaon)",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -33,14 +33,14 @@
33
33
  "typescript": "^5.9.0",
34
34
  "vite": "^7.0.0",
35
35
  "@gaonjs/async": "0.22.0",
36
- "@gaonjs/config": "0.25.11",
36
+ "@gaonjs/config": "0.26.0",
37
37
  "@gaonjs/core": "0.3.0",
38
- "@gaonjs/data": "0.26.1",
39
- "@gaonjs/i18n": "0.4.1",
40
- "@gaonjs/mail": "0.5.3",
41
- "@gaonjs/web": "0.32.0"
38
+ "@gaonjs/i18n": "0.5.0",
39
+ "@gaonjs/mail": "0.5.4",
40
+ "@gaonjs/web": "0.33.0",
41
+ "@gaonjs/data": "0.26.2"
42
42
  },
43
43
  "scripts": {
44
- "build": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json && node -e \"const fs=require('fs');fs.cpSync('src/templates','dist/templates',{recursive:true,filter:(s)=>!s.endsWith('.ts')});fs.rmSync('dist/templates/index.ts',{force:true})\""
44
+ "build": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json && node -e \"const fs=require('fs');fs.rmSync('dist/templates/project',{recursive:true,force:true});fs.cpSync('src/templates','dist/templates',{recursive:true,filter:(s)=>!s.endsWith('.ts')});fs.rmSync('dist/templates/index.ts',{force:true})\""
45
45
  }
46
46
  }
@@ -1,3 +0,0 @@
1
- {
2
- "home": { "tagline": "You are on Gaon." }
3
- }
@@ -1,3 +0,0 @@
1
- {
2
- "home": { "tagline": "가온에 올라탔습니다." }
3
- }