@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.
Files changed (81) hide show
  1. package/dist/__fixtures__/db-minimal/domain/schema/widgets.d.ts +12 -0
  2. package/dist/__fixtures__/db-minimal/domain/schema/widgets.js +7 -0
  3. package/dist/__fixtures__/db-minimal/gaon.config.d.ts +2 -0
  4. package/dist/__fixtures__/db-minimal/gaon.config.js +11 -0
  5. package/dist/commands/check.d.ts +31 -0
  6. package/dist/commands/check.js +223 -0
  7. package/dist/commands/console.d.ts +46 -0
  8. package/dist/commands/console.js +129 -0
  9. package/dist/commands/db.d.ts +20 -0
  10. package/dist/commands/db.js +74 -0
  11. package/dist/commands/dev.d.ts +68 -0
  12. package/dist/commands/dev.js +287 -0
  13. package/dist/commands/new.d.ts +45 -0
  14. package/dist/commands/new.js +274 -0
  15. package/dist/commands/test.d.ts +11 -0
  16. package/dist/commands/test.js +119 -0
  17. package/dist/db/diff.d.ts +17 -0
  18. package/dist/db/diff.js +57 -0
  19. package/dist/db/index.d.ts +4 -0
  20. package/dist/db/index.js +8 -0
  21. package/dist/db/migrate.d.ts +16 -0
  22. package/dist/db/migrate.js +173 -0
  23. package/dist/db/reset.d.ts +18 -0
  24. package/dist/db/reset.js +150 -0
  25. package/dist/db/resolve.d.ts +32 -0
  26. package/dist/db/resolve.js +130 -0
  27. package/dist/dev/console.d.ts +39 -0
  28. package/dist/dev/console.js +100 -0
  29. package/dist/dev/docker.d.ts +52 -0
  30. package/dist/dev/docker.js +163 -0
  31. package/dist/dev/index.d.ts +14 -0
  32. package/dist/dev/index.js +10 -0
  33. package/dist/dev/tsc.d.ts +41 -0
  34. package/dist/dev/tsc.js +127 -0
  35. package/dist/dev/watcher.d.ts +50 -0
  36. package/dist/dev/watcher.js +95 -0
  37. package/dist/dev.d.ts +1 -16
  38. package/dist/dev.js +10 -66
  39. package/dist/doctor/no-auto-import.d.ts +10 -0
  40. package/dist/doctor/no-auto-import.js +158 -0
  41. package/dist/doctor/reporter.d.ts +1 -1
  42. package/dist/doctor/reporter.js +16 -3
  43. package/dist/doctor/setup.d.ts +26 -0
  44. package/dist/doctor/setup.js +52 -0
  45. package/dist/doctor/shared-composable-purity.d.ts +8 -0
  46. package/dist/doctor/shared-composable-purity.js +164 -0
  47. package/dist/doctor/types.d.ts +14 -1
  48. package/dist/doctor/types.js +10 -5
  49. package/dist/doctor.d.ts +21 -5
  50. package/dist/doctor.js +77 -8
  51. package/dist/index.d.ts +9 -2
  52. package/dist/index.js +175 -27
  53. package/dist/templates/index.d.ts +23 -0
  54. package/dist/templates/index.js +66 -0
  55. package/dist/templates/index.ts +85 -0
  56. package/dist/templates/project/.env.example.tpl +18 -0
  57. package/dist/templates/project/.gitignore.tpl +24 -0
  58. package/dist/templates/project/.npmrc.tpl +4 -0
  59. package/dist/templates/project/CLAUDE.md.tpl +119 -0
  60. package/dist/templates/project/apps/web/channels/.gitkeep.tpl +1 -0
  61. package/dist/templates/project/apps/web/components/.gitkeep.tpl +1 -0
  62. package/dist/templates/project/apps/web/composables/useApiPing.ts.tpl +25 -0
  63. package/dist/templates/project/apps/web/controllers/home.ts.tpl +19 -0
  64. package/dist/templates/project/apps/web/layouts/Default.vue.tpl +43 -0
  65. package/dist/templates/project/apps/web/pages/Home/Index.vue.tpl +36 -0
  66. package/dist/templates/project/apps/web/routes.ts.tpl +8 -0
  67. package/dist/templates/project/docker-compose.yaml.tpl +73 -0
  68. package/dist/templates/project/domain/events/.gitkeep.tpl +1 -0
  69. package/dist/templates/project/domain/jobs/.gitkeep.tpl +1 -0
  70. package/dist/templates/project/domain/listeners/.gitkeep.tpl +1 -0
  71. package/dist/templates/project/domain/mails/.gitkeep.tpl +1 -0
  72. package/dist/templates/project/domain/models/.gitkeep.tpl +1 -0
  73. package/dist/templates/project/domain/schema/.gitkeep.tpl +1 -0
  74. package/dist/templates/project/domain/services/.gitkeep.tpl +1 -0
  75. package/dist/templates/project/gaon.config.ts.tpl +27 -0
  76. package/dist/templates/project/package.json.tpl +27 -0
  77. package/dist/templates/project/pnpm-workspace.yaml.tpl +11 -0
  78. package/dist/templates/project/shared/components/.gitkeep.tpl +1 -0
  79. package/dist/templates/project/shared/composables/useDebounce.ts.tpl +21 -0
  80. package/dist/templates/project/tsconfig.json.tpl +25 -0
  81. package/package.json +5 -5
