@gaonjs/cli 0.47.0 → 0.55.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 +3 -1
- package/dist/commands/check.js +44 -2
- package/dist/commands/db.js +9 -0
- package/dist/commands/gen.d.ts +2 -0
- package/dist/commands/gen.js +3 -1
- package/dist/commands/new.js +13 -0
- package/dist/commands/test.js +16 -3
- package/dist/db/journal.d.ts +8 -4
- package/dist/db/journal.js +58 -14
- package/dist/db/migrate.d.ts +3 -1
- package/dist/db/migrate.js +14 -14
- package/dist/db/replay.js +3 -3
- package/dist/db/resolve.d.ts +11 -1
- package/dist/db/resolve.js +24 -2
- package/dist/db/status.js +17 -4
- package/dist/db.js +26 -5
- 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/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 +16 -5
- 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 +32 -0
- package/dist/i18n-config.js +170 -0
- package/dist/index.js +141 -29
- package/dist/mcp/tools.d.ts +1 -1
- package/dist/mcp/tools.js +13 -6
- package/dist/messages-gen.d.ts +1 -1
- package/dist/messages-gen.js +16 -5
- 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/AGENTS.md.tpl +3 -2
- package/dist/templates/project/CLAUDE.md.tpl +1 -1
- package/dist/templates/project/Dockerfile.tpl +11 -1
- package/dist/templates/project/agents/async.md.tpl +70 -14
- package/dist/templates/project/agents/data.md.tpl +173 -52
- package/dist/templates/project/agents/frontend.md.tpl +19 -4
- package/dist/templates/project/agents/i18n.md.tpl +20 -2
- package/dist/templates/project/agents/mail.md.tpl +8 -1
- package/dist/templates/project/agents/realtime.md.tpl +25 -6
- package/dist/templates/project/agents/seal.md.tpl +8 -3
- package/dist/templates/project/agents/security.md.tpl +50 -19
- package/dist/templates/project/agents/storage.md.tpl +39 -6
- package/dist/templates/project/agents/web.md.tpl +54 -22
- package/dist/work.d.ts +3 -0
- package/dist/work.js +4 -0
- package/package.json +7 -7
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, resolveLocalesDir } 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 = resolveLocalesDir(root, i18nCfg.dir);
|
|
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, resolveLocalesDir } 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 = resolveLocalesDir(cwd, analyzeProjectI18n(cwd).dir);
|
|
30
33
|
if (!existsSync(localesDir))
|
|
31
34
|
return { rule: 'locale-parity', issues: [] };
|
|
32
35
|
const resources = loadLocales(localesDir);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RuleReport } from './types.js';
|
|
2
|
+
export interface DiscardedResponse {
|
|
3
|
+
readonly file: string;
|
|
4
|
+
readonly action: string;
|
|
5
|
+
readonly method: string;
|
|
6
|
+
readonly line: number;
|
|
7
|
+
}
|
|
8
|
+
/** 소스 문자열 하나에서 "버려진 응답 호출" 을 수집한다(단위 테스트 진입점). */
|
|
9
|
+
export declare function inspectDiscardedResponses(file: string, source: string): DiscardedResponse[];
|
|
10
|
+
/** apps/ 를 훑어 버려진 응답 호출(무신호 204)을 모두 낸다(경고 등급). */
|
|
11
|
+
export declare function checkRenderReturn(cwd: string): Promise<RuleReport>;
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// @gaonjs/cli · doctor · render-return 검사 (결정 340)
|
|
2
|
+
//
|
|
3
|
+
// 액션 본문이 `this.render(...)` / `this.redirect(...)` / `this.json(...)` 을
|
|
4
|
+
// **호출만 하고 return 하지 않으면** 반환값(=응답)이 버려져 액션이 void 로 끝나고,
|
|
5
|
+
// 디스패처는 조용히 204 No Content 를 낸다 — 컴파일은 통과하고 페이지는 백지다
|
|
6
|
+
// (무신호 파손 클래스 · agents/web.md 함정 "return 누락 = 204").
|
|
7
|
+
//
|
|
8
|
+
// 정적 분석: 컨트롤러 액션 안에서 위 3종 호출이 ExpressionStatement(또는
|
|
9
|
+
// `await` 만 감싼 문장)로 서 있으면 경고. return · 변수 대입 · 인자 전달 등
|
|
10
|
+
// 값이 소비되는 위치는 오탐 방지로 제외한다. notFound/requireAuth 는 throw
|
|
11
|
+
// 기반 가드라 대상이 아니다(response-mixing 의 GUARD_METHODS 와 정합).
|
|
12
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
13
|
+
import { join, relative } from 'node:path';
|
|
14
|
+
import ts from 'typescript';
|
|
15
|
+
const RESPONSE_METHODS = new Set(['render', 'redirect', 'json']);
|
|
16
|
+
/** 소스 문자열 하나에서 "버려진 응답 호출" 을 수집한다(단위 테스트 진입점). */
|
|
17
|
+
export function inspectDiscardedResponses(file, source) {
|
|
18
|
+
const sf = ts.createSourceFile(file, source, ts.ScriptTarget.ES2022, true);
|
|
19
|
+
const found = [];
|
|
20
|
+
const visit = (node) => {
|
|
21
|
+
if (ts.isCallExpression(node) && isControllerCall(node)) {
|
|
22
|
+
const arg = node.arguments[0];
|
|
23
|
+
if (arg && ts.isObjectLiteralExpression(arg)) {
|
|
24
|
+
for (const prop of arg.properties) {
|
|
25
|
+
const action = actionName(prop);
|
|
26
|
+
const body = actionBody(prop);
|
|
27
|
+
if (!action || !body || !ts.isBlock(body))
|
|
28
|
+
continue;
|
|
29
|
+
collectDiscarded(sf, body, action, file, found);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
ts.forEachChild(node, visit);
|
|
34
|
+
};
|
|
35
|
+
visit(sf);
|
|
36
|
+
return found;
|
|
37
|
+
}
|
|
38
|
+
function isControllerCall(node) {
|
|
39
|
+
const e = node.expression;
|
|
40
|
+
if (ts.isIdentifier(e) && e.text === 'controller')
|
|
41
|
+
return true;
|
|
42
|
+
if (ts.isPropertyAccessExpression(e) && e.name.text === 'controller')
|
|
43
|
+
return true;
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
function actionName(prop) {
|
|
47
|
+
if (ts.isMethodDeclaration(prop) && ts.isIdentifier(prop.name))
|
|
48
|
+
return prop.name.text;
|
|
49
|
+
if (ts.isPropertyAssignment(prop) &&
|
|
50
|
+
ts.isIdentifier(prop.name) &&
|
|
51
|
+
(ts.isArrowFunction(prop.initializer) || ts.isFunctionExpression(prop.initializer))) {
|
|
52
|
+
return prop.name.text;
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
function actionBody(prop) {
|
|
57
|
+
if (ts.isMethodDeclaration(prop))
|
|
58
|
+
return prop.body;
|
|
59
|
+
if (ts.isPropertyAssignment(prop)) {
|
|
60
|
+
if (ts.isArrowFunction(prop.initializer))
|
|
61
|
+
return prop.initializer.body;
|
|
62
|
+
if (ts.isFunctionExpression(prop.initializer))
|
|
63
|
+
return prop.initializer.body;
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
/** 문장 위치에 버려진 this.render/redirect/json 호출이 있으면 수집한다. */
|
|
68
|
+
function collectDiscarded(sf, body, action, file, out) {
|
|
69
|
+
const visit = (node) => {
|
|
70
|
+
if (ts.isExpressionStatement(node)) {
|
|
71
|
+
const method = responseCallName(node.expression);
|
|
72
|
+
if (method) {
|
|
73
|
+
const { line } = sf.getLineAndCharacterOfPosition(node.getStart(sf));
|
|
74
|
+
out.push({ file, action, method, line: line + 1 });
|
|
75
|
+
return; // 이 문장은 판정 완료 — 내부 재방문 불요.
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// 중첩 함수(콜백) 안의 호출은 반환 의미가 달라 제외(오탐 방지).
|
|
79
|
+
if (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node))
|
|
80
|
+
return;
|
|
81
|
+
ts.forEachChild(node, visit);
|
|
82
|
+
};
|
|
83
|
+
visit(body);
|
|
84
|
+
}
|
|
85
|
+
/** 식이 (await 를 벗긴) this.render/redirect/json 호출이면 메서드 이름을 돌려준다. */
|
|
86
|
+
function responseCallName(expr) {
|
|
87
|
+
const inner = ts.isAwaitExpression(expr) ? expr.expression : expr;
|
|
88
|
+
if (!ts.isCallExpression(inner))
|
|
89
|
+
return undefined;
|
|
90
|
+
const c = inner.expression;
|
|
91
|
+
if (ts.isPropertyAccessExpression(c) &&
|
|
92
|
+
c.expression.kind === ts.SyntaxKind.ThisKeyword &&
|
|
93
|
+
RESPONSE_METHODS.has(c.name.text)) {
|
|
94
|
+
return c.name.text;
|
|
95
|
+
}
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
/** apps/ 를 훑어 버려진 응답 호출(무신호 204)을 모두 낸다(경고 등급). */
|
|
99
|
+
export async function checkRenderReturn(cwd) {
|
|
100
|
+
const appsDir = join(cwd, 'apps');
|
|
101
|
+
const issues = [];
|
|
102
|
+
for (const app of await safeListDirs(appsDir)) {
|
|
103
|
+
const ctrlDir = join(appsDir, app, 'controllers');
|
|
104
|
+
for (const file of await safeListFiles(ctrlDir)) {
|
|
105
|
+
if (!file.endsWith('.ts') || file.endsWith('.d.ts') || file.endsWith('.test.ts'))
|
|
106
|
+
continue;
|
|
107
|
+
const full = join(ctrlDir, file);
|
|
108
|
+
const source = await readFile(full, 'utf8');
|
|
109
|
+
for (const d of inspectDiscardedResponses(full, source)) {
|
|
110
|
+
const rel = relative(cwd, full);
|
|
111
|
+
issues.push({
|
|
112
|
+
rule: 'render-return',
|
|
113
|
+
level: 'warning',
|
|
114
|
+
file: rel,
|
|
115
|
+
line: d.line,
|
|
116
|
+
message: `버려진 응답 호출: ${rel}:${d.line} · 액션 '${d.action}' 이 this.${d.method}(...) 를 호출만 하고 return 하지 않습니다.\n` +
|
|
117
|
+
`→ 반환값이 곧 응답입니다(errata E-3) — return 이 없으면 액션이 void 로 끝나 조용히 204 No Content(백지)가 나갑니다.\n` +
|
|
118
|
+
`→ \`return this.${d.method}(...)\` 로 고치세요. (의도된 204 는 아무 응답 호출 없이 그냥 return 합니다.)`,
|
|
119
|
+
detail: { action: d.action, method: d.method },
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return { rule: 'render-return', issues };
|
|
125
|
+
}
|
|
126
|
+
async function safeListDirs(dir) {
|
|
127
|
+
try {
|
|
128
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
129
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function safeListFiles(dir) {
|
|
136
|
+
try {
|
|
137
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
138
|
+
return entries.filter((e) => e.isFile()).map((e) => e.name);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
}
|
package/dist/doctor/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type DoctorRule = 'response-mixing' | 'n-plus-one' | 'dependency-direction' | 'connections' | 'migration-diff' | 'shared-purity' | 'no-auto-import' | 'schema-filename' | 'agents-doc-index' | 'column-casing' | 'model-filename' | 'page-filename' | 'auth-wiring' | 'ui-kit-wiring' | 'route-registration' | 'static-collision' | 'method-override' | 'csrf-wiring' | 'internal-anchor' | 'pageprops-destructure' | 'async-offload' | 'page-layout-breakpoint' | 'link-button-nesting' | 'seal-security' | 'schema-relations' | 'no-import-meta-env' | 'locale-parity';
|
|
1
|
+
export type DoctorRule = 'response-mixing' | 'n-plus-one' | 'dependency-direction' | 'connections' | 'migration-diff' | 'shared-purity' | 'no-auto-import' | 'schema-filename' | 'agents-doc-index' | 'column-casing' | 'model-filename' | 'page-filename' | 'auth-wiring' | 'ui-kit-wiring' | 'route-registration' | 'static-collision' | 'method-override' | 'csrf-wiring' | 'internal-anchor' | 'pageprops-destructure' | 'async-offload' | 'page-layout-breakpoint' | 'link-button-nesting' | 'seal-security' | 'schema-relations' | 'no-import-meta-env' | 'locale-parity' | 'render-return';
|
|
2
2
|
export type DoctorLevel = 'passed' | 'warning' | 'error';
|
|
3
3
|
export interface DoctorCheck {
|
|
4
4
|
readonly rule: DoctorRule;
|
package/dist/doctor.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type DoctorCheck, type DoctorFatal, type DoctorFatalCode, type DoctorLe
|
|
|
2
2
|
export type { DoctorCheck, DoctorFatal, DoctorFatalCode, DoctorLevel, DoctorResult, DoctorRule, RuleReport, };
|
|
3
3
|
export type { ResponseKind, ActionUsage } from './doctor/response-mixing.js';
|
|
4
4
|
export { inspectControllerSource, checkResponseMixing } from './doctor/response-mixing.js';
|
|
5
|
+
export { inspectDiscardedResponses, checkRenderReturn } from './doctor/render-return.js';
|
|
5
6
|
export { inspectControllerForNPlusOne, checkNPlusOne } from './doctor/n-plus-one.js';
|
|
6
7
|
export { extractRelativeImports, checkDependencyDirection } from './doctor/dependency-direction.js';
|
|
7
8
|
export { extractConfigDbKeys, analyzeConfigDb, extractKeyUses, checkConnections, } from './doctor/connections.js';
|
|
@@ -26,10 +27,10 @@ export { checkLocaleParity } from './doctor/locale-parity.js';
|
|
|
26
27
|
export { renderHuman, renderJson } from './doctor/reporter.js';
|
|
27
28
|
export { checkTypeScriptApi, detectProject, fatalNoProject, fatalTsApiMissing, } from './doctor/setup.js';
|
|
28
29
|
/**
|
|
29
|
-
* 실행할 검사 이름. 지정 없음(undefined) =
|
|
30
|
+
* 실행할 검사 이름. 지정 없음(undefined) = 28개 모두.
|
|
30
31
|
*/
|
|
31
32
|
/**
|
|
32
|
-
* doctor 정적 검사
|
|
33
|
+
* doctor 정적 검사 28종의 정본 목록(§2.2). `--check=` 필터의 인정 집합도
|
|
33
34
|
* 이 배열을 단일 출처로 삼는다(parseDoctorChecks) — 새 규칙 추가 시 여기만
|
|
34
35
|
* 늘리면 실행·필터·타입이 함께 정합된다(손유지 중복 리스트 표류 방지).
|
|
35
36
|
*/
|
package/dist/doctor.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @gaonjs/cli · `gaon doctor` — 정적 검사 (M9-E · CLI DX 완성 · E-5 확장)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 28 검사를 조립한다:
|
|
5
5
|
* 1) response-mixing (errata E-3 §C · 라이브)
|
|
6
6
|
* 2) n-plus-one (errata E-4 (e))
|
|
7
7
|
* 3) dependency-direction (CLAUDE.md §5 · 4 규칙)
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
* 25) schema-relations (§4.5 · 결정 134 · 커넥션 가로지르는 belongsTo·관계 · 대상 부재 error)
|
|
30
30
|
* 26) no-import-meta-env (결정 198 · F-9 ② · `.vue` 의 import.meta.env = TS1470 → env 접근자 안내 error)
|
|
31
31
|
* 27) locale-parity (결정 216 · 13차 W4 · 로케일 간 키 부분 누락 = fallback 조용 노출 경고)
|
|
32
|
+
* 28) render-return (결정 340 · this.render/redirect/json 호출만 하고 return 누락 = 무신호 204 경고)
|
|
32
33
|
*
|
|
33
34
|
* 각 검사는 순수 함수(cwd → RuleReport). 상위 runDoctorCommand 가 조립해
|
|
34
35
|
* DoctorResult 로 낸다. --json 은 자동화(CI)를 위해 반드시 파싱 가능한
|
|
@@ -42,6 +43,7 @@ import { existsSync } from 'node:fs';
|
|
|
42
43
|
import { join, resolve } from 'node:path';
|
|
43
44
|
import ts from 'typescript';
|
|
44
45
|
import { checkResponseMixing } from './doctor/response-mixing.js';
|
|
46
|
+
import { checkRenderReturn } from './doctor/render-return.js';
|
|
45
47
|
import { checkNPlusOne } from './doctor/n-plus-one.js';
|
|
46
48
|
import { checkDependencyDirection } from './doctor/dependency-direction.js';
|
|
47
49
|
import { checkConnections } from './doctor/connections.js';
|
|
@@ -73,6 +75,7 @@ import { checkTypeScriptApi, detectProject, fatalNoProject, fatalTsApiMissing, }
|
|
|
73
75
|
import { makeResult, } from './doctor/types.js';
|
|
74
76
|
import { FIXERS, FIXER_CAPABILITIES } from './doctor/fixers/index.js';
|
|
75
77
|
export { inspectControllerSource, checkResponseMixing } from './doctor/response-mixing.js';
|
|
78
|
+
export { inspectDiscardedResponses, checkRenderReturn } from './doctor/render-return.js';
|
|
76
79
|
export { inspectControllerForNPlusOne, checkNPlusOne } from './doctor/n-plus-one.js';
|
|
77
80
|
export { extractRelativeImports, checkDependencyDirection } from './doctor/dependency-direction.js';
|
|
78
81
|
export { extractConfigDbKeys, analyzeConfigDb, extractKeyUses, checkConnections, } from './doctor/connections.js';
|
|
@@ -97,10 +100,10 @@ export { checkLocaleParity } from './doctor/locale-parity.js';
|
|
|
97
100
|
export { renderHuman, renderJson } from './doctor/reporter.js';
|
|
98
101
|
export { checkTypeScriptApi, detectProject, fatalNoProject, fatalTsApiMissing, } from './doctor/setup.js';
|
|
99
102
|
/**
|
|
100
|
-
* 실행할 검사 이름. 지정 없음(undefined) =
|
|
103
|
+
* 실행할 검사 이름. 지정 없음(undefined) = 28개 모두.
|
|
101
104
|
*/
|
|
102
105
|
/**
|
|
103
|
-
* doctor 정적 검사
|
|
106
|
+
* doctor 정적 검사 28종의 정본 목록(§2.2). `--check=` 필터의 인정 집합도
|
|
104
107
|
* 이 배열을 단일 출처로 삼는다(parseDoctorChecks) — 새 규칙 추가 시 여기만
|
|
105
108
|
* 늘리면 실행·필터·타입이 함께 정합된다(손유지 중복 리스트 표류 방지).
|
|
106
109
|
*/
|
|
@@ -132,6 +135,7 @@ export const ALL_RULES = [
|
|
|
132
135
|
'schema-relations',
|
|
133
136
|
'no-import-meta-env',
|
|
134
137
|
'locale-parity',
|
|
138
|
+
'render-return',
|
|
135
139
|
];
|
|
136
140
|
const CHECKERS = {
|
|
137
141
|
'response-mixing': checkResponseMixing,
|
|
@@ -161,6 +165,7 @@ const CHECKERS = {
|
|
|
161
165
|
'schema-relations': checkSchemaRelations,
|
|
162
166
|
'no-import-meta-env': checkNoImportMetaEnv,
|
|
163
167
|
'locale-parity': checkLocaleParity,
|
|
168
|
+
'render-return': checkRenderReturn,
|
|
164
169
|
};
|
|
165
170
|
/**
|
|
166
171
|
* 규칙을 순서대로 실행해 RuleReport[] 를 낸다. 규칙 하나가 크래시해도 나머지는
|
|
@@ -269,10 +274,16 @@ export async function runDoctorCommand(opts = {}) {
|
|
|
269
274
|
// --fix: fixer 를 계획하고, --yes 이면 실제 편집한다. reports 를 그대로
|
|
270
275
|
// 넘겨 규칙별 위반을 fixer 가 판단한다(passed 는 건너뜀).
|
|
271
276
|
const fixReport = await runDoctorFix(reports, root, opts.yes === true);
|
|
272
|
-
|
|
277
|
+
// 결정 359: 편집이 실제로 일어났으면 **재검사 결과**를 리포트·종료 코드의 기준으로
|
|
278
|
+
// 삼는다 — 적용 전 리포트로 판정하면 "전부 자동 수정됐는데 exit 1" 로 역전돼
|
|
279
|
+
// CI 자동화(--fix --yes 후 green 기대)가 깨졌다. errors/warnings = 적용 후 잔여.
|
|
280
|
+
const finalResult = fixReport.applied && fixReport.outcomes.some((o) => o.applied)
|
|
281
|
+
? makeResult(await runRules(root, rules))
|
|
282
|
+
: baseResult;
|
|
283
|
+
const resultWithFix = { ...finalResult, fix: fixReport };
|
|
273
284
|
const out = json
|
|
274
285
|
? JSON.stringify(resultWithFix)
|
|
275
|
-
: renderHuman(
|
|
286
|
+
: renderHuman(finalResult) + '\n\n' + renderFixHuman(fixReport);
|
|
276
287
|
process.stdout.write(out + '\n');
|
|
277
288
|
return resultWithFix;
|
|
278
289
|
}
|
package/dist/generate.d.ts
CHANGED
|
@@ -7,6 +7,12 @@ export interface AuthScaffoldOptions {
|
|
|
7
7
|
* `--public` 로 비-web 앱도 공개 가입을 opt-in 한다(공개 비-web 앱용 탈출구).
|
|
8
8
|
*/
|
|
9
9
|
readonly public?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* 결정 338: JWT(API 앱) 변형 — 세션·페이지 대신 토큰 발급/재발급/내 정보
|
|
12
|
+
* 컨트롤러(JSON 전용)와 `auth: { strategy:'jwt' }` 배선을 깐다. API 앱 전용이라
|
|
13
|
+
* `--app <이름>`(비-web) 이 필수다(web 은 세션이 정본 · §7).
|
|
14
|
+
*/
|
|
15
|
+
readonly jwt?: boolean;
|
|
10
16
|
}
|
|
11
17
|
/** 생성할 파일 하나 — 경로는 프로젝트 루트 기준. */
|
|
12
18
|
export interface ScaffoldFile {
|
|
@@ -19,6 +25,12 @@ export interface ScaffoldResult {
|
|
|
19
25
|
readonly patched: string[];
|
|
20
26
|
/** 손 수리가 필요한 지점 — 수리 안내 문장(§7.5.3)을 그대로 담는다. */
|
|
21
27
|
readonly warnings: string[];
|
|
28
|
+
/**
|
|
29
|
+
* 결정 361: 인증 **배선이 미완**인가(app.config auth/routes 자동 패치 실패 —
|
|
30
|
+
* 이대로면 currentUser 영구 null / 로그인 라우트 부재). 안내성 경고(JWT stateless
|
|
31
|
+
* 한계 등)와 구분해, 이 값만 종료 코드(비-0)·JSON ok:false 로 이어진다.
|
|
32
|
+
*/
|
|
33
|
+
readonly incomplete: boolean;
|
|
22
34
|
}
|
|
23
35
|
/**
|
|
24
36
|
* 결정 155·142: `g auth --app <앱>` 이 앱별 세션 secret 환경변수를 .env·.env.example
|
|
@@ -32,7 +44,7 @@ export interface ScaffoldResult {
|
|
|
32
44
|
* .env 를 새로 만드는 관례가 없어(사용자가 cp .env.example .env), 존재하는 파일만 패치한다.
|
|
33
45
|
* 반환: 실제로 키를 추가한 상대 경로들.
|
|
34
46
|
*/
|
|
35
|
-
export declare function patchEnvFiles(root: string, app: string): string[];
|
|
47
|
+
export declare function patchEnvFiles(root: string, app: string, kind?: 'session' | 'jwt'): string[];
|
|
36
48
|
/** 인증 스캐폴드가 생성하는 파일 목록(라우트 제외). 템플릿을 읽어 렌더링한다.
|
|
37
49
|
* 결정 155: 공개 가입 여부(includePublic)로 registration·Signup·dashboard 변형을 가른다. */
|
|
38
50
|
export declare function authScaffoldFiles(opts?: AuthScaffoldOptions): ScaffoldFile[];
|
|
@@ -40,6 +52,11 @@ export declare function authScaffoldFiles(opts?: AuthScaffoldOptions): ScaffoldF
|
|
|
40
52
|
* 기존 routes.ts 에 세션·회원가입 리소스를 끼워 넣는다. 이미 있으면 null.
|
|
41
53
|
* `routes((r) => {` 콜백 여는 지점 뒤에 두 줄을 삽입한다.
|
|
42
54
|
*/
|
|
55
|
+
/**
|
|
56
|
+
* 결정 338: 기존 routes.ts 에 JWT 토큰 라우트 3종을 끼워 넣는다. 이미 session#create
|
|
57
|
+
* 참조가 있으면 null(중복 방지). 삽입 지점 규칙은 patchRoutes 와 동일.
|
|
58
|
+
*/
|
|
59
|
+
export declare function patchRoutesJwt(existing: string): string | null;
|
|
43
60
|
export declare function patchRoutes(existing: string, includePublic?: boolean): string | null;
|
|
44
61
|
/**
|
|
45
62
|
* 기존 app.config.ts 에 auth 배선을 끼워 넣는다 (결정 93 · gaon new 기본 web 앱은
|
|
@@ -57,6 +74,8 @@ export interface GenerateAuthOptions {
|
|
|
57
74
|
readonly json?: boolean;
|
|
58
75
|
/** 결정 155: 비-web 앱도 공개 회원가입을 opt-in(공개 비-web 앱용 탈출구). */
|
|
59
76
|
readonly public?: boolean;
|
|
77
|
+
/** 결정 338: JWT(API 앱) 변형 — `--app <비-web 이름>` 필수 · `--public` 과 배타. */
|
|
78
|
+
readonly jwt?: boolean;
|
|
60
79
|
}
|
|
61
80
|
/** `gaon g auth` 진입점. 스캐폴드를 쓰고 결과를 사람/JSON 으로 출력한다. */
|
|
62
81
|
export declare function runGenerateAuthCommand(opts?: GenerateAuthOptions): number;
|