@gaonjs/cli 0.3.0 → 0.5.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/__fixtures__/db-minimal/domain/schema/widgets.d.ts +12 -0
- package/dist/__fixtures__/db-minimal/domain/schema/widgets.js +7 -0
- package/dist/__fixtures__/db-minimal/gaon.config.d.ts +2 -0
- package/dist/__fixtures__/db-minimal/gaon.config.js +11 -0
- package/dist/commands/check.d.ts +31 -0
- package/dist/commands/check.js +223 -0
- package/dist/commands/console.d.ts +46 -0
- package/dist/commands/console.js +129 -0
- package/dist/commands/db.d.ts +20 -0
- package/dist/commands/db.js +74 -0
- package/dist/commands/dev.d.ts +68 -0
- package/dist/commands/dev.js +287 -0
- package/dist/commands/new.d.ts +45 -0
- package/dist/commands/new.js +274 -0
- package/dist/commands/test.d.ts +11 -0
- package/dist/commands/test.js +119 -0
- package/dist/db/diff.d.ts +17 -0
- package/dist/db/diff.js +57 -0
- package/dist/db/index.d.ts +4 -0
- package/dist/db/index.js +8 -0
- package/dist/db/migrate.d.ts +16 -0
- package/dist/db/migrate.js +173 -0
- package/dist/db/reset.d.ts +18 -0
- package/dist/db/reset.js +150 -0
- package/dist/db/resolve.d.ts +32 -0
- package/dist/db/resolve.js +130 -0
- package/dist/dev/console.d.ts +39 -0
- package/dist/dev/console.js +100 -0
- package/dist/dev/docker.d.ts +52 -0
- package/dist/dev/docker.js +163 -0
- package/dist/dev/index.d.ts +14 -0
- package/dist/dev/index.js +10 -0
- package/dist/dev/tsc.d.ts +41 -0
- package/dist/dev/tsc.js +127 -0
- package/dist/dev/watcher.d.ts +50 -0
- package/dist/dev/watcher.js +95 -0
- package/dist/dev.d.ts +1 -16
- package/dist/dev.js +10 -66
- package/dist/doctor/no-auto-import.d.ts +10 -0
- package/dist/doctor/no-auto-import.js +158 -0
- package/dist/doctor/reporter.d.ts +1 -1
- package/dist/doctor/reporter.js +16 -3
- package/dist/doctor/setup.d.ts +26 -0
- package/dist/doctor/setup.js +52 -0
- package/dist/doctor/shared-composable-purity.d.ts +8 -0
- package/dist/doctor/shared-composable-purity.js +164 -0
- package/dist/doctor/types.d.ts +14 -1
- package/dist/doctor/types.js +10 -5
- package/dist/doctor.d.ts +21 -5
- package/dist/doctor.js +77 -8
- package/dist/index.d.ts +9 -2
- package/dist/index.js +175 -27
- package/dist/templates/index.d.ts +23 -0
- package/dist/templates/index.js +66 -0
- package/dist/templates/index.ts +85 -0
- package/dist/templates/project/.env.example.tpl +18 -0
- package/dist/templates/project/.gitignore.tpl +24 -0
- package/dist/templates/project/.npmrc.tpl +4 -0
- package/dist/templates/project/CLAUDE.md.tpl +119 -0
- package/dist/templates/project/apps/web/channels/.gitkeep.tpl +1 -0
- package/dist/templates/project/apps/web/components/.gitkeep.tpl +1 -0
- package/dist/templates/project/apps/web/composables/useApiPing.ts.tpl +25 -0
- package/dist/templates/project/apps/web/controllers/home.ts.tpl +19 -0
- package/dist/templates/project/apps/web/layouts/Default.vue.tpl +43 -0
- package/dist/templates/project/apps/web/pages/Home/Index.vue.tpl +36 -0
- package/dist/templates/project/apps/web/routes.ts.tpl +8 -0
- package/dist/templates/project/docker-compose.yaml.tpl +73 -0
- package/dist/templates/project/domain/events/.gitkeep.tpl +1 -0
- package/dist/templates/project/domain/jobs/.gitkeep.tpl +1 -0
- package/dist/templates/project/domain/listeners/.gitkeep.tpl +1 -0
- package/dist/templates/project/domain/mails/.gitkeep.tpl +1 -0
- package/dist/templates/project/domain/models/.gitkeep.tpl +1 -0
- package/dist/templates/project/domain/schema/.gitkeep.tpl +1 -0
- package/dist/templates/project/domain/services/.gitkeep.tpl +1 -0
- package/dist/templates/project/gaon.config.ts.tpl +27 -0
- package/dist/templates/project/package.json.tpl +27 -0
- package/dist/templates/project/pnpm-workspace.yaml.tpl +11 -0
- package/dist/templates/project/shared/components/.gitkeep.tpl +1 -0
- package/dist/templates/project/shared/composables/useDebounce.ts.tpl +21 -0
- package/dist/templates/project/tsconfig.json.tpl +25 -0
- package/package.json +5 -5
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// @gaonjs/cli · doctor · 사전 검사 (M9-E-Fix · 크래시 하드닝)
|
|
2
|
+
//
|
|
3
|
+
// doctor 는 사용자 프로젝트의 TS 소스를 AST 로 파싱한다. 두 가지 전제:
|
|
4
|
+
// 1) 실제로 gaon 프로젝트여야 한다 (검사 대상이 존재)
|
|
5
|
+
// 2) typescript 컴파일러 API 를 불러올 수 있어야 한다
|
|
6
|
+
// 전제가 깨지면 규칙 실행 중 크래시("Cannot read properties of undefined
|
|
7
|
+
// (reading 'ES2022')" 등) 대신 우아한 안내 메시지 + exit 2 로 대체한다
|
|
8
|
+
// (CLAUDE.md §7.5.3 · 에러 = 수리 안내서).
|
|
9
|
+
import { existsSync } from 'node:fs';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import ts from 'typescript';
|
|
12
|
+
/**
|
|
13
|
+
* 프로젝트 마커 4종: gaon.config.ts · domain/ · apps/ · shared/.
|
|
14
|
+
* 하나라도 있으면 gaon 프로젝트로 인정한다. 모두 없으면 검사할 대상이
|
|
15
|
+
* 없으므로 우아한 안내 후 종료한다.
|
|
16
|
+
*/
|
|
17
|
+
export function detectProject(cwd) {
|
|
18
|
+
const markers = ['gaon.config.ts', 'domain', 'apps', 'shared'];
|
|
19
|
+
return markers.some((m) => existsSync(join(cwd, m)));
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* typescript 모듈이 doctor 가 필요로 하는 compiler API 를 노출하는지
|
|
23
|
+
* 검사한다. 예: 사용자 프로젝트가 typescript@7 을 받아 오면 default
|
|
24
|
+
* export 가 `{ version, versionMajorMinor }` 스텁만 담아
|
|
25
|
+
* `ts.ScriptTarget` 이 undefined → `ts.createSourceFile` 호출 시 크래시.
|
|
26
|
+
*
|
|
27
|
+
* 인자 tsMod 는 테스트에서 스텁을 주입하기 위해 기본값을 실 ts 로 둔다.
|
|
28
|
+
*/
|
|
29
|
+
export function checkTypeScriptApi(tsMod = ts) {
|
|
30
|
+
return (typeof tsMod.createSourceFile === 'function' &&
|
|
31
|
+
tsMod.ScriptTarget !== undefined &&
|
|
32
|
+
tsMod.ScriptTarget.ES2022 !== undefined);
|
|
33
|
+
}
|
|
34
|
+
/** 프로젝트 마커 없음 · 우아한 안내. */
|
|
35
|
+
export function fatalNoProject(cwd) {
|
|
36
|
+
return {
|
|
37
|
+
code: 'no-project',
|
|
38
|
+
message: `gaon 프로젝트를 찾지 못했습니다 (cwd: ${cwd}).\n` +
|
|
39
|
+
`마커(gaon.config.ts · domain/ · apps/ · shared/) 중 하나도 없습니다.`,
|
|
40
|
+
hint: `→ 'gaon new <name>' 으로 새 프로젝트를 만들거나 실 프로젝트 경로로 이동한 뒤 다시 실행하세요.`,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** typescript compiler API 미노출 · 우아한 안내. */
|
|
44
|
+
export function fatalTsApiMissing(installedVersion) {
|
|
45
|
+
const v = installedVersion ?? '(알 수 없음)';
|
|
46
|
+
return {
|
|
47
|
+
code: 'ts-api-missing',
|
|
48
|
+
message: `typescript 컴파일러 API 를 불러올 수 없습니다 (설치된 typescript: ${v}).\n` +
|
|
49
|
+
`doctor 는 typescript 5.x 의 AST 파서(createSourceFile · ScriptTarget)를 사용합니다.`,
|
|
50
|
+
hint: `→ 프로젝트에 typescript 5.x 를 설치하세요: 'npm i -D typescript@^5.9'.`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { DoctorCheck, RuleReport } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* 단일 shared 컴포저블 소스를 검사해 순수성 위반 목록을 낸다
|
|
4
|
+
* (단위 테스트 진입점).
|
|
5
|
+
*/
|
|
6
|
+
export declare function inspectSharedComposable(file: string, source: string, cwd: string): DoctorCheck[];
|
|
7
|
+
/** shared/composables/ 를 훑어 순수성 위반을 모두 낸다. */
|
|
8
|
+
export declare function checkSharedComposablePurity(cwd: string): Promise<RuleReport>;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// @gaonjs/cli · doctor · shared 컴포저블 순수성 검사 (M9-E 확장 · errata E-5 §2.2)
|
|
2
|
+
//
|
|
3
|
+
// shared/composables/ 는 shared 컴포넌트의 "props 로만" 규칙과 정확히
|
|
4
|
+
// 같은 구도를 따른다. 라우트를 몰라야 하고(api·pageProps 금지) · domain
|
|
5
|
+
// 은 타입으로만 참조해야 한다. 필요한 데이터·호출 함수는 인자로 받는다.
|
|
6
|
+
// 정본 근거: errata E-5 §2.2 (컴포저블·레이아웃 관례 · 결정 25).
|
|
7
|
+
//
|
|
8
|
+
// 검사 대상:
|
|
9
|
+
// 1) 프레임웍 모듈(gaonjs · gaonjs/vue · @gaonjs/vue · @gaonjs/web) 에서
|
|
10
|
+
// 'api' · 'pageProps' 를 value import 하면 error.
|
|
11
|
+
// - type-only 는 무해(구조 참조뿐 · 라우트 지식 필요 없음).
|
|
12
|
+
// 2) domain 을 value import 하면 error.
|
|
13
|
+
// - type-only 는 허용(모델 Row 타입 등 · 순수 타입 참조).
|
|
14
|
+
//
|
|
15
|
+
// 방법: shared/composables/*.ts 를 TS AST 로 파싱해 import 선언만 훑는다.
|
|
16
|
+
// 상대 import 는 실 파일까지 해석하지 않고 경로 접두사(domain/) 로 판정 —
|
|
17
|
+
// 이 검사는 순수성 게이트라 정확도보다 재현성이 우선(false positive 는
|
|
18
|
+
// 오히려 안전).
|
|
19
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
20
|
+
import { join, relative, resolve, dirname } from 'node:path';
|
|
21
|
+
import ts from 'typescript';
|
|
22
|
+
/** 라우트 지식을 담은 심볼 — shared 는 참조할 수 없다. */
|
|
23
|
+
const FORBIDDEN_FRAMEWORK_NAMES = new Set(['api', 'pageProps']);
|
|
24
|
+
/**
|
|
25
|
+
* 프레임웍 모듈 패턴. 문서 표기(gaon/vue)는 실 패키지 이름의 짧은
|
|
26
|
+
* 별칭이며, 실 배포본은 gaonjs 파사드 subpath 와 @gaonjs 스코프를 쓴다.
|
|
27
|
+
* 하나만 잡으면 우회가 쉬우므로 알려진 표기 3종을 모두 매치한다.
|
|
28
|
+
*/
|
|
29
|
+
const FRAMEWORK_MODULE_PATTERNS = [
|
|
30
|
+
/^gaonjs$/,
|
|
31
|
+
/^gaonjs\/vue$/,
|
|
32
|
+
/^@gaonjs\/vue$/,
|
|
33
|
+
/^@gaonjs\/web$/,
|
|
34
|
+
];
|
|
35
|
+
/** import 선언에서 이름과 type-only 플래그를 추출한다. */
|
|
36
|
+
function collectImportedNames(node) {
|
|
37
|
+
const out = [];
|
|
38
|
+
const clause = node.importClause;
|
|
39
|
+
if (!clause)
|
|
40
|
+
return out;
|
|
41
|
+
const clauseTypeOnly = clause.isTypeOnly;
|
|
42
|
+
if (clause.name)
|
|
43
|
+
out.push({ name: clause.name.text, typeOnly: clauseTypeOnly });
|
|
44
|
+
const bindings = clause.namedBindings;
|
|
45
|
+
if (bindings) {
|
|
46
|
+
if (ts.isNamespaceImport(bindings)) {
|
|
47
|
+
out.push({ name: bindings.name.text, typeOnly: clauseTypeOnly });
|
|
48
|
+
}
|
|
49
|
+
else if (ts.isNamedImports(bindings)) {
|
|
50
|
+
for (const el of bindings.elements) {
|
|
51
|
+
out.push({ name: el.name.text, typeOnly: clauseTypeOnly || el.isTypeOnly });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
function isRelativeSpecifier(s) {
|
|
58
|
+
return s.startsWith('./') || s.startsWith('../');
|
|
59
|
+
}
|
|
60
|
+
function matchesFrameworkModule(spec) {
|
|
61
|
+
return FRAMEWORK_MODULE_PATTERNS.some((re) => re.test(spec));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* 단일 shared 컴포저블 소스를 검사해 순수성 위반 목록을 낸다
|
|
65
|
+
* (단위 테스트 진입점).
|
|
66
|
+
*/
|
|
67
|
+
export function inspectSharedComposable(file, source, cwd) {
|
|
68
|
+
const issues = [];
|
|
69
|
+
const rel = relative(cwd, file);
|
|
70
|
+
const sf = ts.createSourceFile(file, source, ts.ScriptTarget.ES2022, true);
|
|
71
|
+
const baseDir = dirname(file);
|
|
72
|
+
const visit = (node) => {
|
|
73
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
74
|
+
const spec = node.moduleSpecifier.text;
|
|
75
|
+
const { line } = sf.getLineAndCharacterOfPosition(node.getStart(sf));
|
|
76
|
+
const names = collectImportedNames(node);
|
|
77
|
+
// 1) 프레임웍의 api/pageProps 를 value import → error
|
|
78
|
+
if (matchesFrameworkModule(spec)) {
|
|
79
|
+
for (const n of names) {
|
|
80
|
+
if (n.typeOnly)
|
|
81
|
+
continue; // type-only 는 라우트 실행 지식이 아님
|
|
82
|
+
if (!FORBIDDEN_FRAMEWORK_NAMES.has(n.name))
|
|
83
|
+
continue;
|
|
84
|
+
issues.push({
|
|
85
|
+
rule: 'shared-composable-purity',
|
|
86
|
+
level: 'error',
|
|
87
|
+
file: rel,
|
|
88
|
+
line: line + 1,
|
|
89
|
+
message: `${rel} (line ${line + 1})\n` +
|
|
90
|
+
` '${spec}' 의 '${n.name}' import 발견 · shared 는 라우트를 몰라야 함\n` +
|
|
91
|
+
`→ 옵션: apps/<앱>/composables/ 로 옮기거나 · ${n.name} 호출을 인자로 받도록 바꾸라`,
|
|
92
|
+
detail: {
|
|
93
|
+
kind: 'framework-api',
|
|
94
|
+
module: spec,
|
|
95
|
+
name: n.name,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// 2) domain value import → error (type-only 는 허용)
|
|
101
|
+
if (isRelativeSpecifier(spec)) {
|
|
102
|
+
const abs = resolve(baseDir, spec);
|
|
103
|
+
const parts = relative(cwd, abs).split(/[\\/]/).filter(Boolean);
|
|
104
|
+
if (parts[0] === 'domain') {
|
|
105
|
+
const valueNames = names.filter((n) => !n.typeOnly);
|
|
106
|
+
if (valueNames.length > 0) {
|
|
107
|
+
issues.push({
|
|
108
|
+
rule: 'shared-composable-purity',
|
|
109
|
+
level: 'error',
|
|
110
|
+
file: rel,
|
|
111
|
+
line: line + 1,
|
|
112
|
+
message: `${rel} (line ${line + 1})\n` +
|
|
113
|
+
` domain 값 import 발견 · shared 컴포저블은 domain 을 타입으로만 참조해야 함\n` +
|
|
114
|
+
`→ 'import type { ... } from ...' 형태로 바꾸거나 · 필요한 값은 인자로 받도록 바꾸라`,
|
|
115
|
+
detail: {
|
|
116
|
+
kind: 'domain-value',
|
|
117
|
+
module: spec,
|
|
118
|
+
names: valueNames.map((n) => n.name),
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
ts.forEachChild(node, visit);
|
|
126
|
+
};
|
|
127
|
+
visit(sf);
|
|
128
|
+
return issues;
|
|
129
|
+
}
|
|
130
|
+
/** shared/composables/ 를 훑어 순수성 위반을 모두 낸다. */
|
|
131
|
+
export async function checkSharedComposablePurity(cwd) {
|
|
132
|
+
const composablesDir = join(cwd, 'shared', 'composables');
|
|
133
|
+
const files = [];
|
|
134
|
+
await collectTsFiles(composablesDir, files);
|
|
135
|
+
const issues = [];
|
|
136
|
+
for (const file of files) {
|
|
137
|
+
const src = await readFile(file, 'utf8');
|
|
138
|
+
issues.push(...inspectSharedComposable(file, src, cwd));
|
|
139
|
+
}
|
|
140
|
+
return { rule: 'shared-composable-purity', issues };
|
|
141
|
+
}
|
|
142
|
+
async function collectTsFiles(root, out) {
|
|
143
|
+
let entries;
|
|
144
|
+
try {
|
|
145
|
+
entries = (await readdir(root, { withFileTypes: true }));
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
for (const e of entries) {
|
|
151
|
+
const name = e.name;
|
|
152
|
+
if (name === 'node_modules' || name === 'dist' || name === '.gaon')
|
|
153
|
+
continue;
|
|
154
|
+
const full = join(root, name);
|
|
155
|
+
if (e.isDirectory())
|
|
156
|
+
await collectTsFiles(full, out);
|
|
157
|
+
else if (e.isFile() &&
|
|
158
|
+
name.endsWith('.ts') &&
|
|
159
|
+
!name.endsWith('.d.ts') &&
|
|
160
|
+
!name.endsWith('.test.ts')) {
|
|
161
|
+
out.push(full);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
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';
|
|
1
|
+
export type DoctorRule = 'response-mixing' | 'n-plus-one' | 'dependency-direction' | 'connections' | 'migration-diff' | 'shared-composable-purity' | 'no-auto-import';
|
|
2
2
|
export type DoctorLevel = 'passed' | 'warning' | 'error';
|
|
3
3
|
export interface DoctorCheck {
|
|
4
4
|
readonly rule: DoctorRule;
|
|
@@ -15,6 +15,19 @@ export interface DoctorResult {
|
|
|
15
15
|
readonly passed: readonly DoctorCheck[];
|
|
16
16
|
readonly warnings: readonly DoctorCheck[];
|
|
17
17
|
readonly errors: readonly DoctorCheck[];
|
|
18
|
+
/**
|
|
19
|
+
* 사전 검사 실패(프로젝트 없음 · TS API 미노출) 시에만 채워진다.
|
|
20
|
+
* 존재하면 runCli 는 exit 2 로 종료한다(사용자 오류 · 크래시 아님).
|
|
21
|
+
*/
|
|
22
|
+
readonly fatal?: DoctorFatal;
|
|
23
|
+
}
|
|
24
|
+
/** 사전 검사 실패 원인 코드. */
|
|
25
|
+
export type DoctorFatalCode = 'no-project' | 'ts-api-missing';
|
|
26
|
+
/** 규칙 실행이 시작될 수 없는 상황. runCli 는 exit 2 로 매핑. */
|
|
27
|
+
export interface DoctorFatal {
|
|
28
|
+
readonly code: DoctorFatalCode;
|
|
29
|
+
readonly message: string;
|
|
30
|
+
readonly hint: string;
|
|
18
31
|
}
|
|
19
32
|
/** 하위 검사 모듈이 상위에서 조립되도록 반환하는 결과 묶음. */
|
|
20
33
|
export interface RuleReport {
|
package/dist/doctor/types.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
// @gaonjs/cli · doctor 공용 타입 (M9-E)
|
|
1
|
+
// @gaonjs/cli · doctor 공용 타입 (M9-E · M9-E-Fix · M9-E 확장 · E-5)
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// · migration-diff
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// 7 검사(response-mixing · n-plus-one · dependency-direction · connections
|
|
4
|
+
// · migration-diff · shared-composable-purity · no-auto-import)가 모두
|
|
5
|
+
// 이 DoctorCheck 를 낸다. 상위(runDoctorCommand)는 level 로 passed/
|
|
6
|
+
// warnings/errors 로 갈라 담는다. 자동화(CI)는 JSON 을 파싱해
|
|
7
|
+
// errors.length > 0 이면 fail 로 판단한다.
|
|
8
|
+
//
|
|
9
|
+
// 사전 검사 실패(프로젝트 마커 없음 · TS API 미노출)는 규칙 실행 자체가
|
|
10
|
+
// 불가능한 상황이므로 별도 `fatal` 필드로 표현한다 — runCli 는 이 경우
|
|
11
|
+
// exit 2(사용자 오류)로 매핑한다(M9-E-Fix · CLAUDE.md §7.5.3).
|
|
7
12
|
export function makeResult(reports) {
|
|
8
13
|
const passed = [];
|
|
9
14
|
const warnings = [];
|
package/dist/doctor.d.ts
CHANGED
|
@@ -1,22 +1,38 @@
|
|
|
1
|
-
import { type DoctorCheck, type DoctorLevel, type DoctorResult, type DoctorRule, type RuleReport } from './doctor/types.js';
|
|
2
|
-
export type { DoctorCheck, DoctorLevel, DoctorResult, DoctorRule, RuleReport };
|
|
1
|
+
import { type DoctorCheck, type DoctorFatal, type DoctorFatalCode, type DoctorLevel, type DoctorResult, type DoctorRule, type RuleReport } from './doctor/types.js';
|
|
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
5
|
export { inspectControllerForNPlusOne, checkNPlusOne } from './doctor/n-plus-one.js';
|
|
6
6
|
export { extractRelativeImports, checkDependencyDirection } from './doctor/dependency-direction.js';
|
|
7
7
|
export { extractConfigDbKeys, extractKeyUses, checkConnections } from './doctor/connections.js';
|
|
8
8
|
export { scanSchema, checkMigrationDiff } from './doctor/migration-diff.js';
|
|
9
|
+
export { inspectSharedComposable, checkSharedComposablePurity, } from './doctor/shared-composable-purity.js';
|
|
10
|
+
export { inspectConfigForAutoImport, inspectPackageJson, checkNoAutoImport, } from './doctor/no-auto-import.js';
|
|
9
11
|
export { renderHuman, renderJson } from './doctor/reporter.js';
|
|
12
|
+
export { checkTypeScriptApi, detectProject, fatalNoProject, fatalTsApiMissing, } from './doctor/setup.js';
|
|
10
13
|
export interface DoctorCommandOptions {
|
|
11
14
|
readonly cwd?: string;
|
|
12
15
|
readonly json?: boolean;
|
|
13
16
|
readonly checks?: readonly DoctorRule[];
|
|
14
17
|
}
|
|
15
18
|
/**
|
|
16
|
-
* `gaon doctor` 진입점 (M9-E · 확장).
|
|
19
|
+
* `gaon doctor` 진입점 (M9-E · 확장 · M9-E-Fix 하드닝).
|
|
17
20
|
*
|
|
18
|
-
* 반환: DoctorResult({passed, warnings, errors}).
|
|
19
|
-
* exit code 는 진입점(runCli)이
|
|
21
|
+
* 반환: DoctorResult({passed, warnings, errors, fatal?}).
|
|
22
|
+
* exit code 는 진입점(runCli)이 결정한다:
|
|
23
|
+
* - result.fatal 이 있으면 → exit 2 (사용자 오류 · 크래시 아님)
|
|
24
|
+
* - result.errors.length > 0 → exit 1
|
|
25
|
+
* - 그 외 → exit 0
|
|
26
|
+
*
|
|
27
|
+
* 사전 검사 2 단계:
|
|
28
|
+
* 1) detectProject — gaon 프로젝트 마커가 하나도 없으면 fatal:no-project
|
|
29
|
+
* 로 종료(예: 사용자가 실수로 빈 폴더에서 실행).
|
|
30
|
+
* 2) checkTypeScriptApi — typescript 컴파일러 API 가 없으면(예: 사용자
|
|
31
|
+
* 프로젝트가 typescript@7 을 받았을 때 default export 가 스텁이라
|
|
32
|
+
* ts.ScriptTarget 이 undefined) fatal:ts-api-missing 로 종료 —
|
|
33
|
+
* "Cannot read properties of undefined (reading 'ES2022')" 크래시 대신.
|
|
34
|
+
* 규칙 실행 중 예외는 각 규칙 단위로 catch → 그 규칙만 error 로 리포트,
|
|
35
|
+
* 나머지 규칙은 계속 실행(부분 결과 확보).
|
|
20
36
|
*/
|
|
21
37
|
export declare function runDoctorCommand(opts?: DoctorCommandOptions): Promise<DoctorResult>;
|
|
22
38
|
export interface DoctorIssue {
|
package/dist/doctor.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @gaonjs/cli · `gaon doctor` — 정적 검사 (M9-E · CLI DX 완성)
|
|
2
|
+
* @gaonjs/cli · `gaon doctor` — 정적 검사 (M9-E · CLI DX 완성 · E-5 확장)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 7 검사를 조립한다:
|
|
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 규칙)
|
|
8
8
|
* 4) connections (v0.15 §4.5)
|
|
9
9
|
* 5) migration-diff (최소 감지 · 상세는 M9-D)
|
|
10
|
+
* 6) shared-composable-purity (errata E-5 §2.2 · 결정 25)
|
|
11
|
+
* 7) no-auto-import (errata E-5 §2.4 · v0.15 §1.2)
|
|
10
12
|
*
|
|
11
13
|
* 각 검사는 순수 함수(cwd → RuleReport). 상위 runDoctorCommand 가 조립해
|
|
12
14
|
* DoctorResult 로 낸다. --json 은 자동화(CI)를 위해 반드시 파싱 가능한
|
|
@@ -16,21 +18,28 @@
|
|
|
16
18
|
* 그대로 export — 기존 테스트가 계속 동작한다.
|
|
17
19
|
*/
|
|
18
20
|
import { resolve } from 'node:path';
|
|
21
|
+
import ts from 'typescript';
|
|
19
22
|
import { checkResponseMixing } from './doctor/response-mixing.js';
|
|
20
23
|
import { checkNPlusOne } from './doctor/n-plus-one.js';
|
|
21
24
|
import { checkDependencyDirection } from './doctor/dependency-direction.js';
|
|
22
25
|
import { checkConnections } from './doctor/connections.js';
|
|
23
26
|
import { checkMigrationDiff } from './doctor/migration-diff.js';
|
|
27
|
+
import { checkSharedComposablePurity } from './doctor/shared-composable-purity.js';
|
|
28
|
+
import { checkNoAutoImport } from './doctor/no-auto-import.js';
|
|
24
29
|
import { renderHuman, renderJson } from './doctor/reporter.js';
|
|
30
|
+
import { checkTypeScriptApi, detectProject, fatalNoProject, fatalTsApiMissing, } from './doctor/setup.js';
|
|
25
31
|
import { makeResult, } from './doctor/types.js';
|
|
26
32
|
export { inspectControllerSource, checkResponseMixing } from './doctor/response-mixing.js';
|
|
27
33
|
export { inspectControllerForNPlusOne, checkNPlusOne } from './doctor/n-plus-one.js';
|
|
28
34
|
export { extractRelativeImports, checkDependencyDirection } from './doctor/dependency-direction.js';
|
|
29
35
|
export { extractConfigDbKeys, extractKeyUses, checkConnections } from './doctor/connections.js';
|
|
30
36
|
export { scanSchema, checkMigrationDiff } from './doctor/migration-diff.js';
|
|
37
|
+
export { inspectSharedComposable, checkSharedComposablePurity, } from './doctor/shared-composable-purity.js';
|
|
38
|
+
export { inspectConfigForAutoImport, inspectPackageJson, checkNoAutoImport, } from './doctor/no-auto-import.js';
|
|
31
39
|
export { renderHuman, renderJson } from './doctor/reporter.js';
|
|
40
|
+
export { checkTypeScriptApi, detectProject, fatalNoProject, fatalTsApiMissing, } from './doctor/setup.js';
|
|
32
41
|
/**
|
|
33
|
-
* 실행할 검사 이름. 지정 없음(undefined) =
|
|
42
|
+
* 실행할 검사 이름. 지정 없음(undefined) = 7개 모두.
|
|
34
43
|
*/
|
|
35
44
|
const ALL_RULES = [
|
|
36
45
|
'response-mixing',
|
|
@@ -38,6 +47,8 @@ const ALL_RULES = [
|
|
|
38
47
|
'dependency-direction',
|
|
39
48
|
'connections',
|
|
40
49
|
'migration-diff',
|
|
50
|
+
'shared-composable-purity',
|
|
51
|
+
'no-auto-import',
|
|
41
52
|
];
|
|
42
53
|
const CHECKERS = {
|
|
43
54
|
'response-mixing': checkResponseMixing,
|
|
@@ -45,24 +56,82 @@ const CHECKERS = {
|
|
|
45
56
|
'dependency-direction': checkDependencyDirection,
|
|
46
57
|
connections: checkConnections,
|
|
47
58
|
'migration-diff': checkMigrationDiff,
|
|
59
|
+
'shared-composable-purity': checkSharedComposablePurity,
|
|
60
|
+
'no-auto-import': checkNoAutoImport,
|
|
48
61
|
};
|
|
49
62
|
/**
|
|
50
|
-
* `gaon doctor` 진입점 (M9-E · 확장).
|
|
63
|
+
* `gaon doctor` 진입점 (M9-E · 확장 · M9-E-Fix 하드닝).
|
|
51
64
|
*
|
|
52
|
-
* 반환: DoctorResult({passed, warnings, errors}).
|
|
53
|
-
* exit code 는 진입점(runCli)이
|
|
65
|
+
* 반환: DoctorResult({passed, warnings, errors, fatal?}).
|
|
66
|
+
* exit code 는 진입점(runCli)이 결정한다:
|
|
67
|
+
* - result.fatal 이 있으면 → exit 2 (사용자 오류 · 크래시 아님)
|
|
68
|
+
* - result.errors.length > 0 → exit 1
|
|
69
|
+
* - 그 외 → exit 0
|
|
70
|
+
*
|
|
71
|
+
* 사전 검사 2 단계:
|
|
72
|
+
* 1) detectProject — gaon 프로젝트 마커가 하나도 없으면 fatal:no-project
|
|
73
|
+
* 로 종료(예: 사용자가 실수로 빈 폴더에서 실행).
|
|
74
|
+
* 2) checkTypeScriptApi — typescript 컴파일러 API 가 없으면(예: 사용자
|
|
75
|
+
* 프로젝트가 typescript@7 을 받았을 때 default export 가 스텁이라
|
|
76
|
+
* ts.ScriptTarget 이 undefined) fatal:ts-api-missing 로 종료 —
|
|
77
|
+
* "Cannot read properties of undefined (reading 'ES2022')" 크래시 대신.
|
|
78
|
+
* 규칙 실행 중 예외는 각 규칙 단위로 catch → 그 규칙만 error 로 리포트,
|
|
79
|
+
* 나머지 규칙은 계속 실행(부분 결과 확보).
|
|
54
80
|
*/
|
|
55
81
|
export async function runDoctorCommand(opts = {}) {
|
|
56
82
|
const cwd = opts.cwd ?? process.cwd();
|
|
57
83
|
const root = resolve(cwd);
|
|
84
|
+
const json = opts.json ?? false;
|
|
85
|
+
// 1) 프로젝트 마커 검사
|
|
86
|
+
if (!detectProject(root)) {
|
|
87
|
+
const result = {
|
|
88
|
+
passed: [],
|
|
89
|
+
warnings: [],
|
|
90
|
+
errors: [],
|
|
91
|
+
fatal: fatalNoProject(root),
|
|
92
|
+
};
|
|
93
|
+
process.stdout.write((json ? renderJson(result) : renderHuman(result)) + '\n');
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
// 2) typescript compiler API 검사
|
|
97
|
+
if (!checkTypeScriptApi()) {
|
|
98
|
+
const installed = ts.version;
|
|
99
|
+
const result = {
|
|
100
|
+
passed: [],
|
|
101
|
+
warnings: [],
|
|
102
|
+
errors: [],
|
|
103
|
+
fatal: fatalTsApiMissing(installed),
|
|
104
|
+
};
|
|
105
|
+
process.stdout.write((json ? renderJson(result) : renderHuman(result)) + '\n');
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
58
108
|
const rules = opts.checks && opts.checks.length ? opts.checks : ALL_RULES;
|
|
59
109
|
const reports = [];
|
|
60
110
|
for (const rule of rules) {
|
|
61
111
|
const fn = CHECKERS[rule];
|
|
62
|
-
|
|
112
|
+
try {
|
|
113
|
+
reports.push(await fn(root));
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
// 규칙 하나가 크래시해도 나머지 규칙 실행은 계속한다. 사용자는
|
|
117
|
+
// 크래시 대신 어느 규칙이 왜 실패했는지 안내받는다.
|
|
118
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
119
|
+
reports.push({
|
|
120
|
+
rule,
|
|
121
|
+
issues: [
|
|
122
|
+
{
|
|
123
|
+
rule,
|
|
124
|
+
level: 'error',
|
|
125
|
+
message: `${rule} 검사 중 예외 발생: ${msg}\n` +
|
|
126
|
+
`→ 나머지 규칙만 임시로 실행하려면 '--check=<다른 규칙>' 를 쓰세요.`,
|
|
127
|
+
detail: { thrown: msg },
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
});
|
|
131
|
+
}
|
|
63
132
|
}
|
|
64
133
|
const result = makeResult(reports);
|
|
65
|
-
const out =
|
|
134
|
+
const out = json ? renderJson(result) : renderHuman(result);
|
|
66
135
|
process.stdout.write(out + '\n');
|
|
67
136
|
return result;
|
|
68
137
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { type DoctorRule } from "./doctor.js";
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
2
|
+
export { startDev, resolveDevLayout, type DevDeps, type DevLayout, type DevApp, type DevEvent, type DevHandle, } from "./dev.js";
|
|
3
|
+
export { runDevCommand, type DevCommandOptions } from "./commands/dev.js";
|
|
4
|
+
export { createDevConsole, findComposeFile, isDockerAvailable, inspectCompose, composeUp, composeDown, ensureInfra, startTscWatchers, killChild, startRestartWatcher, isRestartChange, resolveWatchRoots, type DevConsole, type DevConsoleOptions, type DevSource, type DevLevel, type ComposeStatus, type ComposeUpOptions, type EnsureInfraResult, type DockerLocateOptions, type TscWatcherOptions, type TscWatcherHandle, type RestartWatcherOptions, type RestartWatcherHandle, } from "./dev/index.js";
|
|
5
|
+
export { runCheckCommand, type CheckCommandOptions, type CheckStep, type CheckStepStatus, type CheckStepResult, } from "./commands/check.js";
|
|
6
|
+
export { runNewCommand, type NewCommandOptions, type NewCommandResult } from "./commands/new.js";
|
|
7
|
+
export { runConsoleCommand, type ConsoleCommandOptions } from "./commands/console.js";
|
|
8
|
+
export { runTestCommand, type TestCommandOptions, type TestScope } from "./commands/test.js";
|
|
4
9
|
export { writeAuthScaffold, authScaffoldFiles, patchRoutes, runGenerateAuthCommand, type AuthScaffoldOptions, type ScaffoldFile, type ScaffoldResult, type GenerateAuthOptions, } from "./generate.js";
|
|
5
10
|
export { runGenerateCommand, planScaffold, parseGenerateArgs, type GenerateType, type GenerateOptions, type GenerateResult, } from "./commands/g.js";
|
|
6
11
|
export { runHubCommand, type HubCommandOptions } from "./hub.js";
|
|
@@ -8,6 +13,8 @@ export { runServeCommand, type ServeCommandOptions } from "./serve.js";
|
|
|
8
13
|
export { runWorkCommand, type WorkCommandOptions } from "./work.js";
|
|
9
14
|
export { runJobsCommand, type JobsCommandOptions } from "./jobs.js";
|
|
10
15
|
export { runDbSeedCommand, loadSeed, type DbSeedOptions, type DbSeedResult } from "./db.js";
|
|
16
|
+
export { runDbCommand, type DbSubcommand, type DbCommandOptions, } from "./commands/db.js";
|
|
17
|
+
export { runDbDiff, runDbMigrate, runDbReset, resolveDbTarget, type DbDiffOptions, type DbDiffResult, type DbMigrateOptions, type DbMigrateResult, type DbResetOptions, type DbResetResult, type ResolveDbOptions, type ResolvedDbTarget, } from "./db/index.js";
|
|
11
18
|
export { runDoctorCommand, runDoctor, inspectControllerSource, checkResponseMixing, checkNPlusOne, checkDependencyDirection, checkConnections, checkMigrationDiff, renderHuman, renderJson, type DoctorResult, type DoctorCheck, type DoctorLevel, type DoctorRule, type RuleReport, type DoctorIssue, type LegacyDoctorResult, type ActionUsage, type ResponseKind, type DoctorCommandOptions, } from "./doctor.js";
|
|
12
19
|
export { loadDomain, type LoadedDomain } from "./domain.js";
|
|
13
20
|
export interface RoadmapReport {
|