@@ -0,0 +1,127 @@
1
+ /**
2
+ * @gaonjs/cli · dev/tsc — tsc / vue-tsc watch 스포너 (M9-C)
3
+ *
4
+ * `gaon dev` 는 백그라운드에서 두 개의 타입 검사기를 --watch 모드로 띄운다:
5
+ * - tsc : 프로젝트 전체 TS 소스 (packages/*, apps/*, domain/*)
6
+ * - vue-tsc : .vue 템플릿 포함 타입 검사
7
+ *
8
+ * 두 프로세스 모두 stdout/stderr 를 통합 콘솔로 파이프 — 타입 에러가 나는
9
+ * 순간 개발자가 즉시 알 수 있다. tsconfig.json 은 프로젝트 관례대로 cwd
10
+ * 의 것을 쓴다(없으면 스킵). SIGINT 시 두 자식을 확실히 종료한다.
11
+ *
12
+ * 무의존 원칙 — child_process.spawn 만 쓴다. node_modules/.bin 을 우선
13
+ * 탐색하고, 없으면 npx 로 폴백한다(사용자 환경 편차 방어).
14
+ */
15
+ import { existsSync } from 'node:fs';
16
+ import { join, resolve } from 'node:path';
17
+ import { spawn } from 'node:child_process';
18
+ /** node_modules/.bin/<name> 이 있으면 그 경로, 없으면 undefined. */
19
+ function findLocalBin(cwd, name) {
20
+ const local = resolve(cwd, 'node_modules', '.bin', name);
21
+ if (existsSync(local))
22
+ return local;
23
+ // 모노레포 루트로 한 단계 위 탐색(사용자 프로젝트가 workspace 인 경우).
24
+ const parent = resolve(cwd, '..', '..', 'node_modules', '.bin', name);
25
+ if (existsSync(parent))
26
+ return parent;
27
+ return undefined;
28
+ }
29
+ /** tsc 또는 vue-tsc 를 --watch 로 띄운다. bin 을 못 찾으면 undefined. */
30
+ function spawnWatcher(source, cwd, project, onLine) {
31
+ const bin = findLocalBin(cwd, source);
32
+ if (!bin)
33
+ return undefined;
34
+ // --noEmit 은 워치도 지원(파일만 검사). --pretty false 로 색상 코드를
35
+ // 우리 콘솔에 맡긴다(중복 색상 방지).
36
+ const args = ['--noEmit', '--watch', '--preserveWatchOutput', '--pretty', 'false', '-p', project];
37
+ const child = spawn(bin, args, {
38
+ cwd,
39
+ stdio: ['ignore', 'pipe', 'pipe'],
40
+ env: { ...process.env, FORCE_COLOR: '0' },
41
+ });
42
+ const forward = (chunk, level) => {
43
+ if (!onLine)
44
+ return;
45
+ for (const line of chunk.toString('utf8').split('\n')) {
46
+ const t = line.trimEnd();
47
+ if (t.length === 0)
48
+ continue;
49
+ onLine(source, t, level);
50
+ }
51
+ };
52
+ child.stdout?.on('data', (b) => forward(b, 'info'));
53
+ child.stderr?.on('data', (b) => forward(b, 'error'));
54
+ return child;
55
+ }
56
+ /**
57
+ * tsc + vue-tsc 워처 두 개를 띄운다. project 파일이 없으면 스킵.
58
+ * 개별 워처를 끄는 옵션(no-tsc · no-vue-tsc)은 호출자가 결정한다.
59
+ */
60
+ export function startTscWatchers(opts) {
61
+ const cwd = resolve(opts.cwd ?? process.cwd());
62
+ const project = opts.project ?? join(cwd, 'tsconfig.json');
63
+ const children = [];
64
+ if (!existsSync(project)) {
65
+ // 사용자 프로젝트가 아직 tsconfig 를 갖고 있지 않으면 조용히 스킵.
66
+ // (신규 프로젝트 초기 상태 방어 — 에러가 아니라 no-op.)
67
+ return { stop: async () => { }, children };
68
+ }
69
+ if (opts.enableTsc !== false) {
70
+ const c = spawnWatcher('tsc', cwd, project, opts.onLine);
71
+ if (c) {
72
+ children.push(c);
73
+ c.on('exit', (code) => opts.onExit?.('tsc', code));
74
+ }
75
+ else {
76
+ opts.onLine?.('tsc', `tsc 를 찾지 못했습니다 (node_modules/.bin/tsc). → pnpm install 후 다시 시도하세요.`, 'error');
77
+ }
78
+ }
79
+ if (opts.enableVueTsc !== false) {
80
+ const c = spawnWatcher('vue-tsc', cwd, project, opts.onLine);
81
+ if (c) {
82
+ children.push(c);
83
+ c.on('exit', (code) => opts.onExit?.('vue-tsc', code));
84
+ }
85
+ else {
86
+ // vue-tsc 는 프론트가 Vue 가 아니면 없을 수 있음 — 정보성 로그.
87
+ opts.onLine?.('vue-tsc', `vue-tsc 를 찾지 못했습니다 (node_modules/.bin/vue-tsc). Vue 프로젝트라면 pnpm install 로 설치하세요.`, 'info');
88
+ }
89
+ }
90
+ return {
91
+ children,
92
+ async stop() {
93
+ // SIGTERM → 2초 유예 → SIGKILL. tsc/vue-tsc 는 SIGTERM 에 잘 반응한다.
94
+ await Promise.all(children.map((c) => killChild(c, 2000)));
95
+ },
96
+ };
97
+ }
98
+ /** SIGTERM 후 timeoutMs 안에 안 죽으면 SIGKILL. */
99
+ export function killChild(child, timeoutMs) {
100
+ return new Promise((resolvePromise) => {
101
+ if (child.exitCode !== null || child.signalCode !== null) {
102
+ resolvePromise();
103
+ return;
104
+ }
105
+ const to = setTimeout(() => {
106
+ if (child.exitCode === null && child.signalCode === null) {
107
+ try {
108
+ child.kill('SIGKILL');
109
+ }
110
+ catch {
111
+ /* 이미 죽었으면 무시 */
112
+ }
113
+ }
114
+ }, timeoutMs);
115
+ child.once('exit', () => {
116
+ clearTimeout(to);
117
+ resolvePromise();
118
+ });
119
+ try {
120
+ child.kill('SIGTERM');
121
+ }
122
+ catch {
123
+ clearTimeout(to);
124
+ resolvePromise();
125
+ }
126
+ });
127
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * @gaonjs/cli · dev/watcher — 서버 재시작 트리거 워처 (M9-C)
3
+ *
4
+ * `gaon dev` 오케스트레이터는 서버 코드(apps/*, domain/*, gaon.config.ts,
5
+ * packages/*)의 변경을 감시해 서버 자식 프로세스를 재시작한다. 이 모듈은
6
+ * "무엇을 감시하고, 무엇을 무시할지"만 결정한다 — 재시작 정책은 호출자가
7
+ * 콜백에서 처리한다(관심사 분리).
8
+ *
9
+ * .gaon 파이프라인(tables.d.ts · routes.d.ts 재생성)은 별도(dev.ts 의
10
+ * startDev) 라 여기선 다루지 않는다. 그쪽 파일들은 이 워처의 무시 목록에
11
+ * 들어있어 재시작이 유발되지 않는다(무한 루프 방지).
12
+ *
13
+ * 무의존 — @gaonjs/data 의 watchDir 프리미티브(node:fs.watch 기반)를 재사용.
14
+ */
15
+ export interface RestartWatcherOptions {
16
+ /** 프로젝트 루트(기본 process.cwd). */
17
+ readonly cwd?: string;
18
+ /**
19
+ * 변경이 감지되면 호출. changed 는 감시 루트 기준 상대경로 목록이다.
20
+ * 호출자가 여기서 서버 자식을 재시작한다.
21
+ */
22
+ readonly onRestart: (changed: string[]) => void | Promise<void>;
23
+ /** 에러 훅. 생략 시 무시(런타임을 죽이지 않기 위함). */
24
+ readonly onError?: (err: Error) => void;
25
+ /** debounce(ms). 저장 폭풍을 한 배치로 묶기 위해. 기본 200ms. */
26
+ readonly debounceMs?: number;
27
+ }
28
+ export interface RestartWatcherHandle {
29
+ /** 감시 목록(진단 · 테스트용). */
30
+ readonly roots: readonly string[];
31
+ close(): void;
32
+ }
33
+ /**
34
+ * 재시작을 유발하는 변경인가.
35
+ * - 확장자가 RESTART_EXTS 중 하나
36
+ * - 경로 조각에 IGNORE_SEGMENTS 가 없음
37
+ * - .test.ts / .spec.ts 는 서버 실행에 영향 없으니 무시
38
+ * - .d.ts 는 생성물이므로 무시(스캐폴드/재생성이 만듦)
39
+ */
40
+ export declare function isRestartChange(rel: string): boolean;
41
+ /**
42
+ * 감시할 루트 후보를 프로젝트 관례로 해석한다. 존재하는 것만 담아 반환.
43
+ */
44
+ export declare function resolveWatchRoots(cwd: string): string[];
45
+ /**
46
+ * 서버 재시작 워처를 띄운다. 각 루트에 대해 recursive watcher 를 하나씩
47
+ * 걸고, 필터로 재시작 파일만 통과시킨다. 파일 하나(gaon.config.ts)를 감시할
48
+ * 때도 watchDir 은 부모 디렉터리를 잡고 filter 로 걸러낸다.
49
+ */
50
+ export declare function startRestartWatcher(opts: RestartWatcherOptions): RestartWatcherHandle;
@@ -0,0 +1,95 @@
1
+ /**
2
+ * @gaonjs/cli · dev/watcher — 서버 재시작 트리거 워처 (M9-C)
3
+ *
4
+ * `gaon dev` 오케스트레이터는 서버 코드(apps/*, domain/*, gaon.config.ts,
5
+ * packages/*)의 변경을 감시해 서버 자식 프로세스를 재시작한다. 이 모듈은
6
+ * "무엇을 감시하고, 무엇을 무시할지"만 결정한다 — 재시작 정책은 호출자가
7
+ * 콜백에서 처리한다(관심사 분리).
8
+ *
9
+ * .gaon 파이프라인(tables.d.ts · routes.d.ts 재생성)은 별도(dev.ts 의
10
+ * startDev) 라 여기선 다루지 않는다. 그쪽 파일들은 이 워처의 무시 목록에
11
+ * 들어있어 재시작이 유발되지 않는다(무한 루프 방지).
12
+ *
13
+ * 무의존 — @gaonjs/data 의 watchDir 프리미티브(node:fs.watch 기반)를 재사용.
14
+ */
15
+ import { existsSync } from 'node:fs';
16
+ import { join, resolve } from 'node:path';
17
+ import { watchDir } from '@gaonjs/data';
18
+ // 재시작을 유발하는 확장자. 다른 것(이미지 · 마크다운 등)은 무시.
19
+ const RESTART_EXTS = ['.ts', '.mts', '.cts', '.js', '.mjs', '.cjs', '.json', '.vue'];
20
+ // 재시작을 유발하지 않는 경로 조각(생성물 · 캐시 · 테스트 산출물).
21
+ const IGNORE_SEGMENTS = ['.gaon', 'node_modules', 'dist', '.git', 'coverage', '.turbo'];
22
+ /**
23
+ * 재시작을 유발하는 변경인가.
24
+ * - 확장자가 RESTART_EXTS 중 하나
25
+ * - 경로 조각에 IGNORE_SEGMENTS 가 없음
26
+ * - .test.ts / .spec.ts 는 서버 실행에 영향 없으니 무시
27
+ * - .d.ts 는 생성물이므로 무시(스캐폴드/재생성이 만듦)
28
+ */
29
+ export function isRestartChange(rel) {
30
+ if (!rel)
31
+ return false;
32
+ const norm = rel.replace(/\\/g, '/');
33
+ for (const seg of IGNORE_SEGMENTS) {
34
+ if (norm.startsWith(`${seg}/`) || norm.includes(`/${seg}/`) || norm === seg)
35
+ return false;
36
+ }
37
+ if (norm.endsWith('.d.ts'))
38
+ return false;
39
+ if (/\.(test|spec)\.[cm]?[tj]sx?$/.test(norm))
40
+ return false;
41
+ return RESTART_EXTS.some((ext) => norm.endsWith(ext));
42
+ }
43
+ /**
44
+ * 감시할 루트 후보를 프로젝트 관례로 해석한다. 존재하는 것만 담아 반환.
45
+ */
46
+ export function resolveWatchRoots(cwd) {
47
+ const root = resolve(cwd);
48
+ const candidates = [
49
+ join(root, 'apps'),
50
+ join(root, 'domain'),
51
+ join(root, 'shared'),
52
+ join(root, 'packages'),
53
+ join(root, 'gaon.config.ts'),
54
+ join(root, 'gaon.config.js'),
55
+ ];
56
+ return candidates.filter((p) => existsSync(p));
57
+ }
58
+ /**
59
+ * 서버 재시작 워처를 띄운다. 각 루트에 대해 recursive watcher 를 하나씩
60
+ * 걸고, 필터로 재시작 파일만 통과시킨다. 파일 하나(gaon.config.ts)를 감시할
61
+ * 때도 watchDir 은 부모 디렉터리를 잡고 filter 로 걸러낸다.
62
+ */
63
+ export function startRestartWatcher(opts) {
64
+ const cwd = resolve(opts.cwd ?? process.cwd());
65
+ const debounceMs = opts.debounceMs ?? 200;
66
+ const onError = opts.onError ?? (() => { });
67
+ const roots = resolveWatchRoots(cwd);
68
+ const handles = [];
69
+ // 디렉터리 vs 단일 파일 구분 — 파일이면 부모 디렉터리를 감시하고
70
+ // filter 에서 정확 매칭한다.
71
+ for (const rootPath of roots) {
72
+ const isFile = rootPath.endsWith('.ts') || rootPath.endsWith('.js');
73
+ const watchTarget = isFile ? resolve(rootPath, '..') : rootPath;
74
+ const fileBaseName = isFile ? rootPath.slice(watchTarget.length + 1) : undefined;
75
+ handles.push(watchDir(watchTarget, {
76
+ debounceMs,
77
+ filter: (rel) => {
78
+ if (fileBaseName)
79
+ return rel === fileBaseName;
80
+ return isRestartChange(rel);
81
+ },
82
+ onChange: async (changed) => {
83
+ await opts.onRestart(changed);
84
+ },
85
+ onError,
86
+ }));
87
+ }
88
+ return {
89
+ roots,
90
+ close() {
91
+ for (const h of handles)
92
+ h.close();
93
+ },
94
+ };
95
+ }
package/dist/dev.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type WatchHandle, type WatchOptions } from '@gaonjs/data';
1
+ import type { WatchHandle, WatchOptions } from '@gaonjs/data';
2
2
  export interface DevApp {
3
3
  readonly name: string;
4
4
  readonly appDir: string;
@@ -42,18 +42,3 @@ export interface DevHandle {
42
42
  export declare function startDev(deps: DevDeps): Promise<DevHandle>;
43
43
  /** cwd 관례로 프로젝트 레이아웃을 해석한다(존재하는 것만 포함). */
44
44
  export declare function resolveDevLayout(cwd: string): DevLayout;
45
- export interface DevCommandOptions {
46
- readonly cwd?: string;
47
- readonly json?: boolean;
48
- /** 프로세스 시그널 등록·해제(테스트에서 주입 가능). 기본 process. */
49
- readonly signals?: {
50
- on(sig: 'SIGINT' | 'SIGTERM', fn: () => void): void;
51
- off(sig: 'SIGINT' | 'SIGTERM', fn: () => void): void;
52
- };
53
- }
54
- /**
55
- * `gaon dev` 진입점. cwd 관례로 레이아웃을 엮고 워치 루프를 띄운 뒤
56
- * SIGINT/SIGTERM 에 graceful stop. 프로세스는 워처가 살아 있는 동안
57
- * 유지된다(활성 핸들). 반환 프라미스는 종료 시 resolve.
58
- */
59
- export declare function runDevCommand(opts?: DevCommandOptions): Promise<void>;
package/dist/dev.js CHANGED
@@ -1,19 +1,18 @@
1
1
  /**
2
- * @gaonjs/cli · `gaon dev` — .gaon 갱신 파이프라인 (M3)
2
+ * @gaonjs/cli · dev — .gaon 갱신 파이프라인 (M3 · v0.15 §6.3)
3
3
  *
4
4
  * 파일 워처(@gaonjs/data watchDir) → 재생성(tables.d.ts·routes.d.ts) →
5
- * 디스크 mtime 갱신 → 편집기 tsserver 자동 반영(§6.3, 열린 질문 3).
5
+ * 디스크 mtime 갱신 → 편집기 tsserver 자동 반영. 오케스트레이션은
6
+ * 주입(deps)으로 순수하게 유지하고(테스트 용이 · dbCli 패턴 동일),
7
+ * 상위(gaon dev 오케스트레이터 · commands/dev.ts)가 이 startDev 를 재사용한다.
6
8
  *
7
- * 오케스트레이션은 주입(deps)으로 순수하게 유지하고(테스트 용이·dbCli
8
- * 패턴과 동일), CLI 진입점이 cwd 관례로 실제 경로·재생성기를 엮는다.
9
- * 실제 서버·Vite·Docker 인프라 기동은 후속 마일스톤이다 M3
10
- * 타입 브리지 갱신 루프만 담당한다.
9
+ * M9-C 이전에는 파일이 gaon dev 진입점(runDevCommand)도 겸했으나,
10
+ * M9-C 에서 인프라·서버·타입 검사기를 통합하는 진입점이 commands/dev.ts
11
+ * 이동했다. 파일은 .gaon 재생성 코어(startDev · resolveDevLayout)
12
+ * 유지한다.
11
13
  */
12
14
  import { readdirSync, existsSync } from 'node:fs';
13
15
  import { join, resolve } from 'node:path';
14
- import { generateTablesDts, watchDir } from '@gaonjs/data';
15
- import { generateRoutesDts } from '@gaonjs/web';
16
- import { registerTsResolve } from './tsResolve.js';
17
16
  const isSchemaChange = (f) => f.endsWith('.ts') && !f.endsWith('.d.ts') && !f.endsWith('.test.ts') && !f.includes('.gaon');
18
17
  const isRoutesChange = (f) => !f.includes('.gaon') &&
19
18
  f.endsWith('.ts') &&
@@ -94,60 +93,5 @@ export function resolveDevLayout(cwd) {
94
93
  apps: apps.sort((a, b) => a.name.localeCompare(b.name)),
95
94
  };
96
95
  }
97
- /** 사람이 읽는 이벤트 텍스트. */
98
- function humanEvent(e) {
99
- switch (e.kind) {
100
- case 'ready': {
101
- const parts = [];
102
- if (e.schema)
103
- parts.push('schema→tables.d.ts');
104
- for (const a of e.apps)
105
- parts.push(`${a}→routes.d.ts`);
106
- const what = parts.length ? parts.join(', ') : '(감시 대상 없음 — domain/schema·apps/* 확인)';
107
- return ` gaon dev · 워치 시작 — ${what}\n 파일을 저장하면 .gaon 이 재생성되고 편집기가 자동 반영합니다. (Ctrl+C 종료)`;
108
- }
109
- case 'regen':
110
- return e.target === 'tables'
111
- ? ' ↻ tables.d.ts 재생성'
112
- : ` ↻ ${e.app}/.gaon/routes.d.ts 재생성`;
113
- case 'stopped':
114
- return ' gaon dev · 종료';
115
- }
116
- }
117
- /**
118
- * `gaon dev` 진입점. cwd 관례로 레이아웃을 엮고 워치 루프를 띄운 뒤
119
- * SIGINT/SIGTERM 에 graceful stop. 프로세스는 워처가 살아 있는 동안
120
- * 유지된다(활성 핸들). 반환 프라미스는 종료 시 resolve.
121
- */
122
- export async function runDevCommand(opts = {}) {
123
- const cwd = opts.cwd ?? process.cwd();
124
- const json = opts.json ?? false;
125
- const signals = opts.signals ?? process;
126
- registerTsResolve(); // 사용자 .ts 의 `.js` 상대 import 를 런타임에 해석
127
- const layout = resolveDevLayout(cwd);
128
- const emit = (e) => {
129
- if (json)
130
- process.stdout.write(JSON.stringify(e) + '\n');
131
- else
132
- process.stdout.write(humanEvent(e) + '\n');
133
- };
134
- const deps = {
135
- layout,
136
- regenerateTables: generateTablesDts,
137
- regenerateRoutes: generateRoutesDts,
138
- watch: watchDir,
139
- log: emit,
140
- onError: (err) => process.stderr.write(` ✗ ${err.message}\n`),
141
- };
142
- const handle = await startDev(deps);
143
- await new Promise((resolvePromise) => {
144
- const stop = () => {
145
- signals.off('SIGINT', stop);
146
- signals.off('SIGTERM', stop);
147
- handle.close();
148
- resolvePromise();
149
- };
150
- signals.on('SIGINT', stop);
151
- signals.on('SIGTERM', stop);
152
- });
153
- }
96
+ // runDevCommand · DevCommandOptions M9-C 에서 commands/dev.ts 로 이동.
97
+ // 이 파일은 .gaon 재생성 코어(startDev · resolveDevLayout) 유지한다.
@@ -0,0 +1,10 @@
1
+ import type { DoctorCheck, RuleReport } from './types.js';
2
+ /**
3
+ * config 파일 하나의 소스에서 자동 import 플러그인 import 를 잡는다
4
+ * (단위 테스트 진입점).
5
+ */
6
+ export declare function inspectConfigForAutoImport(file: string, source: string, cwd: string): DoctorCheck[];
7
+ /** package.json 안의 deps 4종을 훑어 자동 import 플러그인 유무를 낸다. */
8
+ export declare function inspectPackageJson(file: string, source: string, cwd: string): DoctorCheck[];
9
+ /** 프로젝트 루트를 훑어 자동 import 설정·의존을 잡는다. */
10
+ export declare function checkNoAutoImport(cwd: string): Promise<RuleReport>;
@@ -0,0 +1,158 @@
1
+ // @gaonjs/cli · doctor · 자동 import 감지 (M9-E 확장 · errata E-5 §2.4)
2
+ //
3
+ // Nuxt 식 자동 import(컴포넌트·컴포저블을 import 문 없이 사용)는 넣지
4
+ // 않는다. 출처가 코드에 보이지 않는 마법은 AI 가 "이 심볼이 어디서
5
+ // 왔는지"를 추측하게 만들어 v0.15 §1.2 (관례로 추측을 없앤다) 와 정면
6
+ // 충돌한다. 모든 컴포넌트·컴포저블은 명시적으로 import 한다.
7
+ //
8
+ // 검사 대상 (E-5 §2.4 · v0.15 §1.2):
9
+ // 1) 프로젝트 루트의 대표 config 파일 안의 import 선언에서
10
+ // 'unplugin-auto-import' · 'unplugin-vue-components' · 'unimport'
11
+ // 발견 시 error.
12
+ // 2) package.json 의 dependencies · devDependencies · peerDependencies
13
+ // · optionalDependencies 에 위 3종이 있으면 error.
14
+ //
15
+ // 방법: config 는 TS AST 로 import 선언만 훑고 · package.json 은 JSON
16
+ // 파싱 + 라인 검색(에러 메시지에 라인을 붙이기 위해 원문에서 재검색).
17
+ import { existsSync } from 'node:fs';
18
+ import { readFile } from 'node:fs/promises';
19
+ import { join, relative } from 'node:path';
20
+ import ts from 'typescript';
21
+ /** 금지 플러그인 목록 — 자동 import 를 도입하는 대표 3종. */
22
+ const FORBIDDEN_PLUGINS = [
23
+ 'unplugin-auto-import',
24
+ 'unplugin-vue-components',
25
+ 'unimport',
26
+ ];
27
+ /**
28
+ * 검사할 config 파일. Vite/Nuxt/Vue-CLI 관례 파일명을 모두 훑는다.
29
+ * 존재하지 않는 파일은 스킵.
30
+ */
31
+ const CONFIG_FILES = [
32
+ 'vite.config.ts',
33
+ 'vite.config.js',
34
+ 'vite.config.mjs',
35
+ 'nuxt.config.ts',
36
+ 'nuxt.config.js',
37
+ 'vue.config.js',
38
+ 'vue.config.ts',
39
+ ];
40
+ /**
41
+ * config 파일 하나의 소스에서 자동 import 플러그인 import 를 잡는다
42
+ * (단위 테스트 진입점).
43
+ */
44
+ export function inspectConfigForAutoImport(file, source, cwd) {
45
+ const issues = [];
46
+ const rel = relative(cwd, file);
47
+ const sf = ts.createSourceFile(file, source, ts.ScriptTarget.ES2022, true);
48
+ const visit = (node) => {
49
+ if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
50
+ const spec = node.moduleSpecifier.text;
51
+ const plugin = matchForbidden(spec);
52
+ if (plugin) {
53
+ const { line } = sf.getLineAndCharacterOfPosition(node.getStart(sf));
54
+ issues.push({
55
+ rule: 'no-auto-import',
56
+ level: 'error',
57
+ file: rel,
58
+ line: line + 1,
59
+ message: `${rel} (line ${line + 1})\n` +
60
+ ` '${plugin}' 발견 · v0.15 §1.2 (관례로 추측 없앤다) 정합 X\n` +
61
+ `→ 자동 import 플러그인 제거 · 명시 import 사용`,
62
+ detail: { plugin, source: 'config-import', module: spec },
63
+ });
64
+ }
65
+ }
66
+ // require('unplugin-...') 형태(CJS config)도 잡는다.
67
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'require') {
68
+ const arg = node.arguments[0];
69
+ if (arg && ts.isStringLiteral(arg)) {
70
+ const plugin = matchForbidden(arg.text);
71
+ if (plugin) {
72
+ const { line } = sf.getLineAndCharacterOfPosition(node.getStart(sf));
73
+ issues.push({
74
+ rule: 'no-auto-import',
75
+ level: 'error',
76
+ file: rel,
77
+ line: line + 1,
78
+ message: `${rel} (line ${line + 1})\n` +
79
+ ` '${plugin}' 발견 · v0.15 §1.2 (관례로 추측 없앤다) 정합 X\n` +
80
+ `→ 자동 import 플러그인 제거 · 명시 import 사용`,
81
+ detail: { plugin, source: 'config-require', module: arg.text },
82
+ });
83
+ }
84
+ }
85
+ }
86
+ ts.forEachChild(node, visit);
87
+ };
88
+ visit(sf);
89
+ return issues;
90
+ }
91
+ /** 지정한 모듈 이름이 금지 목록에 걸리면 해당 플러그인 이름을 낸다. */
92
+ function matchForbidden(spec) {
93
+ return FORBIDDEN_PLUGINS.find((p) => spec === p || spec.startsWith(p + '/'));
94
+ }
95
+ /** package.json 안의 deps 4종을 훑어 자동 import 플러그인 유무를 낸다. */
96
+ export function inspectPackageJson(file, source, cwd) {
97
+ const issues = [];
98
+ const rel = relative(cwd, file);
99
+ let pkg;
100
+ try {
101
+ pkg = JSON.parse(source);
102
+ }
103
+ catch {
104
+ // JSON 파싱 실패 시 이 규칙은 아무 것도 하지 않는다 — 다른 규칙 도구가
105
+ // 잡을 문제(예: gaon check 의 tsconfig 파싱)이지 자동 import 검사의 몫이 아님.
106
+ return [];
107
+ }
108
+ const allDeps = {
109
+ ...(pkg.dependencies ?? {}),
110
+ ...(pkg.devDependencies ?? {}),
111
+ ...(pkg.peerDependencies ?? {}),
112
+ ...(pkg.optionalDependencies ?? {}),
113
+ };
114
+ for (const plugin of FORBIDDEN_PLUGINS) {
115
+ if (!(plugin in allDeps))
116
+ continue;
117
+ const line = findLineForKey(source, plugin);
118
+ const loc = line ? ` (line ${line})` : '';
119
+ issues.push({
120
+ rule: 'no-auto-import',
121
+ level: 'error',
122
+ file: rel,
123
+ line: line,
124
+ message: `${rel}${loc}\n` +
125
+ ` package.json 에 '${plugin}' 발견 · v0.15 §1.2 (관례로 추측 없앤다) 정합 X\n` +
126
+ `→ 자동 import 플러그인 제거 · 명시 import 사용 · 'pnpm remove ${plugin}' 후 vite/nuxt config 도 정리`,
127
+ detail: { plugin, source: 'package-json' },
128
+ });
129
+ }
130
+ return issues;
131
+ }
132
+ /** package.json 원문에서 `"<key>"` 가 처음 나오는 1-기반 라인 번호. */
133
+ function findLineForKey(source, key) {
134
+ const lines = source.split(/\r?\n/);
135
+ const needle = `"${key}"`;
136
+ for (let i = 0; i < lines.length; i++) {
137
+ if (lines[i].includes(needle))
138
+ return i + 1;
139
+ }
140
+ return undefined;
141
+ }
142
+ /** 프로젝트 루트를 훑어 자동 import 설정·의존을 잡는다. */
143
+ export async function checkNoAutoImport(cwd) {
144
+ const issues = [];
145
+ for (const cfg of CONFIG_FILES) {
146
+ const full = join(cwd, cfg);
147
+ if (!existsSync(full))
148
+ continue;
149
+ const src = await readFile(full, 'utf8');
150
+ issues.push(...inspectConfigForAutoImport(full, src, cwd));
151
+ }
152
+ const pkgPath = join(cwd, 'package.json');
153
+ if (existsSync(pkgPath)) {
154
+ const src = await readFile(pkgPath, 'utf8');
155
+ issues.push(...inspectPackageJson(pkgPath, src, cwd));
156
+ }
157
+ return { rule: 'no-auto-import', issues };
158
+ }
@@ -1,5 +1,5 @@
1
1
  import type { DoctorResult } from './types.js';
