@gaonjs/cli 0.42.3 → 0.47.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/db.js +29 -7
- package/dist/commands/new.d.ts +2 -0
- package/dist/commands/new.js +8 -3
- package/dist/db/reset.js +20 -10
- package/dist/db/resolve.d.ts +15 -0
- package/dist/db/resolve.js +29 -0
- package/dist/doctor/pageprops-destructure.d.ts +2 -2
- package/dist/doctor/pageprops-destructure.js +29 -23
- package/dist/doctor/schema-relations.js +6 -1
- package/dist/doctor.js +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.js +18 -2
- package/dist/mcp/tools.js +15 -1
- package/dist/scaffold/job.js +3 -1
- package/dist/templates/project/.dockerignore.tpl +3 -0
- package/dist/templates/project/.env.example.tpl +1 -1
- package/dist/templates/project/AGENTS.md.tpl +3 -2
- package/dist/templates/project/CLAUDE.md.tpl +4 -3
- package/dist/templates/project/Dockerfile.tpl +6 -1
- package/dist/templates/project/agents/async.md.tpl +44 -6
- package/dist/templates/project/agents/data.md.tpl +206 -15
- package/dist/templates/project/agents/frontend.md.tpl +36 -8
- package/dist/templates/project/agents/mail.md.tpl +2 -1
- package/dist/templates/project/agents/realtime.md.tpl +26 -2
- package/dist/templates/project/agents/seal.md.tpl +9 -1
- package/dist/templates/project/agents/security.md.tpl +18 -0
- package/dist/templates/project/agents/storage.md.tpl +15 -8
- package/dist/templates/project/agents/web.md.tpl +38 -2
- 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 +11 -6
- package/dist/doctor/shared-composable-purity.d.ts +0 -8
- package/dist/doctor/shared-composable-purity.js +0 -164
- 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/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/db/reset.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { existsSync } from 'node:fs';
|
|
11
11
|
import { join } from 'node:path';
|
|
12
12
|
import { sql } from 'kysely';
|
|
13
|
-
import { snapshotFromDb, MIGRATIONS_TABLE } from '@gaonjs/data';
|
|
13
|
+
import { snapshotFromDb, orderTableDropsByFk, MIGRATIONS_TABLE } from '@gaonjs/data';
|
|
14
14
|
import { resolveDbTarget } from './resolve.js';
|
|
15
15
|
import { runDbMigrate } from './migrate.js';
|
|
16
16
|
import { runDbSeedCommand } from '../db.js';
|
|
@@ -71,7 +71,8 @@ export async function runDbReset(opts) {
|
|
|
71
71
|
// 현재 DB 스냅샷 — 지울 테이블 목록. dialect.introspect 는 이미
|
|
72
72
|
// _gaon_migrations 를 제외하지만, reset 은 이력도 지워야 하므로 별도 처리.
|
|
73
73
|
const snapshot = await snapshotFromDb(target.db, target.dialect);
|
|
74
|
-
|
|
74
|
+
// FK 역순(자식 먼저)으로 정렬해 mysql(CASCADE 없음) 다중 drop 이 FK 위반 없이 돌게 한다(F4 · 결정 275).
|
|
75
|
+
const userTables = orderTableDropsByFk(snapshot, Object.keys(snapshot).sort());
|
|
75
76
|
// dry-run: 무엇을 지울지·만들지를 계산해 출력. 실제 파괴는 안 함.
|
|
76
77
|
if (opts.dryRun) {
|
|
77
78
|
const dropSqls = userTables.map((t) => target.dialect.dropTable(t));
|
|
@@ -93,15 +94,24 @@ export async function runDbReset(opts) {
|
|
|
93
94
|
};
|
|
94
95
|
}
|
|
95
96
|
else {
|
|
96
|
-
// DROP ALL — 이력 테이블도 함께.
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
97
|
+
// DROP ALL — 이력 테이블도 함께. pg 는 dropTable 이 CASCADE, mysql 은 CASCADE 가 없어
|
|
98
|
+
// FK 역순 정렬(위 orderTableDropsByFk)로 안전하게 지운다. 순환 FK 방어로 mysql 은
|
|
99
|
+
// FOREIGN_KEY_CHECKS 를 잠시 끈다(F4 · 결정 275).
|
|
100
|
+
const isMysql = target.dialect.name === 'mysql';
|
|
101
|
+
if (isMysql)
|
|
102
|
+
await sql.raw('set foreign_key_checks = 0').execute(target.db);
|
|
103
|
+
try {
|
|
104
|
+
for (const t of userTables) {
|
|
105
|
+
await sql.raw(target.dialect.dropTable(t)).execute(target.db);
|
|
106
|
+
droppedTables.push(t);
|
|
107
|
+
}
|
|
108
|
+
// 이력 테이블 삭제 — 존재하지 않아도 에러 없게.
|
|
109
|
+
await sql.raw(`drop table if exists ${MIGRATIONS_TABLE}`).execute(target.db);
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
if (isMysql)
|
|
113
|
+
await sql.raw('set foreign_key_checks = 1').execute(target.db);
|
|
102
114
|
}
|
|
103
|
-
// 이력 테이블 삭제 — 존재하지 않아도 에러 없게.
|
|
104
|
-
await sql.raw(`drop table if exists ${MIGRATIONS_TABLE}`).execute(target.db);
|
|
105
115
|
}
|
|
106
116
|
}
|
|
107
117
|
finally {
|
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 를 로드한다.
|
|
@@ -128,6 +150,13 @@ export async function resolveDbTarget(opts) {
|
|
|
128
150
|
` 등록된 키: ${known.length ? known.join(', ') : '(없음)'}\n` +
|
|
129
151
|
` → gaon.config.ts 의 db.${dbKey} 를 선언하고 다시 실행하세요.`);
|
|
130
152
|
}
|
|
153
|
+
// 결정 279(§7.4): 문서형(mongodb) 커넥션은 마이그레이션이 없다 — SQL 전용 경로(diff/migrate/
|
|
154
|
+
// introspect)로 흘리지 않고 명확히 안내한다. 몽고 인덱스는 Mongoose 스키마 선언으로 관리한다.
|
|
155
|
+
if (connCfg.adapter === 'mongodb') {
|
|
156
|
+
throw new Error(`[gaon db] 커넥션 '${dbKey}' 은 문서형(mongodb)입니다 — 문서형은 마이그레이션이 없습니다(§7.4).\n` +
|
|
157
|
+
` → 몽고 인덱스는 Mongoose 스키마 선언(index:true / schema.index())으로 관리합니다(mongoose autoIndex 기본값이 부팅 시 보장).\n` +
|
|
158
|
+
` → gaon db diff/migrate 는 SQL 커넥션(postgres·mysql)에만 실행하세요(키 생략 시 순회가 문서형을 자동으로 건너뜁니다 · 결정 292).`);
|
|
159
|
+
}
|
|
131
160
|
const db = createDb(connCfg);
|
|
132
161
|
registerConnection(dbKey, db, connCfg.adapter);
|
|
133
162
|
const dialect = dialectFor(connCfg.adapter);
|
|
@@ -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>;
|
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
// @gaonjs/cli · doctor · pageProps() 구조분해 검출 (결정 99 · 경고)
|
|
1
|
+
// @gaonjs/cli · doctor · pageProps()/useShared() 구조분해 검출 (결정 99 · 302 · 경고)
|
|
2
2
|
//
|
|
3
|
-
// `pageProps()` 는 반응형 프록시다 — 같은 페이지로의
|
|
4
|
-
// 새 props 를 주면(댓글
|
|
5
|
-
// `const { posts } = pageProps<...>()
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
3
|
+
// `pageProps()`·`useShared()` 는 반응형 프록시다 — 같은 페이지로의 리다이렉트/
|
|
4
|
+
// 리로드로 서버가 새 props 를 주면(댓글 작성·삭제·로그인 등) 재마운트 없이 화면이
|
|
5
|
+
// 즉시 갱신된다. 하지만 `const { posts } = pageProps<...>()`·`const { csrf } =
|
|
6
|
+
// useShared()` 처럼 **구조분해**하면 그 순간 값을 한 번 읽어 반응성이 끊긴다
|
|
7
|
+
// (Vue defineProps 를 구조분해하면 안 되는 것과 같은 함정 · 첫 실사용 블로그에서
|
|
8
|
+
// preserveState:false 강제 리로드 우회로 나타난 근본 원인). 이 검사가 그 구조분해를
|
|
9
|
+
// 경고로 잡는다. 판정은 소스 텍스트 기반(주석 제외).
|
|
10
|
+
//
|
|
11
|
+
// 결정 302: 문서(agents/frontend.md)는 useShared 구조분해도 같은 함정으로 경고해
|
|
12
|
+
// 왔는데 검사는 pageProps·`.vue` 만 봤다 — useShared 를 정규식에 더하고, pageProps/
|
|
13
|
+
// useShared 사용이 허용되는 앱 전용 컴포저블(`apps/**/*.ts`)까지 순회를 넓힌다.
|
|
9
14
|
//
|
|
10
15
|
// 오탐 방지: `const props = pageProps<...>()`(변수 바인딩)는 정상이므로 건드리지
|
|
11
16
|
// 않는다 — 여는 중괄호 `{` 로 시작하는 구조분해 바인딩만 잡는다.
|
|
@@ -20,44 +25,45 @@ function stripCommentsKeepLines(source) {
|
|
|
20
25
|
.replace(/<!--[\s\S]*?-->/g, blank)
|
|
21
26
|
.replace(/(^|[^:])\/\/[^\n]*/g, (_m, p1) => p1 + ' '.repeat(_m.length - p1.length));
|
|
22
27
|
}
|
|
23
|
-
// `const|let|var { ... } = pageProps` — 구조분해 바인딩의 초기화가
|
|
24
|
-
// 제네릭 인자(`<'web:posts#index'>`)·공백·중첩 중괄호는 허용하되,
|
|
25
|
-
// 구조분해만 잡는다(변수 바인딩 `const props =` 은 제외).
|
|
26
|
-
const DESTRUCTURE_RE = /\b(?:const|let|var)\s*\{[^}]*\}\s*=\s*pageProps\b/g;
|
|
27
|
-
/** 소스에 pageProps() 구조분해가 있는지(단위 테스트 진입점 · 주석은 제외). */
|
|
28
|
+
// `const|let|var { ... } = pageProps|useShared` — 구조분해 바인딩의 초기화가 반응형
|
|
29
|
+
// 프록시 호출. 제네릭 인자(`<'web:posts#index'>`)·공백·중첩 중괄호는 허용하되,
|
|
30
|
+
// `{` 로 시작하는 구조분해만 잡는다(변수 바인딩 `const props =` 은 제외 · 결정 302).
|
|
31
|
+
const DESTRUCTURE_RE = /\b(?:const|let|var)\s*\{[^}]*\}\s*=\s*(pageProps|useShared)\b/g;
|
|
32
|
+
/** 소스에 pageProps()/useShared() 구조분해가 있는지(단위 테스트 진입점 · 주석은 제외). */
|
|
28
33
|
export function usesPagePropsDestructure(source) {
|
|
29
34
|
DESTRUCTURE_RE.lastIndex = 0;
|
|
30
35
|
return DESTRUCTURE_RE.test(stripCommentsKeepLines(source));
|
|
31
36
|
}
|
|
32
|
-
/** apps/ 의 .vue 를 훑어 pageProps() 구조분해를 경고로 낸다. */
|
|
37
|
+
/** apps/ 의 .vue + .ts(컴포저블 등)를 훑어 pageProps()/useShared() 구조분해를 경고로 낸다. */
|
|
33
38
|
export async function checkPagePropsDestructure(cwd) {
|
|
34
39
|
const appsDir = join(cwd, 'apps');
|
|
35
40
|
const issues = [];
|
|
36
|
-
for (const abs of await
|
|
41
|
+
for (const abs of await walkFrontendSources(appsDir)) {
|
|
37
42
|
const source = await readFile(abs, 'utf8').catch(() => '');
|
|
38
43
|
const stripped = stripCommentsKeepLines(source);
|
|
39
44
|
const rel = relative(cwd, abs);
|
|
40
45
|
for (const m of stripped.matchAll(DESTRUCTURE_RE)) {
|
|
46
|
+
const callee = m[1] ?? 'pageProps';
|
|
41
47
|
const line = stripped.slice(0, m.index ?? 0).split('\n').length;
|
|
42
48
|
issues.push({
|
|
43
49
|
rule: 'pageprops-destructure',
|
|
44
50
|
level: 'warning',
|
|
45
51
|
file: rel,
|
|
46
52
|
line,
|
|
47
|
-
message:
|
|
48
|
-
|
|
49
|
-
`한 번 읽어 반응성이 끊깁니다 — 같은 페이지로의 리다이렉트/리로드(댓글
|
|
50
|
-
`후 새 데이터가 화면에 반영되지 않습니다(결정 99).\n` +
|
|
51
|
-
`→ 변수로 받아 속성으로 접근하세요: \`const
|
|
52
|
-
`\`
|
|
53
|
+
message: `${callee}() 구조분해 발견: ${rel}:${line} 이 \`const { … } = ${callee}(…)\` 로 ` +
|
|
54
|
+
`값을 구조분해합니다. ${callee}() 는 반응형 프록시라, 구조분해하면 그 순간 값을 ` +
|
|
55
|
+
`한 번 읽어 반응성이 끊깁니다 — 같은 페이지로의 리다이렉트/리로드(댓글 작성·삭제·로그인 등) ` +
|
|
56
|
+
`후 새 데이터가 화면에 반영되지 않습니다(결정 99 · 302).\n` +
|
|
57
|
+
`→ 변수로 받아 속성으로 접근하세요: \`const v = ${callee}(…)\` 후 ` +
|
|
58
|
+
`\`v.<필드>\`. (Vue defineProps 를 구조분해하면 안 되는 것과 같은 이유입니다.)`,
|
|
53
59
|
detail: { file: rel, line },
|
|
54
60
|
});
|
|
55
61
|
}
|
|
56
62
|
}
|
|
57
63
|
return { rule: 'pageprops-destructure', issues };
|
|
58
64
|
}
|
|
59
|
-
/** apps/ 하위 .vue
|
|
60
|
-
async function
|
|
65
|
+
/** apps/ 하위 .vue·.ts 절대경로(결정 302 — 컴포저블 .ts 포함 · 생성 파일 .gaon 제외). */
|
|
66
|
+
async function walkFrontendSources(dir) {
|
|
61
67
|
const out = [];
|
|
62
68
|
const walk = async (d) => {
|
|
63
69
|
let entries;
|
|
@@ -74,7 +80,7 @@ async function walkVue(dir) {
|
|
|
74
80
|
continue;
|
|
75
81
|
await walk(abs);
|
|
76
82
|
}
|
|
77
|
-
else if (e.isFile() && e.name.endsWith('.vue')) {
|
|
83
|
+
else if (e.isFile() && (e.name.endsWith('.vue') || (e.name.endsWith('.ts') && !e.name.endsWith('.d.ts')))) {
|
|
78
84
|
out.push(abs);
|
|
79
85
|
}
|
|
80
86
|
}
|
|
@@ -13,12 +13,16 @@
|
|
|
13
13
|
// belongsTo 를 검출할 수 있다(resolve.ts scanTables 는 단일 dbKey 로 거르므로 부적합).
|
|
14
14
|
import { existsSync } from 'node:fs';
|
|
15
15
|
import { join, relative } from 'node:path';
|
|
16
|
-
import { scanSchemaDir, checkCrossConnectionRelations, checkRelationTargets, } from '@gaonjs/data';
|
|
16
|
+
import { scanSchemaDir, checkCrossConnectionRelations, checkRelationTargets, checkPartitions, } from '@gaonjs/data';
|
|
17
17
|
import { registerTsResolve } from '../tsResolve.js';
|
|
18
18
|
/** TableDef 판별 — scanSchemaDir 이 준 모듈 네임스페이스에서 table() 산출만 고른다. */
|
|
19
19
|
function isTableDef(v) {
|
|
20
20
|
return (typeof v === 'object' &&
|
|
21
21
|
v !== null &&
|
|
22
|
+
// 결정 279(§4.3·§6.2): 문서형 collection() 산출(__gaonKind)은 크로스커넥션 관계 로드에서
|
|
23
|
+
// 제외한다 — SQL↔문서형 belongsTo/hasMany 는 애초에 관계 그래프에 오르지 않는다(추가 코드
|
|
24
|
+
// 없이 격리 · generator.ts isTableDef 와 동일 판별).
|
|
25
|
+
!('__gaonKind' in v) &&
|
|
22
26
|
'name' in v &&
|
|
23
27
|
'defs' in v &&
|
|
24
28
|
'db' in v &&
|
|
@@ -108,6 +112,7 @@ export async function checkSchemaRelations(cwd) {
|
|
|
108
112
|
const diags = [
|
|
109
113
|
...checkCrossConnectionRelations(tables),
|
|
110
114
|
...checkRelationTargets(tables),
|
|
115
|
+
...checkPartitions(tables), // 결정 277 — 파티션 키 컬럼 존재 검사
|
|
111
116
|
];
|
|
112
117
|
const issues = diags.map((d) => toCheck(d, fileOf, ownerOf(d.message)));
|
|
113
118
|
return { rule: 'schema-relations', issues };
|
package/dist/doctor.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* 17) method-override (결정 89 · _method HTTP 스푸핑 hack 경고)
|
|
22
22
|
* 18) csrf-wiring (결정 93 · 비-GET 라우트 + session 미배선 = CSRF 무방비 경고)
|
|
23
23
|
* 19) internal-anchor (결정 96 · 앱 내부 이동 일반 <a> = 풀 리로드 경고)
|
|
24
|
-
* 20) pageprops-destructure (결정 99 · pageProps() 구조분해 = 반응성 끊김 경고)
|
|
24
|
+
* 20) pageprops-destructure (결정 99·302 · pageProps()/useShared() 구조분해 = 반응성 끊김 경고 · .vue+.ts)
|
|
25
25
|
* 21) async-offload (결정 102·103 · 컨트롤러 인라인 메일·이미지·외부 HTTP = 응답 지연 경고)
|
|
26
26
|
* 22) page-layout-breakpoint (결정 107 · 페이지 레이아웃 브레이크포인트 직접 사용 = 안내 경고)
|
|
27
27
|
* 23) link-button-nesting (결정 113 · Link 로 Button 감싸기 = <a><button> 중첩 경고)
|
package/dist/index.d.ts
CHANGED
|
@@ -64,6 +64,12 @@ export interface ParsedNewArgs {
|
|
|
64
64
|
readonly packageManager?: "pnpm" | "npm" | "yarn";
|
|
65
65
|
/** pm 값이 주어졌으나 pnpm·npm·yarn 이 아닐 때 그 원문(에러 안내용). */
|
|
66
66
|
readonly unknownPm?: string;
|
|
67
|
+
/**
|
|
68
|
+
* 결정 314: 파사드 버전 스펙(예 `^1.21.0`). create-gaon 이 자기 발행 메타에서
|
|
69
|
+
* 해석해 주입한다 — npx 설치 트리엔 gaonjs 패키지가 없어 상향 탐색(결정 242)이
|
|
70
|
+
* fail-loud 로 죽기 때문. 미지정이면 기존 자동 감지 경로 그대로.
|
|
71
|
+
*/
|
|
72
|
+
readonly gaonjsVersion?: string;
|
|
67
73
|
}
|
|
68
74
|
/**
|
|
69
75
|
* `gaon new <name>` 의 인자를 파싱한다.
|
package/dist/index.js
CHANGED
|
@@ -172,12 +172,22 @@ export function parseDoctorChecks(argv) {
|
|
|
172
172
|
export function parseNewArgs(rest) {
|
|
173
173
|
let name;
|
|
174
174
|
let pmRaw;
|
|
175
|
+
let gaonjsVersion;
|
|
175
176
|
for (let i = 0; i < rest.length; i++) {
|
|
176
177
|
const a = rest[i];
|
|
177
178
|
if (a === "--package-manager" || a === "--pm") {
|
|
178
179
|
pmRaw = rest[++i];
|
|
179
180
|
continue;
|
|
180
181
|
}
|
|
182
|
+
// 결정 314: 값을 취하는 플래그라 pm 과 같은 이유로 값을 건너뛴다(이름 오인 방지).
|
|
183
|
+
if (a === "--gaonjs-version") {
|
|
184
|
+
gaonjsVersion = rest[++i];
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (a.startsWith("--gaonjs-version=")) {
|
|
188
|
+
gaonjsVersion = a.slice("--gaonjs-version=".length);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
181
191
|
const eqPrefix = a.startsWith("--package-manager=")
|
|
182
192
|
? "--package-manager="
|
|
183
193
|
: a.startsWith("--pm=")
|
|
@@ -192,10 +202,15 @@ export function parseNewArgs(rest) {
|
|
|
192
202
|
if (name === undefined)
|
|
193
203
|
name = a;
|
|
194
204
|
}
|
|
205
|
+
const version = gaonjsVersion === undefined || gaonjsVersion === "" ? undefined : gaonjsVersion;
|
|
195
206
|
if (pmRaw === "pnpm" || pmRaw === "npm" || pmRaw === "yarn") {
|
|
196
|
-
return { name, packageManager: pmRaw };
|
|
207
|
+
return { name, packageManager: pmRaw, gaonjsVersion: version };
|
|
197
208
|
}
|
|
198
|
-
return {
|
|
209
|
+
return {
|
|
210
|
+
name,
|
|
211
|
+
unknownPm: pmRaw === undefined || pmRaw === "" ? undefined : pmRaw,
|
|
212
|
+
gaonjsVersion: version,
|
|
213
|
+
};
|
|
199
214
|
}
|
|
200
215
|
/**
|
|
201
216
|
* `gaon db <sub>` 의 인자를 파싱한다(argv = 전체 · argv[0]='db', argv[1]=sub).
|
|
@@ -588,6 +603,7 @@ export function runCli(argv, opts = {}) {
|
|
|
588
603
|
skipInstall: argv.includes("--skip-install"),
|
|
589
604
|
skipGit: argv.includes("--skip-git"),
|
|
590
605
|
packageManager,
|
|
606
|
+
gaonjsVersion: parsed.gaonjsVersion,
|
|
591
607
|
})
|
|
592
608
|
.then((code) => {
|
|
593
609
|
process.exitCode = code;
|
package/dist/mcp/tools.js
CHANGED
|
@@ -201,6 +201,7 @@ export async function getSchemaTool(args, cwd) {
|
|
|
201
201
|
index: t.constraints.index,
|
|
202
202
|
check: t.constraints.check,
|
|
203
203
|
},
|
|
204
|
+
partitionBy: t.partitionBy ? { strategy: t.partitionBy.strategy, columns: [...t.partitionBy.columns] } : undefined,
|
|
204
205
|
}));
|
|
205
206
|
const lines = [];
|
|
206
207
|
if (tables.length === 0) {
|
|
@@ -231,10 +232,23 @@ export async function getSchemaTool(args, cwd) {
|
|
|
231
232
|
const chk = t.constraints.check ?? [];
|
|
232
233
|
if (uniq.length > 0)
|
|
233
234
|
lines.push(` unique: ${uniq.map((u) => `[${u.join(', ')}]`).join(', ')}`);
|
|
235
|
+
// 인덱스 원소는 문자열 배열(btree) 또는 method/partial/표현식 객체(결정 273).
|
|
234
236
|
if (idx.length > 0)
|
|
235
|
-
lines.push(` index: ${idx
|
|
237
|
+
lines.push(` index: ${idx
|
|
238
|
+
.map((u) => {
|
|
239
|
+
if (Array.isArray(u))
|
|
240
|
+
return `[${u.join(', ')}]`;
|
|
241
|
+
const o = u;
|
|
242
|
+
const target = o.expr ?? `[${(o.cols ?? []).join(', ')}]`;
|
|
243
|
+
const usingSfx = o.using ? ` using ${o.using}` : '';
|
|
244
|
+
const whereSfx = o.where ? ` where ${o.where}` : '';
|
|
245
|
+
return `${target}${usingSfx}${whereSfx}`;
|
|
246
|
+
})
|
|
247
|
+
.join(', ')}`);
|
|
236
248
|
if (chk.length > 0)
|
|
237
249
|
lines.push(` check: ${chk.map(([n]) => n).join(', ')}`);
|
|
250
|
+
if (t.partitionBy)
|
|
251
|
+
lines.push(` partition: ${t.partitionBy.strategy} (${t.partitionBy.columns.join(', ')})`);
|
|
238
252
|
}
|
|
239
253
|
lines.push('');
|
|
240
254
|
lines.push(`총 ${tables.length}개 테이블`);
|
package/dist/scaffold/job.js
CHANGED
|
@@ -67,7 +67,9 @@ export function jobTestScaffold(pascalName) {
|
|
|
67
67
|
``,
|
|
68
68
|
`describe('${pascal} (실 NATS JetStream)', () => {`,
|
|
69
69
|
` it('발행한 잡이 워커에서 처리된다', async () => {`,
|
|
70
|
-
|
|
70
|
+
// 결정 317: 무인자 — connectNats 는 옵션 객체만 받는다(문자열은 조용히 무시됐다).
|
|
71
|
+
// 접속지는 NATS_URL env 폴백이 정본이다(agents/testing.md 와 동일).
|
|
72
|
+
` const nats = await connectNats()`,
|
|
71
73
|
` try {`,
|
|
72
74
|
` await expectJobProcessed(${camel}, () => ${camel}.later({ id: 'test' }), { nats })`,
|
|
73
75
|
` } finally {`,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# {{PROJECT_NAME}} 환경 변수 — gaon new 가 .env 로 자동 복제(결정 198). 값만 채워 쓰세요.
|
|
2
|
-
# gaon.config.ts
|
|
2
|
+
# gaon.config.ts 가 process.env.KEY 로 참조한다. gaon dev · gaon serve 가 자동 로드.
|
|
3
3
|
|
|
4
4
|
# DB — docker-compose.yaml 의 postgres 서비스와 정합.
|
|
5
5
|
DATABASE_URL=postgres://{{PROJECT_NAME}}:{{PROJECT_NAME}}@127.0.0.1:5432/{{PROJECT_NAME}}_dev
|
|
@@ -132,12 +132,12 @@ Gaon 의 제1 설계 목표는 **"AI 가 개발을 가장 잘하는 프레임웍
|
|
|
132
132
|
17. `method-override` — `_method` HTTP 메서드 스푸핑 hack(Gaon 미지원 · router.delete 를 쓰라) (결정 89 · 경고)
|
|
133
133
|
18. `csrf-wiring` — 비-GET 라우트(POST/PUT/PATCH/DELETE)가 있는데 `app.config.ts` 에 session 미배선 = CSRF 무방비 (결정 93 · 경고)
|
|
134
134
|
19. `internal-anchor` — 앱 내부 경로 일반 `<a href="/...">`(풀 리로드로 SPA 파손 · `Link`/`router.visit` 를 쓰라 · 외부 URL·`target="_blank"` 는 제외) (결정 96 · 경고)
|
|
135
|
-
20. `pageprops-destructure` — `const { x } = pageProps(…)` 구조분해(반응성 끊김 · 리다이렉트/리로드 후 갱신 안 됨 · `const props = pageProps(…)` 후 `props.x` 로 접근하라) (결정 99 · 경고)
|
|
135
|
+
20. `pageprops-destructure` — `const { x } = pageProps(…)`/`useShared(…)` 구조분해(반응성 끊김 · 리다이렉트/리로드 후 갱신 안 됨 · `const props = pageProps(…)` 후 `props.x` 로 접근하라 · `.vue`+앱 `.ts` 공통) (결정 99 · 302 · 경고)
|
|
136
136
|
21. `async-offload` — 컨트롤러 액션 인라인의 무거운/외부 작업(메일 SDK·이미지 처리 sharp/jimp·외부 HTTP)이 응답을 지연 (`domain/jobs/` 잡 + `.later()` 로 빼라 · JSON/API 앱 외부 호출·빠른 내부 호출은 오탐 방지로 제외) (결정 102·103 · 경고)
|
|
137
137
|
22. `page-layout-breakpoint` — 페이지 파일이 레이아웃 브레이크포인트(`sm:flex-row`·`md:grid-cols-2` 등)를 직접 사용(반응형은 UI 킷 블록이 책임 · `PageShell` 등으로 감싸라 · 킷에 없는 표현이면 그대로 둬도 됨 · 표시/타이포/여백 반응형은 오탐 방지로 제외) (결정 107 · 안내 경고)
|
|
138
138
|
23. `link-button-nesting` — `<Link><Button>…</Button></Link>` 이중 감싸기(`<a><button>` 중첩 · HTML 비준수·접근성 결함 · 버튼 모양 링크는 `<Button href="…">` 한 표면을 쓰라 · Link 직계 자식 Button 만 검출) (결정 113 · 경고)
|
|
139
139
|
24. `seal-security` — `@gaonjs/seal` 을 켠 앱에서 (a) `gaon.config.ts` 가 진짜 방어층(rate limit·보안 헤더·CORS)을 **명시적으로 껐을** 때 = 봉인을 켜고 방어를 끄는 역전 **경고**, (b) `main.ts` 가 seal 클라이언트를 배선(`@gaonjs/seal/client` 정적 import + `createGaonApp` sealClient)하지 않았을 때 = 봉인 문서를 브라우저가 못 열어 blank 가 되는 **에러**(`gaon doctor --fix --yes` 의 `seal-client-wiring` fixer 가 자동 배선). seal 은 서버 검증을 대체하지 않는다 (결정 121·124 · `agents/seal.md`)
|
|
140
|
-
25. `schema-relations` — 커넥션을 가로지르는 belongsTo·역방향 관계(SQL 조인이 커넥션을 못 넘음)와 존재하지 않는 관계 대상 = **에러**(§4.5). data 패키지 검사(`checkCrossConnectionRelations`·`checkRelationTargets`)를 CLI 러너가 배선 — 배포 후 raw postgres 에러 대신 doctor 가 잡는다 (결정 134 · `agents/data.md`)
|
|
140
|
+
25. `schema-relations` — 커넥션을 가로지르는 belongsTo·역방향 관계(SQL 조인이 커넥션을 못 넘음)와 존재하지 않는 관계 대상 = **에러**(§4.5). **파티션 키 컬럼이 실제 컬럼인지도 검사**(결정 277 · `checkPartitions` — 오타·유령 컬럼). data 패키지 검사(`checkCrossConnectionRelations`·`checkRelationTargets`·`checkPartitions`)를 CLI 러너가 배선 — 배포 후 raw postgres 에러 대신 doctor 가 잡는다 (결정 134·277 · `agents/data.md`)
|
|
141
141
|
26. `no-import-meta-env` — `.vue`(SFC) `<script>` 에서 `import.meta.env` 직접 사용 = **에러**. SFC 는 nodenext 아래 CommonJS 출력으로 분류돼 vue-tsc 가 TS1470 로 거부한다(`gaon check` red). 클라 공개 환경변수는 `import { env } from 'gaonjs/vue'` 로 읽으라(VITE_* 접두 제거·타입드 · `.gaon/env.d.ts` 는 `.env` 스캔 생성) — 템플릿 프로즈·주석의 언급은 오탐 제외 (결정 198 · `agents/frontend.md` §9)
|
|
142
142
|
27. `locale-parity` — `locales/` 의 로케일 간 키 부분 누락 = **경고**. 어떤 키가 특정 로케일에만 빠지면 `messages.d.ts`(기준 로케일 기준)는 컴파일을 통과하고, 런타임에 그 로케일 사용자는 fallback(대개 다른 언어) 번역을 조용히 본다. 검사가 로케일 간 키 diff 를 계산해 빠진 파일·키를 짚는다(`--json` 은 `detail.missing` 으로 구조화). 로케일이 0·1개면 무소음 (결정 216 · `agents/i18n.md`)
|
|
143
143
|
|
|
@@ -252,6 +252,7 @@ gaon doctor # 정적 검사 27종 (§2.2)
|
|
|
252
252
|
| `@gaonjs/core` | 메타데이터 · 직렬화 프리미티브(Hidden) |
|
|
253
253
|
| `@gaonjs/mail` / `storage` / `i18n` | 메일 · 파일 스토리지 · 다국어 |
|
|
254
254
|
| `@gaonjs/seal` | 페이로드 봉인(선택 플러그인 · `agents/seal.md`) |
|
|
255
|
+
| `@gaonjs/adapter-mongo` | MongoDB 문서형 `collection()` 어댑터(선택 · mongoose optional peer · `agents/data.md`) |
|
|
255
256
|
|
|
256
257
|
## 6. 원칙 · 엄수
|
|
257
258
|
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
Gaon 프레임웍 문서: https://gaonjs.dev
|
|
11
11
|
|
|
12
|
-
- 설계 정본(v0.
|
|
12
|
+
- 설계 정본(v0.17) · errata E-1(파사드 = `gaonjs`) · E-3(JSON 액션 +
|
|
13
13
|
`api()`) · E-4(컬럼 확장) · E-5(컴포저블·레이아웃)
|
|
14
14
|
- 앱 내 One Way(§1) — 선택지가 있는 것을 만들지 않는다. 하나로 정한다.
|
|
15
15
|
|
|
@@ -17,8 +17,9 @@ Gaon 프레임웍 문서: https://gaonjs.dev
|
|
|
17
17
|
|
|
18
18
|
1. **TypeScript 전용.** JS 파일 추가 금지. 데코레이터 금지 — 함수·객체
|
|
19
19
|
스타일(`model()`·`controller()`·`job()`) 만 쓴다.
|
|
20
|
-
2. **`.gaon/` 자동 생성 파일 편집 금지.** `routes.d.ts`·`
|
|
21
|
-
는 `gaon check` / `gaon dev`
|
|
20
|
+
2. **`.gaon/` 자동 생성 파일 편집 금지.** `routes.d.ts`·`routes.manifest.ts`·
|
|
21
|
+
`tables.d.ts`·`messages.d.ts`·`env.d.ts` 는 `gaon check` / `gaon dev` /
|
|
22
|
+
`gaon gen` 이 재생성한다.
|
|
22
23
|
3. **의존 방향 4규칙**(doctor 강제): 앱→domain 허용 · domain→앱 금지 ·
|
|
23
24
|
앱→앱 금지 · 앱→shared 허용(shared 는 앱 import 금지, domain 은
|
|
24
25
|
타입 import 만).
|
|
@@ -18,7 +18,12 @@ RUN pnpm install --frozen-lockfile
|
|
|
18
18
|
FROM base AS build
|
|
19
19
|
COPY --from=deps /app/node_modules ./node_modules
|
|
20
20
|
COPY . .
|
|
21
|
-
|
|
21
|
+
# gaon build 는 .gaon/env.d.ts 생성에 .env 가 필수(결정 198)인데 .env 는 이미지에
|
|
22
|
+
# 넣지 않는다(.dockerignore) — .env.example 을 임시 복제해 빌드하고 즉시 지운다.
|
|
23
|
+
# 지우는 이유: example 의 placeholder(SESSION_SECRET 등)가 런타임에 남으면 compose
|
|
24
|
+
# 가 env 를 안 넘겼을 때 fail-loud 검증을 조용히 통과시킨다(결정 313). 런타임 env
|
|
25
|
+
# 는 compose.prod.yaml 의 environment 가 단일 소스다.
|
|
26
|
+
RUN cp .env.example .env && pnpm build && rm -f .env
|
|
22
27
|
|
|
23
28
|
# 3) 런타임 — 소스 + 번들 + 의존을 그대로 실행.
|
|
24
29
|
FROM base AS runtime
|