@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.
@@ -46,22 +46,24 @@ export const DEFAULT_ARCHITECTURE_CONFIG: ArchitectureConfig = {
46
46
  description: "자동 생성 파일. 직접 수정 금지",
47
47
  readonly: true,
48
48
  },
49
- "apps/server/": {
50
- pattern: "apps/server/**",
51
- description: "백엔드 로직",
49
+ ".mandu/generated/server/": {
50
+ pattern: ".mandu/generated/server/**",
51
+ description: "서버 생성 파일",
52
52
  allowedFiles: ["*.ts"],
53
+ readonly: true,
53
54
  },
54
- "apps/web/": {
55
- pattern: "apps/web/**",
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: "apps/web/**",
64
+ source: ".mandu/generated/web/**",
63
65
  forbid: ["fs", "child_process", "path", "crypto"],
64
- reason: "프론트엔드에서 Node.js 내장 모듈 사용 금지",
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: ["apps/**"],
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("apps/web")) {
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\/|apps\/|packages\/)[^\s,)]+/);
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: "spec/spec.lock.json",
143
+ file: ".mandu/spec.lock.json",
144
144
  description: "Spec lock 파일 갱신",
145
145
  type: "command",
146
146
  command: "bunx mandu spec-upsert",
@@ -63,7 +63,6 @@ const DEFAULT_COMMON_DIRS = [
63
63
  "client",
64
64
  "src/islands",
65
65
  "islands",
66
- "apps/web",
67
66
  ];
68
67
 
69
68
  /**
@@ -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 SPEC_DIR = "spec";
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, SPEC_DIR, HISTORY_DIR, CHANGES_FILE);
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, SPEC_DIR, HISTORY_DIR);
46
+ const historyDir = path.join(rootDir, MANDU_DIR, HISTORY_DIR);
47
47
 
48
48
  // 디렉토리 확보
49
49
  await Bun.write(path.join(historyDir, ".gitkeep"), "");
@@ -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, SPEC_DIR, HISTORY_DIR, SNAPSHOTS_DIR, `${snapshotId}.snapshot.json`);
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 specDir = path.join(rootDir, SPEC_DIR);
75
- const manifestPath = path.join(specDir, MANIFEST_FILE);
76
- const lockPath = path.join(specDir, LOCK_FILE);
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 specDir = path.join(rootDir, SPEC_DIR);
172
- const manifestPath = path.join(specDir, MANIFEST_FILE);
173
- const lockPath = path.join(specDir, LOCK_FILE);
174
- const slotsDir = path.join(specDir, SLOTS_DIR);
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, SPEC_DIR, HISTORY_DIR, SNAPSHOTS_DIR);
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 SPEC_DIR = "spec";
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, SPEC_DIR, HISTORY_DIR);
31
+ return path.join(rootDir, MANDU_DIR, HISTORY_DIR);
32
32
  }
33
33
 
34
34
  /**
@@ -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
- guard?: {
20
- preset?: "mandu" | "fsd" | "clean" | "hexagonal" | "atomic";
21
- srcDir?: string;
22
- exclude?: string[];
23
- realtime?: boolean;
24
- rules?: Record<string, GuardRuleSeverity>;
25
- contractRequired?: GuardRuleSeverity;
26
- };
27
- build?: {
28
- outDir?: string;
29
- minify?: boolean;
30
- sourcemap?: boolean;
31
- splitting?: boolean;
32
- };
33
- dev?: {
34
- hmr?: boolean;
35
- watchDirs?: string[];
36
- };
37
- fsRoutes?: {
38
- routesDir?: string;
39
- extensions?: string[];
40
- exclude?: string[];
41
- islandSuffix?: string;
42
- legacyManifestPath?: string;
43
- mergeWithLegacy?: boolean;
44
- };
45
- seo?: {
46
- enabled?: boolean;
47
- defaultTitle?: string;
48
- titleTemplate?: string;
49
- };
50
- }
51
-
52
- export const CONFIG_FILES = [
53
- "mandu.config.ts",
54
- "mandu.config.js",
55
- "mandu.config.json",
56
- path.join(".mandu", "guard.json"),
57
- ];
58
-
59
- export function coerceConfig(raw: unknown, source: string): ManduConfig {
60
- if (!raw || typeof raw !== "object") return {};
61
-
62
- // .mandu/guard.json can be guard-only
63
- if (source.endsWith("guard.json") && !("guard" in (raw as Record<string, unknown>))) {
64
- return { guard: raw as ManduConfig["guard"] };
65
- }
66
-
67
- return raw as ManduConfig;
68
- }
69
-
70
- export async function loadManduConfig(rootDir: string): Promise<ManduConfig> {
71
- for (const fileName of CONFIG_FILES) {
72
- const filePath = path.join(rootDir, fileName);
73
- if (!(await Bun.file(filePath).exists())) {
74
- continue;
75
- }
76
-
77
- if (fileName.endsWith(".json")) {
78
- try {
79
- const parsed = await readJsonFile(filePath);
80
- return coerceConfig(parsed, fileName);
81
- } catch {
82
- return {};
83
- }
84
- }
85
-
86
- try {
87
- const module = await import(filePath);
88
- const raw = module?.default ?? module;
89
- return coerceConfig(raw, fileName);
90
- } catch {
91
- return {};
92
- }
93
- }
94
-
95
- return {};
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
+ }