@mandujs/core 0.12.2 → 0.13.1
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.ko.md +304 -304
- package/package.json +1 -1
- package/src/brain/architecture/analyzer.ts +28 -26
- package/src/brain/doctor/analyzer.ts +1 -1
- package/src/bundler/dev.ts +0 -1
- package/src/change/history.ts +3 -3
- package/src/change/snapshot.ts +10 -9
- package/src/change/transaction.ts +2 -2
- package/src/config/mandu.ts +103 -96
- package/src/config/validate.ts +225 -215
- package/src/error/classifier.ts +2 -2
- package/src/error/formatter.ts +32 -32
- package/src/error/stack-analyzer.ts +5 -0
- package/src/filling/context.ts +592 -569
- package/src/filling/index.ts +2 -0
- package/src/filling/sse.test.ts +168 -0
- package/src/filling/sse.ts +162 -0
- package/src/generator/contract-glue.ts +2 -1
- package/src/generator/generate.ts +12 -10
- package/src/generator/templates.ts +80 -79
- package/src/guard/auto-correct.ts +1 -1
- package/src/guard/check.ts +128 -128
- package/src/guard/presets/cqrs.test.ts +35 -14
- package/src/index.ts +7 -1
- package/src/paths.test.ts +47 -0
- package/src/paths.ts +47 -0
- package/src/report/build.ts +1 -1
- package/src/router/fs-routes.ts +344 -401
- package/src/router/fs-types.ts +270 -278
- package/src/router/index.ts +81 -81
- package/src/runtime/escape.ts +44 -0
- package/src/runtime/server.ts +281 -24
- package/src/runtime/ssr.ts +362 -367
- package/src/runtime/streaming-ssr.ts +1236 -1245
- package/src/watcher/rules.ts +5 -5
|
@@ -46,22 +46,24 @@ export const DEFAULT_ARCHITECTURE_CONFIG: ArchitectureConfig = {
|
|
|
46
46
|
description: "자동 생성 파일. 직접 수정 금지",
|
|
47
47
|
readonly: true,
|
|
48
48
|
},
|
|
49
|
-
"
|
|
50
|
-
pattern: "
|
|
51
|
-
description: "
|
|
49
|
+
".mandu/generated/server/": {
|
|
50
|
+
pattern: ".mandu/generated/server/**",
|
|
51
|
+
description: "서버 생성 파일",
|
|
52
52
|
allowedFiles: ["*.ts"],
|
|
53
|
+
readonly: true,
|
|
53
54
|
},
|
|
54
|
-
"
|
|
55
|
-
pattern: "
|
|
56
|
-
description: "
|
|
55
|
+
".mandu/generated/web/": {
|
|
56
|
+
pattern: ".mandu/generated/web/**",
|
|
57
|
+
description: "웹 생성 파일",
|
|
57
58
|
allowedFiles: ["*.ts", "*.tsx"],
|
|
59
|
+
readonly: true,
|
|
58
60
|
},
|
|
59
61
|
},
|
|
60
62
|
imports: [
|
|
61
63
|
{
|
|
62
|
-
source: "
|
|
64
|
+
source: ".mandu/generated/web/**",
|
|
63
65
|
forbid: ["fs", "child_process", "path", "crypto"],
|
|
64
|
-
reason: "
|
|
66
|
+
reason: "프론트엔드 생성 파일에서 Node.js 내장 모듈 사용 금지",
|
|
65
67
|
},
|
|
66
68
|
{
|
|
67
69
|
source: "**/generated/**",
|
|
@@ -87,7 +89,7 @@ export const DEFAULT_ARCHITECTURE_CONFIG: ArchitectureConfig = {
|
|
|
87
89
|
},
|
|
88
90
|
{
|
|
89
91
|
name: "app",
|
|
90
|
-
folders: ["
|
|
92
|
+
folders: ["app/**", "src/**"],
|
|
91
93
|
canDependOn: ["spec", "generated"],
|
|
92
94
|
},
|
|
93
95
|
],
|
|
@@ -173,9 +175,9 @@ export class ArchitectureAnalyzer {
|
|
|
173
175
|
/**
|
|
174
176
|
* 파일 위치 검증
|
|
175
177
|
*/
|
|
176
|
-
async checkLocation(request: CheckLocationRequest): Promise<CheckLocationResult> {
|
|
177
|
-
const violations: ArchitectureViolation[] = [];
|
|
178
|
-
const normalizedPath = this.toRelativePath(request.path);
|
|
178
|
+
async checkLocation(request: CheckLocationRequest): Promise<CheckLocationResult> {
|
|
179
|
+
const violations: ArchitectureViolation[] = [];
|
|
180
|
+
const normalizedPath = this.toRelativePath(request.path);
|
|
179
181
|
|
|
180
182
|
// 1. readonly 폴더 검사
|
|
181
183
|
for (const [key, rule] of Object.entries(this.config.folders || {})) {
|
|
@@ -288,14 +290,14 @@ export class ArchitectureAnalyzer {
|
|
|
288
290
|
/**
|
|
289
291
|
* Import 검증
|
|
290
292
|
*/
|
|
291
|
-
async checkImports(request: CheckImportRequest): Promise<CheckImportResult> {
|
|
293
|
+
async checkImports(request: CheckImportRequest): Promise<CheckImportResult> {
|
|
292
294
|
const violations: Array<{
|
|
293
295
|
import: string;
|
|
294
296
|
reason: string;
|
|
295
297
|
suggestion?: string;
|
|
296
298
|
}> = [];
|
|
297
299
|
|
|
298
|
-
const normalizedSource = this.toRelativePath(request.sourceFile);
|
|
300
|
+
const normalizedSource = this.toRelativePath(request.sourceFile);
|
|
299
301
|
|
|
300
302
|
for (const importPath of request.imports) {
|
|
301
303
|
for (const rule of this.config.imports || []) {
|
|
@@ -368,7 +370,7 @@ export class ArchitectureAnalyzer {
|
|
|
368
370
|
/**
|
|
369
371
|
* 코드에서 import 문 추출
|
|
370
372
|
*/
|
|
371
|
-
private extractImports(content: string): string[] {
|
|
373
|
+
private extractImports(content: string): string[] {
|
|
372
374
|
const imports: string[] = [];
|
|
373
375
|
|
|
374
376
|
// ES6 import
|
|
@@ -385,15 +387,15 @@ export class ArchitectureAnalyzer {
|
|
|
385
387
|
}
|
|
386
388
|
|
|
387
389
|
return imports;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
private toRelativePath(filePath: string): string {
|
|
391
|
-
const normalized = filePath.replace(/\\/g, "/");
|
|
392
|
-
if (path.isAbsolute(normalized)) {
|
|
393
|
-
return path.relative(this.rootDir, normalized).replace(/\\/g, "/");
|
|
394
|
-
}
|
|
395
|
-
return normalized;
|
|
396
|
-
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private toRelativePath(filePath: string): string {
|
|
393
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
394
|
+
if (path.isAbsolute(normalized)) {
|
|
395
|
+
return path.relative(this.rootDir, normalized).replace(/\\/g, "/");
|
|
396
|
+
}
|
|
397
|
+
return normalized;
|
|
398
|
+
}
|
|
397
399
|
|
|
398
400
|
/**
|
|
399
401
|
* 폴더 스캔
|
|
@@ -449,7 +451,7 @@ export class ArchitectureAnalyzer {
|
|
|
449
451
|
*/
|
|
450
452
|
private getImportSuggestion(importPath: string, sourceFile: string): string {
|
|
451
453
|
if (importPath === "fs") {
|
|
452
|
-
if (sourceFile.includes("
|
|
454
|
+
if (sourceFile.includes(".mandu/generated/web") || sourceFile.includes("client")) {
|
|
453
455
|
return "프론트엔드에서는 fetch API를 사용하세요";
|
|
454
456
|
}
|
|
455
457
|
return "Bun.file() 또는 Bun.write()를 사용하세요";
|
|
@@ -513,7 +515,7 @@ ${JSON.stringify(this.config.folders, null, 2)}
|
|
|
513
515
|
]);
|
|
514
516
|
|
|
515
517
|
// 응답에서 경로 추출 시도
|
|
516
|
-
const pathMatch = result.content.match(/(?:spec\/|
|
|
518
|
+
const pathMatch = result.content.match(/(?:spec\/|\.mandu\/|app\/|src\/|packages\/)[^\s,)]+/);
|
|
517
519
|
|
|
518
520
|
return {
|
|
519
521
|
suggestion: result.content,
|
|
@@ -140,7 +140,7 @@ export function generateTemplatePatches(
|
|
|
140
140
|
switch (violation.ruleId) {
|
|
141
141
|
case GUARD_RULES.SPEC_HASH_MISMATCH?.id:
|
|
142
142
|
patches.push({
|
|
143
|
-
file: "
|
|
143
|
+
file: ".mandu/spec.lock.json",
|
|
144
144
|
description: "Spec lock 파일 갱신",
|
|
145
145
|
type: "command",
|
|
146
146
|
command: "bunx mandu spec-upsert",
|
package/src/bundler/dev.ts
CHANGED
package/src/change/history.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { ChangeRecord, HistoryConfig } from "./types";
|
|
|
3
3
|
import { deleteSnapshot, listSnapshotIds } from "./snapshot";
|
|
4
4
|
import { DEFAULT_HISTORY_CONFIG } from "./types";
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const MANDU_DIR = ".mandu";
|
|
7
7
|
const HISTORY_DIR = "history";
|
|
8
8
|
const CHANGES_FILE = "changes.json";
|
|
9
9
|
|
|
@@ -11,7 +11,7 @@ const CHANGES_FILE = "changes.json";
|
|
|
11
11
|
* Changes 파일 경로
|
|
12
12
|
*/
|
|
13
13
|
function getChangesPath(rootDir: string): string {
|
|
14
|
-
return path.join(rootDir,
|
|
14
|
+
return path.join(rootDir, MANDU_DIR, HISTORY_DIR, CHANGES_FILE);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/**
|
|
@@ -43,7 +43,7 @@ export async function getChange(rootDir: string, id: string): Promise<ChangeReco
|
|
|
43
43
|
*/
|
|
44
44
|
async function writeChanges(rootDir: string, changes: ChangeRecord[]): Promise<void> {
|
|
45
45
|
const changesPath = getChangesPath(rootDir);
|
|
46
|
-
const historyDir = path.join(rootDir,
|
|
46
|
+
const historyDir = path.join(rootDir, MANDU_DIR, HISTORY_DIR);
|
|
47
47
|
|
|
48
48
|
// 디렉토리 확보
|
|
49
49
|
await Bun.write(path.join(historyDir, ".gitkeep"), "");
|
package/src/change/snapshot.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "../lockfile";
|
|
12
12
|
import { validateAndReport } from "../config";
|
|
13
13
|
|
|
14
|
+
const MANDU_DIR = ".mandu";
|
|
14
15
|
const SPEC_DIR = "spec";
|
|
15
16
|
const MANIFEST_FILE = "routes.manifest.json";
|
|
16
17
|
const LOCK_FILE = "spec.lock.json";
|
|
@@ -33,7 +34,7 @@ function generateSnapshotId(): string {
|
|
|
33
34
|
* 스냅샷 저장 경로 반환
|
|
34
35
|
*/
|
|
35
36
|
function getSnapshotPath(rootDir: string, snapshotId: string): string {
|
|
36
|
-
return path.join(rootDir,
|
|
37
|
+
return path.join(rootDir, MANDU_DIR, HISTORY_DIR, SNAPSHOTS_DIR, `${snapshotId}.snapshot.json`);
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
/**
|
|
@@ -71,9 +72,9 @@ async function collectSlotContents(rootDir: string): Promise<Record<string, stri
|
|
|
71
72
|
* 현재 spec 상태의 스냅샷 생성
|
|
72
73
|
*/
|
|
73
74
|
export async function createSnapshot(rootDir: string): Promise<Snapshot> {
|
|
74
|
-
const
|
|
75
|
-
const manifestPath = path.join(
|
|
76
|
-
const lockPath = path.join(
|
|
75
|
+
const manduDir = path.join(rootDir, MANDU_DIR);
|
|
76
|
+
const manifestPath = path.join(manduDir, MANIFEST_FILE);
|
|
77
|
+
const lockPath = path.join(manduDir, LOCK_FILE);
|
|
77
78
|
|
|
78
79
|
// Manifest 읽기 (필수)
|
|
79
80
|
const manifestFile = Bun.file(manifestPath);
|
|
@@ -168,10 +169,10 @@ export async function readSnapshotById(rootDir: string, snapshotId: string): Pro
|
|
|
168
169
|
* 스냅샷으로부터 상태 복원
|
|
169
170
|
*/
|
|
170
171
|
export async function restoreSnapshot(rootDir: string, snapshot: Snapshot): Promise<RestoreResult> {
|
|
171
|
-
const
|
|
172
|
-
const manifestPath = path.join(
|
|
173
|
-
const lockPath = path.join(
|
|
174
|
-
const slotsDir = path.join(
|
|
172
|
+
const manduDir = path.join(rootDir, MANDU_DIR);
|
|
173
|
+
const manifestPath = path.join(manduDir, MANIFEST_FILE);
|
|
174
|
+
const lockPath = path.join(manduDir, LOCK_FILE);
|
|
175
|
+
const slotsDir = path.join(rootDir, SPEC_DIR, SLOTS_DIR);
|
|
175
176
|
|
|
176
177
|
const restoredFiles: string[] = [];
|
|
177
178
|
const failedFiles: string[] = [];
|
|
@@ -258,7 +259,7 @@ export async function deleteSnapshot(rootDir: string, snapshotId: string): Promi
|
|
|
258
259
|
* 모든 스냅샷 ID 목록 조회
|
|
259
260
|
*/
|
|
260
261
|
export async function listSnapshotIds(rootDir: string): Promise<string[]> {
|
|
261
|
-
const snapshotsDir = path.join(rootDir,
|
|
262
|
+
const snapshotsDir = path.join(rootDir, MANDU_DIR, HISTORY_DIR, SNAPSHOTS_DIR);
|
|
262
263
|
|
|
263
264
|
try {
|
|
264
265
|
const entries = await Array.fromAsync(
|
|
@@ -8,7 +8,7 @@ import type {
|
|
|
8
8
|
} from "./types";
|
|
9
9
|
import { createSnapshot, writeSnapshot, readSnapshotById, restoreSnapshot } from "./snapshot";
|
|
10
10
|
|
|
11
|
-
const
|
|
11
|
+
const MANDU_DIR = ".mandu";
|
|
12
12
|
const HISTORY_DIR = "history";
|
|
13
13
|
const CHANGES_FILE = "changes.json";
|
|
14
14
|
const ACTIVE_FILE = "active.json";
|
|
@@ -28,7 +28,7 @@ function generateChangeId(): string {
|
|
|
28
28
|
* History 디렉토리 경로
|
|
29
29
|
*/
|
|
30
30
|
function getHistoryDir(rootDir: string): string {
|
|
31
|
-
return path.join(rootDir,
|
|
31
|
+
return path.join(rootDir, MANDU_DIR, HISTORY_DIR);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/**
|
package/src/config/mandu.ts
CHANGED
|
@@ -1,96 +1,103 @@
|
|
|
1
|
-
import path from "path";
|
|
2
|
-
import { readJsonFile } from "../utils/bun";
|
|
3
|
-
|
|
4
|
-
export type GuardRuleSeverity = "error" | "warn" | "warning" | "off";
|
|
5
|
-
|
|
6
|
-
export interface ManduConfig {
|
|
7
|
-
server?: {
|
|
8
|
-
port?: number;
|
|
9
|
-
hostname?: string;
|
|
10
|
-
cors?:
|
|
11
|
-
| boolean
|
|
12
|
-
| {
|
|
13
|
-
origin?: string | string[];
|
|
14
|
-
methods?: string[];
|
|
15
|
-
credentials?: boolean;
|
|
16
|
-
};
|
|
17
|
-
streaming?: boolean;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
export
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { readJsonFile } from "../utils/bun";
|
|
3
|
+
|
|
4
|
+
export type GuardRuleSeverity = "error" | "warn" | "warning" | "off";
|
|
5
|
+
|
|
6
|
+
export interface ManduConfig {
|
|
7
|
+
server?: {
|
|
8
|
+
port?: number;
|
|
9
|
+
hostname?: string;
|
|
10
|
+
cors?:
|
|
11
|
+
| boolean
|
|
12
|
+
| {
|
|
13
|
+
origin?: string | string[];
|
|
14
|
+
methods?: string[];
|
|
15
|
+
credentials?: boolean;
|
|
16
|
+
};
|
|
17
|
+
streaming?: boolean;
|
|
18
|
+
rateLimit?:
|
|
19
|
+
| boolean
|
|
20
|
+
| {
|
|
21
|
+
windowMs?: number;
|
|
22
|
+
max?: number;
|
|
23
|
+
message?: string;
|
|
24
|
+
statusCode?: number;
|
|
25
|
+
headers?: boolean;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
guard?: {
|
|
29
|
+
preset?: "mandu" | "fsd" | "clean" | "hexagonal" | "atomic";
|
|
30
|
+
srcDir?: string;
|
|
31
|
+
exclude?: string[];
|
|
32
|
+
realtime?: boolean;
|
|
33
|
+
rules?: Record<string, GuardRuleSeverity>;
|
|
34
|
+
contractRequired?: GuardRuleSeverity;
|
|
35
|
+
};
|
|
36
|
+
build?: {
|
|
37
|
+
outDir?: string;
|
|
38
|
+
minify?: boolean;
|
|
39
|
+
sourcemap?: boolean;
|
|
40
|
+
splitting?: boolean;
|
|
41
|
+
};
|
|
42
|
+
dev?: {
|
|
43
|
+
hmr?: boolean;
|
|
44
|
+
watchDirs?: string[];
|
|
45
|
+
};
|
|
46
|
+
fsRoutes?: {
|
|
47
|
+
routesDir?: string;
|
|
48
|
+
extensions?: string[];
|
|
49
|
+
exclude?: string[];
|
|
50
|
+
islandSuffix?: string;
|
|
51
|
+
};
|
|
52
|
+
seo?: {
|
|
53
|
+
enabled?: boolean;
|
|
54
|
+
defaultTitle?: string;
|
|
55
|
+
titleTemplate?: string;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const CONFIG_FILES = [
|
|
60
|
+
"mandu.config.ts",
|
|
61
|
+
"mandu.config.js",
|
|
62
|
+
"mandu.config.json",
|
|
63
|
+
path.join(".mandu", "guard.json"),
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
export function coerceConfig(raw: unknown, source: string): ManduConfig {
|
|
67
|
+
if (!raw || typeof raw !== "object") return {};
|
|
68
|
+
|
|
69
|
+
// .mandu/guard.json can be guard-only
|
|
70
|
+
if (source.endsWith("guard.json") && !("guard" in (raw as Record<string, unknown>))) {
|
|
71
|
+
return { guard: raw as ManduConfig["guard"] };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return raw as ManduConfig;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function loadManduConfig(rootDir: string): Promise<ManduConfig> {
|
|
78
|
+
for (const fileName of CONFIG_FILES) {
|
|
79
|
+
const filePath = path.join(rootDir, fileName);
|
|
80
|
+
if (!(await Bun.file(filePath).exists())) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (fileName.endsWith(".json")) {
|
|
85
|
+
try {
|
|
86
|
+
const parsed = await readJsonFile(filePath);
|
|
87
|
+
return coerceConfig(parsed, fileName);
|
|
88
|
+
} catch {
|
|
89
|
+
return {};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const module = await import(filePath);
|
|
95
|
+
const raw = module?.default ?? module;
|
|
96
|
+
return coerceConfig(raw, fileName);
|
|
97
|
+
} catch {
|
|
98
|
+
return {};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {};
|
|
103
|
+
}
|