2
- /** JSON 출력 문자열(끝 개행 없음). */
2
+ /** JSON 출력 문자열(끝 개행 없음). fatal 이 있으면 그대로 포함된다. */
3
3
  export declare function renderJson(result: DoctorResult): string;
4
4
  /** 사람이 읽는 요약(끝 개행 없음). */
5
5
  export declare function renderHuman(result: DoctorResult): string;
@@ -1,13 +1,26 @@
1
- // @gaonjs/cli · doctor · 출력 포맷 (human / JSON) — M9-E
1
+ // @gaonjs/cli · doctor · 출력 포맷 (human / JSON) — M9-E · M9-E-Fix
2
2
  //
3
3
  // human: 사람이 읽는 요약 + 규칙별 이슈 목록.
4
- // JSON: 자동화(CI)용 · {passed, warnings, errors} 그대로 직렬화 · 파싱 안정.
5
- /** JSON 출력 문자열(끝 개행 없음). */
4
+ // JSON: 자동화(CI)용 · {passed, warnings, errors, fatal?} 그대로 직렬화
5
+ // · 파싱 안정.
6
+ //
7
+ // fatal 이 있으면 규칙 실행 자체가 불가능한 상황 — 크래시 대신 우아한
8
+ // 안내(§7.5.3 · 에러 = 수리 안내서)를 출력한다.
9
+ /** JSON 출력 문자열(끝 개행 없음). fatal 이 있으면 그대로 포함된다. */
6
10
  export function renderJson(result) {
7
11
  return JSON.stringify(result);
8
12
  }
