@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
|
@@ -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
|
}
|
|
@@ -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 규칙)
|
|
@@ -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> 중첩 경고)
|
|
@@ -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;
|