@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
package/src/guard/check.ts
CHANGED
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
import { GUARD_RULES, FORBIDDEN_IMPORTS, type GuardViolation } from "./rules";
|
|
2
|
-
import { verifyLock, computeHash } from "../spec/lock";
|
|
3
|
-
import { runContractGuardCheck } from "./contract-guard";
|
|
4
|
-
import { validateSlotContent } from "../slot/validator";
|
|
5
|
-
import type { RoutesManifest } from "../spec/schema";
|
|
6
|
-
import type { GeneratedMap } from "../generator/generate";
|
|
7
|
-
import { loadManduConfig, type GuardRuleSeverity } from "../config";
|
|
8
|
-
import path from "path";
|
|
9
|
-
import fs from "fs/promises";
|
|
10
|
-
|
|
11
|
-
export interface GuardCheckResult {
|
|
12
|
-
passed: boolean;
|
|
13
|
-
violations: GuardViolation[];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function normalizeSeverity(level: GuardRuleSeverity): "error" | "warning" | "off" {
|
|
17
|
-
if (level === "warn") return "warning";
|
|
18
|
-
return level;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function applyRuleSeverity(
|
|
22
|
-
violations: GuardViolation[],
|
|
23
|
-
config: { rules?: Record<string, GuardRuleSeverity>; contractRequired?: GuardRuleSeverity }
|
|
24
|
-
): GuardViolation[] {
|
|
25
|
-
const resolved: GuardViolation[] = [];
|
|
26
|
-
const ruleOverrides = config.rules ?? {};
|
|
27
|
-
|
|
28
|
-
for (const violation of violations) {
|
|
29
|
-
let override = ruleOverrides[violation.ruleId];
|
|
30
|
-
if (violation.ruleId === "CONTRACT_MISSING" && config.contractRequired) {
|
|
31
|
-
override = config.contractRequired;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const baseSeverity = violation.severity ?? GUARD_RULES[violation.ruleId]?.severity ?? "error";
|
|
35
|
-
const finalSeverity = override ? normalizeSeverity(override) : baseSeverity;
|
|
36
|
-
|
|
37
|
-
if (finalSeverity === "off") continue;
|
|
38
|
-
|
|
39
|
-
resolved.push({ ...violation, severity: finalSeverity });
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return resolved;
|
|
43
|
-
}
|
|
1
|
+
import { GUARD_RULES, FORBIDDEN_IMPORTS, type GuardViolation } from "./rules";
|
|
2
|
+
import { verifyLock, computeHash } from "../spec/lock";
|
|
3
|
+
import { runContractGuardCheck } from "./contract-guard";
|
|
4
|
+
import { validateSlotContent } from "../slot/validator";
|
|
5
|
+
import type { RoutesManifest } from "../spec/schema";
|
|
6
|
+
import type { GeneratedMap } from "../generator/generate";
|
|
7
|
+
import { loadManduConfig, type GuardRuleSeverity } from "../config";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import fs from "fs/promises";
|
|
10
|
+
|
|
11
|
+
export interface GuardCheckResult {
|
|
12
|
+
passed: boolean;
|
|
13
|
+
violations: GuardViolation[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizeSeverity(level: GuardRuleSeverity): "error" | "warning" | "off" {
|
|
17
|
+
if (level === "warn") return "warning";
|
|
18
|
+
return level;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function applyRuleSeverity(
|
|
22
|
+
violations: GuardViolation[],
|
|
23
|
+
config: { rules?: Record<string, GuardRuleSeverity>; contractRequired?: GuardRuleSeverity }
|
|
24
|
+
): GuardViolation[] {
|
|
25
|
+
const resolved: GuardViolation[] = [];
|
|
26
|
+
const ruleOverrides = config.rules ?? {};
|
|
27
|
+
|
|
28
|
+
for (const violation of violations) {
|
|
29
|
+
let override = ruleOverrides[violation.ruleId];
|
|
30
|
+
if (violation.ruleId === "CONTRACT_MISSING" && config.contractRequired) {
|
|
31
|
+
override = config.contractRequired;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const baseSeverity = violation.severity ?? GUARD_RULES[violation.ruleId]?.severity ?? "error";
|
|
35
|
+
const finalSeverity = override ? normalizeSeverity(override) : baseSeverity;
|
|
36
|
+
|
|
37
|
+
if (finalSeverity === "off") continue;
|
|
38
|
+
|
|
39
|
+
resolved.push({ ...violation, severity: finalSeverity });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return resolved;
|
|
43
|
+
}
|
|
44
44
|
|
|
45
45
|
async function fileExists(filePath: string): Promise<boolean> {
|
|
46
46
|
try {
|
|
@@ -116,8 +116,8 @@ export async function checkInvalidGeneratedImport(
|
|
|
116
116
|
// Scan non-generated source files
|
|
117
117
|
const sourceDirs = [
|
|
118
118
|
path.join(rootDir, "packages"),
|
|
119
|
-
path.join(rootDir, "
|
|
120
|
-
path.join(rootDir, "
|
|
119
|
+
path.join(rootDir, "src"),
|
|
120
|
+
path.join(rootDir, "app"),
|
|
121
121
|
];
|
|
122
122
|
|
|
123
123
|
for (const sourceDir of sourceDirs) {
|
|
@@ -200,15 +200,15 @@ export async function checkSlotContentValidation(
|
|
|
200
200
|
if (issue.severity === "error") {
|
|
201
201
|
// Map slot issue codes to guard rule IDs
|
|
202
202
|
let ruleId = "SLOT_VALIDATION_ERROR";
|
|
203
|
-
if (issue.code === "MISSING_DEFAULT_EXPORT") {
|
|
204
|
-
ruleId = GUARD_RULES.SLOT_MISSING_DEFAULT_EXPORT?.id ?? "SLOT_MISSING_DEFAULT_EXPORT";
|
|
205
|
-
} else if (issue.code === "NO_RESPONSE_PATTERN" || issue.code === "INVALID_HANDLER_RETURN") {
|
|
206
|
-
ruleId = GUARD_RULES.SLOT_INVALID_RETURN?.id ?? "SLOT_INVALID_RETURN";
|
|
207
|
-
} else if (issue.code === "MISSING_FILLING_PATTERN") {
|
|
208
|
-
ruleId = GUARD_RULES.SLOT_MISSING_FILLING_PATTERN?.id ?? "SLOT_MISSING_FILLING_PATTERN";
|
|
209
|
-
} else if (issue.code === "ZOD_DIRECT_IMPORT") {
|
|
210
|
-
ruleId = GUARD_RULES.SLOT_ZOD_DIRECT_IMPORT?.id ?? "SLOT_ZOD_DIRECT_IMPORT";
|
|
211
|
-
}
|
|
203
|
+
if (issue.code === "MISSING_DEFAULT_EXPORT") {
|
|
204
|
+
ruleId = GUARD_RULES.SLOT_MISSING_DEFAULT_EXPORT?.id ?? "SLOT_MISSING_DEFAULT_EXPORT";
|
|
205
|
+
} else if (issue.code === "NO_RESPONSE_PATTERN" || issue.code === "INVALID_HANDLER_RETURN") {
|
|
206
|
+
ruleId = GUARD_RULES.SLOT_INVALID_RETURN?.id ?? "SLOT_INVALID_RETURN";
|
|
207
|
+
} else if (issue.code === "MISSING_FILLING_PATTERN") {
|
|
208
|
+
ruleId = GUARD_RULES.SLOT_MISSING_FILLING_PATTERN?.id ?? "SLOT_MISSING_FILLING_PATTERN";
|
|
209
|
+
} else if (issue.code === "ZOD_DIRECT_IMPORT") {
|
|
210
|
+
ruleId = GUARD_RULES.SLOT_ZOD_DIRECT_IMPORT?.id ?? "SLOT_ZOD_DIRECT_IMPORT";
|
|
211
|
+
}
|
|
212
212
|
|
|
213
213
|
violations.push({
|
|
214
214
|
ruleId,
|
|
@@ -339,7 +339,7 @@ export async function checkSpecDirNaming(
|
|
|
339
339
|
ruleId: GUARD_RULES.SLOT_DIR_INVALID_FILE.id,
|
|
340
340
|
file: `spec/slots/${file}`,
|
|
341
341
|
message: `spec/slots/에 .slot.ts가 아닌 파일: ${file}`,
|
|
342
|
-
suggestion: `.slot.ts로 이름을 바꾸거나, 이 파일이 client slot이면
|
|
342
|
+
suggestion: `.slot.ts로 이름을 바꾸거나, 이 파일이 client slot이면 spec/slots/로 .client.ts 접미사로 이동하세요`,
|
|
343
343
|
});
|
|
344
344
|
}
|
|
345
345
|
}
|
|
@@ -364,76 +364,76 @@ export async function checkSpecDirNaming(
|
|
|
364
364
|
return violations;
|
|
365
365
|
}
|
|
366
366
|
|
|
367
|
-
export async function runGuardCheck(
|
|
368
|
-
manifest: RoutesManifest,
|
|
369
|
-
rootDir: string
|
|
370
|
-
): Promise<GuardCheckResult> {
|
|
371
|
-
const config = await loadManduConfig(rootDir);
|
|
372
|
-
|
|
373
|
-
const lockPath = path.join(rootDir, "
|
|
374
|
-
const mapPath = path.join(rootDir, "
|
|
375
|
-
|
|
376
|
-
// ============================================
|
|
377
|
-
// Phase 1: 독립적인 검사 병렬 실행
|
|
378
|
-
// ============================================
|
|
379
|
-
const [
|
|
380
|
-
hashViolation,
|
|
381
|
-
importViolations,
|
|
382
|
-
slotViolations,
|
|
383
|
-
specDirViolations,
|
|
384
|
-
islandViolations,
|
|
385
|
-
] = await Promise.all([
|
|
386
|
-
checkSpecHashMismatch(manifest, lockPath),
|
|
387
|
-
checkInvalidGeneratedImport(rootDir),
|
|
388
|
-
checkSlotFileExists(manifest, rootDir),
|
|
389
|
-
checkSpecDirNaming(rootDir),
|
|
390
|
-
checkIslandFirstIntegrity(manifest, rootDir),
|
|
391
|
-
]);
|
|
392
|
-
|
|
393
|
-
const violations: GuardViolation[] = [];
|
|
394
|
-
if (hashViolation) violations.push(hashViolation);
|
|
395
|
-
violations.push(...importViolations);
|
|
396
|
-
violations.push(...slotViolations);
|
|
397
|
-
violations.push(...specDirViolations);
|
|
398
|
-
violations.push(...islandViolations);
|
|
399
|
-
|
|
400
|
-
// ============================================
|
|
401
|
-
// Phase 2: generatedMap 의존 검사
|
|
402
|
-
// ============================================
|
|
403
|
-
let generatedMap: GeneratedMap | null = null;
|
|
404
|
-
if (await fileExists(mapPath)) {
|
|
405
|
-
try {
|
|
406
|
-
const mapContent = await Bun.file(mapPath).text();
|
|
407
|
-
generatedMap = JSON.parse(mapContent);
|
|
408
|
-
} catch {
|
|
409
|
-
// Map file corrupted or missing
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
if (generatedMap) {
|
|
414
|
-
const [editViolations, forbiddenViolations] = await Promise.all([
|
|
415
|
-
checkGeneratedManualEdit(rootDir, generatedMap),
|
|
416
|
-
checkForbiddenImportsInGenerated(rootDir, generatedMap),
|
|
417
|
-
]);
|
|
418
|
-
violations.push(...editViolations);
|
|
419
|
-
violations.push(...forbiddenViolations);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
// ============================================
|
|
423
|
-
// Phase 3: Slot + Contract 검사 병렬
|
|
424
|
-
// ============================================
|
|
425
|
-
const [slotContentViolations, contractViolations] = await Promise.all([
|
|
426
|
-
checkSlotContentValidation(manifest, rootDir),
|
|
427
|
-
runContractGuardCheck(manifest, rootDir),
|
|
428
|
-
]);
|
|
429
|
-
violations.push(...slotContentViolations);
|
|
430
|
-
violations.push(...contractViolations);
|
|
431
|
-
|
|
432
|
-
const resolvedViolations = applyRuleSeverity(violations, config.guard ?? {});
|
|
433
|
-
const passed = resolvedViolations.every((v) => v.severity !== "error");
|
|
434
|
-
|
|
435
|
-
return {
|
|
436
|
-
passed,
|
|
437
|
-
violations: resolvedViolations,
|
|
438
|
-
};
|
|
439
|
-
}
|
|
367
|
+
export async function runGuardCheck(
|
|
368
|
+
manifest: RoutesManifest,
|
|
369
|
+
rootDir: string
|
|
370
|
+
): Promise<GuardCheckResult> {
|
|
371
|
+
const config = await loadManduConfig(rootDir);
|
|
372
|
+
|
|
373
|
+
const lockPath = path.join(rootDir, ".mandu/spec.lock.json");
|
|
374
|
+
const mapPath = path.join(rootDir, ".mandu/generated/generated.map.json");
|
|
375
|
+
|
|
376
|
+
// ============================================
|
|
377
|
+
// Phase 1: 독립적인 검사 병렬 실행
|
|
378
|
+
// ============================================
|
|
379
|
+
const [
|
|
380
|
+
hashViolation,
|
|
381
|
+
importViolations,
|
|
382
|
+
slotViolations,
|
|
383
|
+
specDirViolations,
|
|
384
|
+
islandViolations,
|
|
385
|
+
] = await Promise.all([
|
|
386
|
+
checkSpecHashMismatch(manifest, lockPath),
|
|
387
|
+
checkInvalidGeneratedImport(rootDir),
|
|
388
|
+
checkSlotFileExists(manifest, rootDir),
|
|
389
|
+
checkSpecDirNaming(rootDir),
|
|
390
|
+
checkIslandFirstIntegrity(manifest, rootDir),
|
|
391
|
+
]);
|
|
392
|
+
|
|
393
|
+
const violations: GuardViolation[] = [];
|
|
394
|
+
if (hashViolation) violations.push(hashViolation);
|
|
395
|
+
violations.push(...importViolations);
|
|
396
|
+
violations.push(...slotViolations);
|
|
397
|
+
violations.push(...specDirViolations);
|
|
398
|
+
violations.push(...islandViolations);
|
|
399
|
+
|
|
400
|
+
// ============================================
|
|
401
|
+
// Phase 2: generatedMap 의존 검사
|
|
402
|
+
// ============================================
|
|
403
|
+
let generatedMap: GeneratedMap | null = null;
|
|
404
|
+
if (await fileExists(mapPath)) {
|
|
405
|
+
try {
|
|
406
|
+
const mapContent = await Bun.file(mapPath).text();
|
|
407
|
+
generatedMap = JSON.parse(mapContent);
|
|
408
|
+
} catch {
|
|
409
|
+
// Map file corrupted or missing
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (generatedMap) {
|
|
414
|
+
const [editViolations, forbiddenViolations] = await Promise.all([
|
|
415
|
+
checkGeneratedManualEdit(rootDir, generatedMap),
|
|
416
|
+
checkForbiddenImportsInGenerated(rootDir, generatedMap),
|
|
417
|
+
]);
|
|
418
|
+
violations.push(...editViolations);
|
|
419
|
+
violations.push(...forbiddenViolations);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ============================================
|
|
423
|
+
// Phase 3: Slot + Contract 검사 병렬
|
|
424
|
+
// ============================================
|
|
425
|
+
const [slotContentViolations, contractViolations] = await Promise.all([
|
|
426
|
+
checkSlotContentValidation(manifest, rootDir),
|
|
427
|
+
runContractGuardCheck(manifest, rootDir),
|
|
428
|
+
]);
|
|
429
|
+
violations.push(...slotContentViolations);
|
|
430
|
+
violations.push(...contractViolations);
|
|
431
|
+
|
|
432
|
+
const resolvedViolations = applyRuleSeverity(violations, config.guard ?? {});
|
|
433
|
+
const passed = resolvedViolations.every((v) => v.severity !== "error");
|
|
434
|
+
|
|
435
|
+
return {
|
|
436
|
+
passed,
|
|
437
|
+
violations: resolvedViolations,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
@@ -52,20 +52,41 @@ describe("CQRS Preset - Structure", () => {
|
|
|
52
52
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
53
53
|
|
|
54
54
|
describe("CQRS Preset - Command/Query Separation", () => {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
55
|
+
const findLayer = (name: string) => cqrsPreset.layers.find((l) => l.name === name)!;
|
|
56
|
+
|
|
57
|
+
it("commands cannot import queries", () => {
|
|
58
|
+
const commands = findLayer("application/commands");
|
|
59
|
+
expect(commands.canImport).not.toContain("application/queries");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("queries cannot import commands or events", () => {
|
|
63
|
+
const queries = findLayer("application/queries");
|
|
64
|
+
expect(queries.canImport).not.toContain("application/commands");
|
|
65
|
+
expect(queries.canImport).not.toContain("application/events");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("commands can import domain, dto, events", () => {
|
|
69
|
+
const commands = findLayer("application/commands");
|
|
70
|
+
expect(commands.canImport).toContain("domain");
|
|
71
|
+
expect(commands.canImport).toContain("application/dto");
|
|
72
|
+
expect(commands.canImport).toContain("application/events");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("queries can import domain, dto", () => {
|
|
76
|
+
const queries = findLayer("application/queries");
|
|
77
|
+
expect(queries.canImport).toContain("domain");
|
|
78
|
+
expect(queries.canImport).toContain("application/dto");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("domain can only import shared", () => {
|
|
82
|
+
const domain = findLayer("domain");
|
|
83
|
+
expect(domain.canImport).toEqual(["shared"]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("shared cannot import anything", () => {
|
|
87
|
+
const shared = findLayer("shared");
|
|
88
|
+
expect(shared.canImport).toEqual([]);
|
|
89
|
+
});
|
|
69
90
|
});
|
|
70
91
|
|
|
71
92
|
// ═══════════════════════════════════════════════════════════════════════════
|
package/src/index.ts
CHANGED
|
@@ -21,9 +21,10 @@ export * from "./seo";
|
|
|
21
21
|
export * from "./island";
|
|
22
22
|
export * from "./intent";
|
|
23
23
|
export * from "./devtools";
|
|
24
|
+
export * from "./paths";
|
|
24
25
|
|
|
25
26
|
// Consolidated Mandu namespace
|
|
26
|
-
import { ManduFilling, ManduContext, ManduFillingFactory } from "./filling";
|
|
27
|
+
import { ManduFilling, ManduContext, ManduFillingFactory, createSSEConnection } from "./filling";
|
|
27
28
|
import { createContract, defineHandler, defineRoute, createClient, contractFetch, createClientContract } from "./contract";
|
|
28
29
|
import { defineContract, generateAllFromContract, generateOpenAPISpec } from "./contract/define";
|
|
29
30
|
import { island, isIsland, type IslandComponent, type HydrationStrategy } from "./island";
|
|
@@ -74,6 +75,11 @@ export const Mandu = {
|
|
|
74
75
|
*/
|
|
75
76
|
context: ManduFillingFactory.context,
|
|
76
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Create a Server-Sent Events (SSE) connection helper
|
|
80
|
+
*/
|
|
81
|
+
sse: createSSEConnection,
|
|
82
|
+
|
|
77
83
|
// === Contract API ===
|
|
78
84
|
/**
|
|
79
85
|
* Define a typed API contract
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, test, expect } from "bun:test";
|
|
2
|
+
import { resolveGeneratedPaths, GENERATED_RELATIVE_PATHS } from "./paths";
|
|
3
|
+
import path from "path";
|
|
4
|
+
|
|
5
|
+
describe("resolveGeneratedPaths", () => {
|
|
6
|
+
test("should return .mandu/generated based paths", () => {
|
|
7
|
+
const rootDir = "/project";
|
|
8
|
+
const paths = resolveGeneratedPaths(rootDir);
|
|
9
|
+
|
|
10
|
+
expect(paths.serverRoutesDir).toBe(
|
|
11
|
+
path.join("/project", ".mandu/generated/server/routes")
|
|
12
|
+
);
|
|
13
|
+
expect(paths.webRoutesDir).toBe(
|
|
14
|
+
path.join("/project", ".mandu/generated/web/routes")
|
|
15
|
+
);
|
|
16
|
+
expect(paths.typesDir).toBe(
|
|
17
|
+
path.join("/project", ".mandu/generated/server/types")
|
|
18
|
+
);
|
|
19
|
+
expect(paths.mapDir).toBe(
|
|
20
|
+
path.join("/project", ".mandu/generated")
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("should handle Windows-style root paths", () => {
|
|
25
|
+
const rootDir = "C:\\Users\\User\\project";
|
|
26
|
+
const paths = resolveGeneratedPaths(rootDir);
|
|
27
|
+
|
|
28
|
+
expect(paths.serverRoutesDir).toContain(".mandu");
|
|
29
|
+
expect(paths.serverRoutesDir).toContain("server");
|
|
30
|
+
expect(paths.serverRoutesDir).toContain("routes");
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("GENERATED_RELATIVE_PATHS", () => {
|
|
35
|
+
test("should not contain apps/ prefix", () => {
|
|
36
|
+
expect(GENERATED_RELATIVE_PATHS.serverRoutes).not.toContain("apps/");
|
|
37
|
+
expect(GENERATED_RELATIVE_PATHS.webRoutes).not.toContain("apps/");
|
|
38
|
+
expect(GENERATED_RELATIVE_PATHS.types).not.toContain("apps/");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("should use .mandu/generated prefix", () => {
|
|
42
|
+
expect(GENERATED_RELATIVE_PATHS.serverRoutes).toStartWith(".mandu/generated");
|
|
43
|
+
expect(GENERATED_RELATIVE_PATHS.webRoutes).toStartWith(".mandu/generated");
|
|
44
|
+
expect(GENERATED_RELATIVE_PATHS.types).toStartWith(".mandu/generated");
|
|
45
|
+
expect(GENERATED_RELATIVE_PATHS.map).toStartWith(".mandu/generated");
|
|
46
|
+
});
|
|
47
|
+
});
|
package/src/paths.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 프레임워크가 생성하는 파일들의 경로 구조
|
|
5
|
+
* apps/ 하드코딩 대신 .mandu/ 기반 중앙 관리
|
|
6
|
+
*/
|
|
7
|
+
export interface GeneratedPaths {
|
|
8
|
+
/** 서버 라우트 핸들러 디렉토리 */
|
|
9
|
+
serverRoutesDir: string;
|
|
10
|
+
/** 웹 라우트 컴포넌트 디렉토리 */
|
|
11
|
+
webRoutesDir: string;
|
|
12
|
+
/** 타입 글루 디렉토리 */
|
|
13
|
+
typesDir: string;
|
|
14
|
+
/** 생성 맵 디렉토리 */
|
|
15
|
+
mapDir: string;
|
|
16
|
+
/** 생성된 매니페스트 경로 */
|
|
17
|
+
manifestPath: string;
|
|
18
|
+
/** 생성된 lock 경로 */
|
|
19
|
+
lockPath: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 프로젝트 루트에서 생성 경로를 결정
|
|
24
|
+
*/
|
|
25
|
+
export function resolveGeneratedPaths(rootDir: string): GeneratedPaths {
|
|
26
|
+
return {
|
|
27
|
+
serverRoutesDir: path.join(rootDir, ".mandu/generated/server/routes"),
|
|
28
|
+
webRoutesDir: path.join(rootDir, ".mandu/generated/web/routes"),
|
|
29
|
+
typesDir: path.join(rootDir, ".mandu/generated/server/types"),
|
|
30
|
+
mapDir: path.join(rootDir, ".mandu/generated"),
|
|
31
|
+
manifestPath: path.join(rootDir, ".mandu/routes.manifest.json"),
|
|
32
|
+
lockPath: path.join(rootDir, ".mandu/spec.lock.json"),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 생성된 파일의 상대 경로 (generatedMap.files 키 등에 사용)
|
|
38
|
+
*/
|
|
39
|
+
export const GENERATED_RELATIVE_PATHS = {
|
|
40
|
+
serverRoutes: ".mandu/generated/server/routes",
|
|
41
|
+
webRoutes: ".mandu/generated/web/routes",
|
|
42
|
+
types: ".mandu/generated/server/types",
|
|
43
|
+
map: ".mandu/generated",
|
|
44
|
+
manifest: ".mandu/routes.manifest.json",
|
|
45
|
+
lock: ".mandu/spec.lock.json",
|
|
46
|
+
history: ".mandu/history",
|
|
47
|
+
} as const;
|
package/src/report/build.ts
CHANGED
|
@@ -32,7 +32,7 @@ export function buildGuardReport(checkResult: GuardCheckResult): ManuduReport {
|
|
|
32
32
|
);
|
|
33
33
|
|
|
34
34
|
if (hasHashMismatch) {
|
|
35
|
-
nextActions.push("bunx mandu
|
|
35
|
+
nextActions.push("bunx mandu routes generate");
|
|
36
36
|
}
|
|
37
37
|
if (hasManualEdit) {
|
|
38
38
|
nextActions.push("bunx mandu generate");
|