9
13
  /** 사람이 읽는 요약(끝 개행 없음). */
10
14
  export function renderHuman(result) {
15
+ if (result.fatal) {
16
+ const lines = [];
17
+ lines.push(` ✗ gaon doctor · 시작 불가 (${result.fatal.code})`);
18
+ for (const ln of result.fatal.message.split('\n')) {
19
+ lines.push(` ${ln}`);
20
+ }
21
+ lines.push(` ${result.fatal.hint}`);
22
+ return lines.join('\n');
23
+ }
11
24
  const lines = [];
12
25
  const passN = result.passed.length;
13
26
  const warnN = result.warnings.length;
@@ -0,0 +1,26 @@
1
+ import type { DoctorFatal } from './types.js';
2
+ export type { DoctorFatal, DoctorFatalCode } from './types.js';
3
+ /**
4
+ * 프로젝트 마커 4종: gaon.config.ts · domain/ · apps/ · shared/.
5
+ * 하나라도 있으면 gaon 프로젝트로 인정한다. 모두 없으면 검사할 대상이
6
+ * 없으므로 우아한 안내 후 종료한다.
7
+ */
8
+ export declare function detectProject(cwd: string): boolean;
9
+ /**
10
+ * typescript 모듈이 doctor 가 필요로 하는 compiler API 를 노출하는지
11
+ * 검사한다. 예: 사용자 프로젝트가 typescript@7 을 받아 오면 default
12
+ * export 가 `{ version, versionMajorMinor }` 스텁만 담아
13
+ * `ts.ScriptTarget` 이 undefined → `ts.createSourceFile` 호출 시 크래시.
14
+ *
15
+ * 인자 tsMod 는 테스트에서 스텁을 주입하기 위해 기본값을 실 ts 로 둔다.
16
+ */
17
+ export declare function checkTypeScriptApi(tsMod?: {
18
+ readonly ScriptTarget?: {
19
+ readonly ES2022?: number;
20
+ };
21
+ readonly createSourceFile?: unknown;
22
+ }): boolean;
23
+ /** 프로젝트 마커 없음 · 우아한 안내. */
24
+ export declare function fatalNoProject(cwd: string): DoctorFatal;
25
+ /** typescript compiler API 미노출 · 우아한 안내. */
26
+ export declare function fatalTsApiMissing(installedVersion: string | undefined): DoctorFatal;