@mandujs/core 0.3.2 → 0.3.4

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 CHANGED
@@ -1,200 +1,200 @@
1
- <p align="center">
2
- <img src="https://raw.githubusercontent.com/konamgil/mandu/main/mandu_only_simbol.png" alt="Mandu" width="200" />
3
- </p>
4
-
5
- <h1 align="center">@mandujs/core</h1>
6
-
7
- <p align="center">
8
- <strong>Mandu Framework Core</strong><br/>
9
- Spec, Generator, Guard, Runtime, Filling
10
- </p>
11
-
12
- <p align="center">
13
- English | <a href="./README.ko.md"><strong>한국어</strong></a>
14
- </p>
15
-
16
- ## Installation
17
-
18
- ```bash
19
- bun add @mandujs/core
20
- ```
21
-
22
- > Typically used through `@mandujs/cli`. Direct usage is for advanced use cases.
23
-
24
- ## Module Structure
25
-
26
- ```
27
- @mandujs/core
28
- ├── spec/ # Spec schema and loading
29
- ├── generator/ # Code generation
30
- ├── guard/ # Architecture checking and auto-correction
31
- ├── runtime/ # Server and router
32
- └── report/ # Guard report generation
33
- ```
34
-
35
- ## Spec Module
36
-
37
- Route manifest schema definition and loading.
38
-
39
- ```typescript
40
- import { loadManifest, RoutesManifest, RouteSpec } from "@mandujs/core";
41
-
42
- // Load and validate manifest
43
- const result = await loadManifest("spec/routes.manifest.json");
44
-
45
- if (result.success && result.data) {
46
- const manifest: RoutesManifest = result.data;
47
- manifest.routes.forEach((route: RouteSpec) => {
48
- console.log(route.id, route.pattern, route.kind);
49
- });
50
- }
51
- ```
52
-
53
- ### Lock File
54
-
55
- ```typescript
56
- import { writeLock, readLock } from "@mandujs/core";
57
-
58
- // Write lock file
59
- const lock = await writeLock("spec/spec.lock.json", manifest);
60
- console.log(lock.routesHash);
61
-
62
- // Read lock file
63
- const existing = await readLock("spec/spec.lock.json");
64
- ```
65
-
66
- ## Generator Module
67
-
68
- Spec-based code generation.
69
-
70
- ```typescript
71
- import { generateRoutes, GenerateResult } from "@mandujs/core";
72
-
73
- const result: GenerateResult = await generateRoutes(manifest, "./");
74
-
75
- console.log("Created:", result.created);
76
- console.log("Skipped:", result.skipped); // Existing slot files
77
- ```
78
-
79
- ### Template Functions
80
-
81
- ```typescript
82
- import {
83
- generateApiHandler,
84
- generateApiHandlerWithSlot,
85
- generateSlotLogic,
86
- generatePageComponent
87
- } from "@mandujs/core";
88
-
89
- // Generate API handler
90
- const code = generateApiHandler(route);
91
-
92
- // API handler with slot
93
- const codeWithSlot = generateApiHandlerWithSlot(route);
94
-
95
- // Slot logic file
96
- const slotCode = generateSlotLogic(route);
97
- ```
98
-
99
- ## Guard Module
100
-
101
- Architecture rule checking and auto-correction.
102
-
103
- ```typescript
104
- import {
105
- runGuardCheck,
106
- runAutoCorrect,
107
- GuardResult,
108
- GuardViolation
109
- } from "@mandujs/core";
110
-
111
- // Run check
112
- const result: GuardResult = await runGuardCheck(manifest, "./");
113
-
114
- if (!result.passed) {
115
- result.violations.forEach((v: GuardViolation) => {
116
- console.log(`${v.rule}: ${v.message}`);
117
- });
118
-
119
- // Run auto-correction
120
- const corrected = await runAutoCorrect(result.violations, manifest, "./");
121
- console.log("Fixed:", corrected.steps);
122
- console.log("Remaining violations:", corrected.remainingViolations);
123
- }
124
- ```
125
-
126
- ### Guard Rules
127
-
128
- | Rule ID | Description | Auto-correctable |
129
- |---------|-------------|------------------|
130
- | `SPEC_HASH_MISMATCH` | Spec and lock hash mismatch | ✅ |
131
- | `GENERATED_MANUAL_EDIT` | Manual edit to generated file | ✅ |
132
- | `HANDLER_NOT_FOUND` | Handler file not found | ❌ |
133
- | `COMPONENT_NOT_FOUND` | Component file not found | ❌ |
134
- | `SLOT_NOT_FOUND` | Slot file not found | ✅ |
135
-
136
- ## Runtime Module
137
-
138
- Server startup and routing.
139
-
140
- ```typescript
141
- import {
142
- startServer,
143
- registerApiHandler,
144
- registerPageLoader
145
- } from "@mandujs/core";
146
-
147
- // Register API handler
148
- registerApiHandler("getUsers", async (req) => {
149
- return { users: [] };
150
- });
151
-
152
- // Register page loader
153
- registerPageLoader("homePage", () => import("./pages/Home"));
154
-
155
- // Start server
156
- const server = startServer(manifest, { port: 3000 });
157
-
158
- // Stop
159
- server.stop();
160
- ```
161
-
162
- ## Report Module
163
-
164
- Guard result report generation.
165
-
166
- ```typescript
167
- import { buildGuardReport } from "@mandujs/core";
168
-
169
- const report = buildGuardReport(guardResult, lockPath);
170
- console.log(report); // Formatted text report
171
- ```
172
-
173
- ## Types
174
-
175
- ```typescript
176
- import type {
177
- RoutesManifest,
178
- RouteSpec,
179
- RouteKind,
180
- SpecLock,
181
- GuardResult,
182
- GuardViolation,
183
- GenerateResult,
184
- AutoCorrectResult,
185
- } from "@mandujs/core";
186
- ```
187
-
188
- ## Requirements
189
-
190
- - Bun >= 1.0.0
191
- - React >= 18.0.0
192
- - Zod >= 3.0.0
193
-
194
- ## Related Packages
195
-
196
- - [@mandujs/cli](https://www.npmjs.com/package/@mandujs/cli) - CLI tool
197
-
198
- ## License
199
-
200
- MIT
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/konamgil/mandu/main/mandu_only_simbol.png" alt="Mandu" width="200" />
3
+ </p>
4
+
5
+ <h1 align="center">@mandujs/core</h1>
6
+
7
+ <p align="center">
8
+ <strong>Mandu Framework Core</strong><br/>
9
+ Spec, Generator, Guard, Runtime, Filling
10
+ </p>
11
+
12
+ <p align="center">
13
+ English | <a href="./README.ko.md"><strong>한국어</strong></a>
14
+ </p>
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ bun add @mandujs/core
20
+ ```
21
+
22
+ > Typically used through `@mandujs/cli`. Direct usage is for advanced use cases.
23
+
24
+ ## Module Structure
25
+
26
+ ```
27
+ @mandujs/core
28
+ ├── spec/ # Spec schema and loading
29
+ ├── generator/ # Code generation
30
+ ├── guard/ # Architecture checking and auto-correction
31
+ ├── runtime/ # Server and router
32
+ └── report/ # Guard report generation
33
+ ```
34
+
35
+ ## Spec Module
36
+
37
+ Route manifest schema definition and loading.
38
+
39
+ ```typescript
40
+ import { loadManifest, RoutesManifest, RouteSpec } from "@mandujs/core";
41
+
42
+ // Load and validate manifest
43
+ const result = await loadManifest("spec/routes.manifest.json");
44
+
45
+ if (result.success && result.data) {
46
+ const manifest: RoutesManifest = result.data;
47
+ manifest.routes.forEach((route: RouteSpec) => {
48
+ console.log(route.id, route.pattern, route.kind);
49
+ });
50
+ }
51
+ ```
52
+
53
+ ### Lock File
54
+
55
+ ```typescript
56
+ import { writeLock, readLock } from "@mandujs/core";
57
+
58
+ // Write lock file
59
+ const lock = await writeLock("spec/spec.lock.json", manifest);
60
+ console.log(lock.routesHash);
61
+
62
+ // Read lock file
63
+ const existing = await readLock("spec/spec.lock.json");
64
+ ```
65
+
66
+ ## Generator Module
67
+
68
+ Spec-based code generation.
69
+
70
+ ```typescript
71
+ import { generateRoutes, GenerateResult } from "@mandujs/core";
72
+
73
+ const result: GenerateResult = await generateRoutes(manifest, "./");
74
+
75
+ console.log("Created:", result.created);
76
+ console.log("Skipped:", result.skipped); // Existing slot files
77
+ ```
78
+
79
+ ### Template Functions
80
+
81
+ ```typescript
82
+ import {
83
+ generateApiHandler,
84
+ generateApiHandlerWithSlot,
85
+ generateSlotLogic,
86
+ generatePageComponent
87
+ } from "@mandujs/core";
88
+
89
+ // Generate API handler
90
+ const code = generateApiHandler(route);
91
+
92
+ // API handler with slot
93
+ const codeWithSlot = generateApiHandlerWithSlot(route);
94
+
95
+ // Slot logic file
96
+ const slotCode = generateSlotLogic(route);
97
+ ```
98
+
99
+ ## Guard Module
100
+
101
+ Architecture rule checking and auto-correction.
102
+
103
+ ```typescript
104
+ import {
105
+ runGuardCheck,
106
+ runAutoCorrect,
107
+ GuardResult,
108
+ GuardViolation
109
+ } from "@mandujs/core";
110
+
111
+ // Run check
112
+ const result: GuardResult = await runGuardCheck(manifest, "./");
113
+
114
+ if (!result.passed) {
115
+ result.violations.forEach((v: GuardViolation) => {
116
+ console.log(`${v.rule}: ${v.message}`);
117
+ });
118
+
119
+ // Run auto-correction
120
+ const corrected = await runAutoCorrect(result.violations, manifest, "./");
121
+ console.log("Fixed:", corrected.steps);
122
+ console.log("Remaining violations:", corrected.remainingViolations);
123
+ }
124
+ ```
125
+
126
+ ### Guard Rules
127
+
128
+ | Rule ID | Description | Auto-correctable |
129
+ |---------|-------------|------------------|
130
+ | `SPEC_HASH_MISMATCH` | Spec and lock hash mismatch | ✅ |
131
+ | `GENERATED_MANUAL_EDIT` | Manual edit to generated file | ✅ |
132
+ | `HANDLER_NOT_FOUND` | Handler file not found | ❌ |
133
+ | `COMPONENT_NOT_FOUND` | Component file not found | ❌ |
134
+ | `SLOT_NOT_FOUND` | Slot file not found | ✅ |
135
+
136
+ ## Runtime Module
137
+
138
+ Server startup and routing.
139
+
140
+ ```typescript
141
+ import {
142
+ startServer,
143
+ registerApiHandler,
144
+ registerPageLoader
145
+ } from "@mandujs/core";
146
+
147
+ // Register API handler
148
+ registerApiHandler("getUsers", async (req) => {
149
+ return { users: [] };
150
+ });
151
+
152
+ // Register page loader
153
+ registerPageLoader("homePage", () => import("./pages/Home"));
154
+
155
+ // Start server
156
+ const server = startServer(manifest, { port: 3000 });
157
+
158
+ // Stop
159
+ server.stop();
160
+ ```
161
+
162
+ ## Report Module
163
+
164
+ Guard result report generation.
165
+
166
+ ```typescript
167
+ import { buildGuardReport } from "@mandujs/core";
168
+
169
+ const report = buildGuardReport(guardResult, lockPath);
170
+ console.log(report); // Formatted text report
171
+ ```
172
+
173
+ ## Types
174
+
175
+ ```typescript
176
+ import type {
177
+ RoutesManifest,
178
+ RouteSpec,
179
+ RouteKind,
180
+ SpecLock,
181
+ GuardResult,
182
+ GuardViolation,
183
+ GenerateResult,
184
+ AutoCorrectResult,
185
+ } from "@mandujs/core";
186
+ ```
187
+
188
+ ## Requirements
189
+
190
+ - Bun >= 1.0.0
191
+ - React >= 18.0.0
192
+ - Zod >= 3.0.0
193
+
194
+ ## Related Packages
195
+
196
+ - [@mandujs/cli](https://www.npmjs.com/package/@mandujs/cli) - CLI tool
197
+
198
+ ## License
199
+
200
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandujs/core",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -29,8 +29,10 @@
29
29
  "publishConfig": {
30
30
  "access": "public"
31
31
  },
32
+ "engines": {
33
+ "bun": ">=1.0.0"
34
+ },
32
35
  "peerDependencies": {
33
- "bun": ">=1.0.0",
34
36
  "react": ">=18.0.0",
35
37
  "react-dom": ">=18.0.0",
36
38
  "zod": ">=3.0.0"
@@ -0,0 +1,145 @@
1
+ import path from "path";
2
+ import type { ChangeRecord, HistoryConfig } from "./types";
3
+ import { deleteSnapshot, listSnapshotIds } from "./snapshot";
4
+ import { DEFAULT_HISTORY_CONFIG } from "./types";
5
+
6
+ const SPEC_DIR = "spec";
7
+ const HISTORY_DIR = "history";
8
+ const CHANGES_FILE = "changes.json";
9
+
10
+ /**
11
+ * Changes 파일 경로
12
+ */
13
+ function getChangesPath(rootDir: string): string {
14
+ return path.join(rootDir, SPEC_DIR, HISTORY_DIR, CHANGES_FILE);
15
+ }
16
+
17
+ /**
18
+ * 모든 변경 기록 조회
19
+ */
20
+ export async function listChanges(rootDir: string): Promise<ChangeRecord[]> {
21
+ const changesPath = getChangesPath(rootDir);
22
+ try {
23
+ const file = Bun.file(changesPath);
24
+ if (!(await file.exists())) {
25
+ return [];
26
+ }
27
+ return await file.json();
28
+ } catch {
29
+ return [];
30
+ }
31
+ }
32
+
33
+ /**
34
+ * 특정 변경 기록 조회
35
+ */
36
+ export async function getChange(rootDir: string, id: string): Promise<ChangeRecord | null> {
37
+ const changes = await listChanges(rootDir);
38
+ return changes.find((c) => c.id === id) || null;
39
+ }
40
+
41
+ /**
42
+ * 변경 기록 저장
43
+ */
44
+ async function writeChanges(rootDir: string, changes: ChangeRecord[]): Promise<void> {
45
+ const changesPath = getChangesPath(rootDir);
46
+ const historyDir = path.join(rootDir, SPEC_DIR, HISTORY_DIR);
47
+
48
+ // 디렉토리 확보
49
+ await Bun.write(path.join(historyDir, ".gitkeep"), "");
50
+
51
+ await Bun.write(changesPath, JSON.stringify(changes, null, 2));
52
+ }
53
+
54
+ /**
55
+ * 오래된 스냅샷 정리
56
+ * @param rootDir 프로젝트 루트 디렉토리
57
+ * @param keepCount 유지할 스냅샷 수 (기본: 5)
58
+ * @returns 삭제된 스냅샷 ID 목록
59
+ */
60
+ export async function pruneHistory(
61
+ rootDir: string,
62
+ keepCount: number = DEFAULT_HISTORY_CONFIG.maxSnapshots
63
+ ): Promise<string[]> {
64
+ const deletedIds: string[] = [];
65
+
66
+ // 모든 스냅샷 ID 조회 (최신 순으로 정렬됨)
67
+ const snapshotIds = await listSnapshotIds(rootDir);
68
+
69
+ if (snapshotIds.length <= keepCount) {
70
+ return deletedIds;
71
+ }
72
+
73
+ // 변경 기록 조회
74
+ const changes = await listChanges(rootDir);
75
+
76
+ // 활성 트랜잭션의 스냅샷 ID 수집 (삭제 불가)
77
+ const activeSnapshotIds = new Set(
78
+ changes.filter((c) => c.status === "active").map((c) => c.snapshotId)
79
+ );
80
+
81
+ // 유지할 스냅샷 외의 것들 삭제
82
+ const toDelete = snapshotIds.slice(keepCount);
83
+
84
+ for (const snapshotId of toDelete) {
85
+ // 활성 트랜잭션의 스냅샷은 삭제하지 않음
86
+ if (activeSnapshotIds.has(snapshotId)) {
87
+ continue;
88
+ }
89
+
90
+ const deleted = await deleteSnapshot(rootDir, snapshotId);
91
+ if (deleted) {
92
+ deletedIds.push(snapshotId);
93
+ }
94
+ }
95
+
96
+ // 삭제된 스냅샷에 연결된 변경 기록도 정리
97
+ if (deletedIds.length > 0) {
98
+ const deletedSet = new Set(deletedIds);
99
+ const remainingChanges = changes.filter((c) => {
100
+ // 활성 상태는 유지
101
+ if (c.status === "active") {
102
+ return true;
103
+ }
104
+ // 스냅샷이 삭제되지 않은 것만 유지
105
+ return !deletedSet.has(c.snapshotId);
106
+ });
107
+
108
+ await writeChanges(rootDir, remainingChanges);
109
+ }
110
+
111
+ return deletedIds;
112
+ }
113
+
114
+ /**
115
+ * History 설정 로드 (향후 mandu.config.json에서 로드)
116
+ */
117
+ export async function loadHistoryConfig(rootDir: string): Promise<HistoryConfig> {
118
+ // 향후 mandu.config.json에서 로드하도록 확장
119
+ // 현재는 기본값 반환
120
+ return DEFAULT_HISTORY_CONFIG;
121
+ }
122
+
123
+ /**
124
+ * 변경 통계 조회
125
+ */
126
+ export async function getChangeStats(
127
+ rootDir: string
128
+ ): Promise<{
129
+ total: number;
130
+ active: number;
131
+ committed: number;
132
+ rolledBack: number;
133
+ snapshotCount: number;
134
+ }> {
135
+ const changes = await listChanges(rootDir);
136
+ const snapshotIds = await listSnapshotIds(rootDir);
137
+
138
+ return {
139
+ total: changes.length,
140
+ active: changes.filter((c) => c.status === "active").length,
141
+ committed: changes.filter((c) => c.status === "committed").length,
142
+ rolledBack: changes.filter((c) => c.status === "rolled_back").length,
143
+ snapshotCount: snapshotIds.length,
144
+ };
145
+ }
@@ -0,0 +1,40 @@
1
+ // Types
2
+ export type {
3
+ ChangeRecord,
4
+ Snapshot,
5
+ TransactionState,
6
+ HistoryConfig,
7
+ RestoreResult,
8
+ CommitResult,
9
+ RollbackResult,
10
+ BeginChangeOptions,
11
+ } from "./types";
12
+
13
+ export { DEFAULT_HISTORY_CONFIG } from "./types";
14
+
15
+ // Integrity
16
+ export { computeFileHash, collectFileHashes, collectFilePaths } from "./integrity";
17
+
18
+ // Snapshot
19
+ export {
20
+ createSnapshot,
21
+ readSnapshot,
22
+ writeSnapshot,
23
+ readSnapshotById,
24
+ restoreSnapshot,
25
+ deleteSnapshot,
26
+ listSnapshotIds,
27
+ } from "./snapshot";
28
+
29
+ // Transaction
30
+ export {
31
+ beginChange,
32
+ commitChange,
33
+ rollbackChange,
34
+ hasActiveTransaction,
35
+ getActiveTransaction,
36
+ getTransactionStatus,
37
+ } from "./transaction";
38
+
39
+ // History
40
+ export { listChanges, getChange, pruneHistory, loadHistoryConfig, getChangeStats } from "./history";
@@ -0,0 +1,81 @@
1
+ import { createHash } from "crypto";
2
+ import path from "path";
3
+
4
+ /**
5
+ * 파일의 SHA-256 해시를 계산
6
+ */
7
+ export async function computeFileHash(filePath: string): Promise<string> {
8
+ try {
9
+ const file = Bun.file(filePath);
10
+ const exists = await file.exists();
11
+
12
+ if (!exists) {
13
+ throw new Error(`File not found: ${filePath}`);
14
+ }
15
+
16
+ const content = await file.text();
17
+ return createHash("sha256").update(content).digest("hex");
18
+ } catch (error) {
19
+ if (error instanceof Error && error.message.startsWith("File not found")) {
20
+ throw error;
21
+ }
22
+ throw new Error(
23
+ `Failed to compute hash for ${filePath}: ${error instanceof Error ? error.message : String(error)}`
24
+ );
25
+ }
26
+ }
27
+
28
+ /**
29
+ * 여러 파일의 해시를 수집
30
+ * @param rootDir 프로젝트 루트 디렉토리
31
+ * @param relativePaths 상대 경로 배열
32
+ * @returns 상대 경로 → 해시 맵
33
+ */
34
+ export async function collectFileHashes(
35
+ rootDir: string,
36
+ relativePaths: string[]
37
+ ): Promise<Record<string, string>> {
38
+ const hashes: Record<string, string> = {};
39
+
40
+ await Promise.all(
41
+ relativePaths.map(async (relativePath) => {
42
+ const absolutePath = path.join(rootDir, relativePath);
43
+ try {
44
+ const hash = await computeFileHash(absolutePath);
45
+ hashes[relativePath] = hash;
46
+ } catch {
47
+ // 파일이 없는 경우 무시 (스냅샷 시점에 없었을 수 있음)
48
+ }
49
+ })
50
+ );
51
+
52
+ return hashes;
53
+ }
54
+
55
+ /**
56
+ * 디렉토리 내 모든 파일 경로를 재귀적으로 수집
57
+ */
58
+ export async function collectFilePaths(
59
+ dirPath: string,
60
+ basePath: string = dirPath
61
+ ): Promise<string[]> {
62
+ const paths: string[] = [];
63
+
64
+ try {
65
+ const entries = await Array.fromAsync(
66
+ new Bun.Glob("**/*").scan({
67
+ cwd: dirPath,
68
+ onlyFiles: true,
69
+ })
70
+ );
71
+
72
+ for (const entry of entries) {
73
+ const relativePath = path.relative(basePath, path.join(dirPath, entry));
74
+ paths.push(relativePath);
75
+ }
76
+ } catch {
77
+ // 디렉토리가 없는 경우 빈 배열 반환
78
+ }
79
+
80
+ return paths;
81
+ }