@mandujs/cli 0.1.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.
- package/package.json +40 -0
- package/src/commands/dev.ts +60 -0
- package/src/commands/generate-apply.ts +40 -0
- package/src/commands/guard-check.ts +40 -0
- package/src/commands/init.ts +88 -0
- package/src/commands/spec-upsert.ts +47 -0
- package/src/main.ts +107 -0
- package/src/util/fs.ts +9 -0
- package/templates/default/apps/server/main.ts +49 -0
- package/templates/default/apps/web/entry.tsx +35 -0
- package/templates/default/package.json +22 -0
- package/templates/default/spec/routes.manifest.json +18 -0
- package/templates/default/tsconfig.json +14 -0
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mandujs/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Agent-Native Fullstack Framework - 에이전트가 코딩해도 아키텍처가 무너지지 않는 개발 OS",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/main.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mandu": "./src/main.ts"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src/**/*",
|
|
12
|
+
"templates/**/*"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"ai",
|
|
16
|
+
"agent",
|
|
17
|
+
"framework",
|
|
18
|
+
"fullstack",
|
|
19
|
+
"bun",
|
|
20
|
+
"typescript",
|
|
21
|
+
"react",
|
|
22
|
+
"ssr",
|
|
23
|
+
"code-generation"
|
|
24
|
+
],
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/konamgil/mandu.git"
|
|
28
|
+
},
|
|
29
|
+
"author": "konamgil",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@mandujs/core": "^0.1.0"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"bun": ">=1.0.0"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { loadManifest, startServer, registerApiHandler, registerPageLoader } from "@mandu/core";
|
|
2
|
+
import { resolveFromCwd } from "../util/fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
|
|
5
|
+
export interface DevOptions {
|
|
6
|
+
port?: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function dev(options: DevOptions = {}): Promise<void> {
|
|
10
|
+
const specPath = resolveFromCwd("spec/routes.manifest.json");
|
|
11
|
+
const rootDir = resolveFromCwd(".");
|
|
12
|
+
|
|
13
|
+
console.log(`🥟 Mandu Dev Server`);
|
|
14
|
+
console.log(`📄 Spec 파일: ${specPath}\n`);
|
|
15
|
+
|
|
16
|
+
const result = await loadManifest(specPath);
|
|
17
|
+
|
|
18
|
+
if (!result.success || !result.data) {
|
|
19
|
+
console.error("❌ Spec 로드 실패:");
|
|
20
|
+
result.errors?.forEach((e) => console.error(` - ${e}`));
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
console.log(`✅ Spec 로드 완료: ${result.data.routes.length}개 라우트`);
|
|
25
|
+
|
|
26
|
+
for (const route of result.data.routes) {
|
|
27
|
+
if (route.kind === "api") {
|
|
28
|
+
const modulePath = path.resolve(rootDir, route.module);
|
|
29
|
+
try {
|
|
30
|
+
const module = await import(modulePath);
|
|
31
|
+
registerApiHandler(route.id, module.default || module.handler);
|
|
32
|
+
console.log(` 📡 API: ${route.pattern} -> ${route.id}`);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error(` ❌ API 핸들러 로드 실패: ${route.id}`, error);
|
|
35
|
+
}
|
|
36
|
+
} else if (route.kind === "page" && route.componentModule) {
|
|
37
|
+
const componentPath = path.resolve(rootDir, route.componentModule);
|
|
38
|
+
registerPageLoader(route.id, () => import(componentPath));
|
|
39
|
+
console.log(` 📄 Page: ${route.pattern} -> ${route.id}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log("");
|
|
44
|
+
|
|
45
|
+
const port = options.port || Number(process.env.PORT) || 3000;
|
|
46
|
+
|
|
47
|
+
const server = startServer(result.data, { port });
|
|
48
|
+
|
|
49
|
+
process.on("SIGINT", () => {
|
|
50
|
+
console.log("\n🛑 서버 종료 중...");
|
|
51
|
+
server.stop();
|
|
52
|
+
process.exit(0);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
process.on("SIGTERM", () => {
|
|
56
|
+
console.log("\n🛑 서버 종료 중...");
|
|
57
|
+
server.stop();
|
|
58
|
+
process.exit(0);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { loadManifest, generateRoutes, buildGenerateReport, printReportSummary, writeReport } from "@mandu/core";
|
|
2
|
+
import { resolveFromCwd, getRootDir } from "../util/fs";
|
|
3
|
+
|
|
4
|
+
export async function generateApply(): Promise<boolean> {
|
|
5
|
+
const specPath = resolveFromCwd("spec/routes.manifest.json");
|
|
6
|
+
const rootDir = getRootDir();
|
|
7
|
+
|
|
8
|
+
console.log(`🥟 Mandu Generate`);
|
|
9
|
+
console.log(`📄 Spec 파일: ${specPath}\n`);
|
|
10
|
+
|
|
11
|
+
const result = await loadManifest(specPath);
|
|
12
|
+
|
|
13
|
+
if (!result.success || !result.data) {
|
|
14
|
+
console.error("❌ Spec 로드 실패:");
|
|
15
|
+
result.errors?.forEach((e) => console.error(` - ${e}`));
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
console.log(`✅ Spec 로드 완료 (${result.data.routes.length}개 라우트)`);
|
|
20
|
+
console.log(`🔄 코드 생성 중...\n`);
|
|
21
|
+
|
|
22
|
+
const generateResult = await generateRoutes(result.data, rootDir);
|
|
23
|
+
|
|
24
|
+
const report = buildGenerateReport(generateResult);
|
|
25
|
+
printReportSummary(report);
|
|
26
|
+
|
|
27
|
+
const reportPath = resolveFromCwd("mandu-report.json");
|
|
28
|
+
await writeReport(report, reportPath);
|
|
29
|
+
console.log(`📋 Report 저장: ${reportPath}`);
|
|
30
|
+
|
|
31
|
+
if (!generateResult.success) {
|
|
32
|
+
console.log(`\n❌ generate 실패`);
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log(`\n✅ generate 완료`);
|
|
37
|
+
console.log(`💡 다음 단계: bunx mandu guard`);
|
|
38
|
+
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { loadManifest, runGuardCheck, buildGuardReport, printReportSummary, writeReport } from "@mandu/core";
|
|
2
|
+
import { resolveFromCwd, getRootDir } from "../util/fs";
|
|
3
|
+
|
|
4
|
+
export async function guardCheck(): Promise<boolean> {
|
|
5
|
+
const specPath = resolveFromCwd("spec/routes.manifest.json");
|
|
6
|
+
const rootDir = getRootDir();
|
|
7
|
+
|
|
8
|
+
console.log(`🥟 Mandu Guard`);
|
|
9
|
+
console.log(`📄 Spec 파일: ${specPath}\n`);
|
|
10
|
+
|
|
11
|
+
const result = await loadManifest(specPath);
|
|
12
|
+
|
|
13
|
+
if (!result.success || !result.data) {
|
|
14
|
+
console.error("❌ Spec 로드 실패:");
|
|
15
|
+
result.errors?.forEach((e) => console.error(` - ${e}`));
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
console.log(`✅ Spec 로드 완료`);
|
|
20
|
+
console.log(`🔍 Guard 검사 중...\n`);
|
|
21
|
+
|
|
22
|
+
const checkResult = await runGuardCheck(result.data, rootDir);
|
|
23
|
+
|
|
24
|
+
const report = buildGuardReport(checkResult);
|
|
25
|
+
printReportSummary(report);
|
|
26
|
+
|
|
27
|
+
const reportPath = resolveFromCwd("mandu-report.json");
|
|
28
|
+
await writeReport(report, reportPath);
|
|
29
|
+
console.log(`📋 Report 저장: ${reportPath}`);
|
|
30
|
+
|
|
31
|
+
if (!checkResult.passed) {
|
|
32
|
+
console.log(`\n❌ guard 실패: ${checkResult.violations.length}개 위반 발견`);
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log(`\n✅ guard 통과`);
|
|
37
|
+
console.log(`💡 다음 단계: bunx mandu dev`);
|
|
38
|
+
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
|
|
4
|
+
export interface InitOptions {
|
|
5
|
+
name?: string;
|
|
6
|
+
template?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async function copyDir(src: string, dest: string, projectName: string): Promise<void> {
|
|
10
|
+
await fs.mkdir(dest, { recursive: true });
|
|
11
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
12
|
+
|
|
13
|
+
for (const entry of entries) {
|
|
14
|
+
const srcPath = path.join(src, entry.name);
|
|
15
|
+
const destPath = path.join(dest, entry.name);
|
|
16
|
+
|
|
17
|
+
if (entry.isDirectory()) {
|
|
18
|
+
await copyDir(srcPath, destPath, projectName);
|
|
19
|
+
} else {
|
|
20
|
+
let content = await fs.readFile(srcPath, "utf-8");
|
|
21
|
+
// Replace template variables
|
|
22
|
+
content = content.replace(/\{\{PROJECT_NAME\}\}/g, projectName);
|
|
23
|
+
await fs.writeFile(destPath, content);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getTemplatesDir(): string {
|
|
29
|
+
// When installed via npm, templates are in the CLI package
|
|
30
|
+
const commandsDir = import.meta.dir;
|
|
31
|
+
// packages/cli/src/commands -> go up 2 levels to cli package root
|
|
32
|
+
return path.resolve(commandsDir, "../../templates");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function init(options: InitOptions = {}): Promise<boolean> {
|
|
36
|
+
const projectName = options.name || "my-mandu-app";
|
|
37
|
+
const template = options.template || "default";
|
|
38
|
+
const targetDir = path.resolve(process.cwd(), projectName);
|
|
39
|
+
|
|
40
|
+
console.log(`🥟 Mandu Init`);
|
|
41
|
+
console.log(`📁 프로젝트: ${projectName}`);
|
|
42
|
+
console.log(`📦 템플릿: ${template}\n`);
|
|
43
|
+
|
|
44
|
+
// Check if target directory exists
|
|
45
|
+
try {
|
|
46
|
+
await fs.access(targetDir);
|
|
47
|
+
console.error(`❌ 디렉토리가 이미 존재합니다: ${targetDir}`);
|
|
48
|
+
return false;
|
|
49
|
+
} catch {
|
|
50
|
+
// Directory doesn't exist, good to proceed
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const templatesDir = getTemplatesDir();
|
|
54
|
+
const templateDir = path.join(templatesDir, template);
|
|
55
|
+
|
|
56
|
+
// Check if template exists
|
|
57
|
+
try {
|
|
58
|
+
await fs.access(templateDir);
|
|
59
|
+
} catch {
|
|
60
|
+
console.error(`❌ 템플릿을 찾을 수 없습니다: ${template}`);
|
|
61
|
+
console.error(` 사용 가능한 템플릿: default`);
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(`📋 템플릿 복사 중...`);
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
await copyDir(templateDir, targetDir, projectName);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error(`❌ 템플릿 복사 실패:`, error);
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Create empty generated directories
|
|
75
|
+
await fs.mkdir(path.join(targetDir, "apps/server/generated/routes"), { recursive: true });
|
|
76
|
+
await fs.mkdir(path.join(targetDir, "apps/web/generated/routes"), { recursive: true });
|
|
77
|
+
|
|
78
|
+
console.log(`\n✅ 프로젝트 생성 완료!\n`);
|
|
79
|
+
console.log(`📍 위치: ${targetDir}`);
|
|
80
|
+
console.log(`\n🚀 시작하기:`);
|
|
81
|
+
console.log(` cd ${projectName}`);
|
|
82
|
+
console.log(` bun install`);
|
|
83
|
+
console.log(` bun run spec`);
|
|
84
|
+
console.log(` bun run generate`);
|
|
85
|
+
console.log(` bun run dev`);
|
|
86
|
+
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { loadManifest, writeLock, readLock } from "@mandu/core";
|
|
2
|
+
import { resolveFromCwd } from "../util/fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
|
|
5
|
+
export interface SpecUpsertOptions {
|
|
6
|
+
file?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function specUpsert(options: SpecUpsertOptions): Promise<boolean> {
|
|
10
|
+
const specPath = options.file
|
|
11
|
+
? resolveFromCwd(options.file)
|
|
12
|
+
: resolveFromCwd("spec/routes.manifest.json");
|
|
13
|
+
|
|
14
|
+
console.log(`🥟 Mandu Spec Upsert`);
|
|
15
|
+
console.log(`📄 Spec 파일: ${specPath}\n`);
|
|
16
|
+
|
|
17
|
+
const result = await loadManifest(specPath);
|
|
18
|
+
|
|
19
|
+
if (!result.success || !result.data) {
|
|
20
|
+
console.error("❌ Spec 검증 실패:");
|
|
21
|
+
result.errors?.forEach((e) => console.error(` - ${e}`));
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
console.log(`✅ Spec 검증 통과`);
|
|
26
|
+
console.log(` - 버전: ${result.data.version}`);
|
|
27
|
+
console.log(` - 라우트 수: ${result.data.routes.length}`);
|
|
28
|
+
|
|
29
|
+
for (const route of result.data.routes) {
|
|
30
|
+
const kindIcon = route.kind === "api" ? "📡" : "📄";
|
|
31
|
+
console.log(` ${kindIcon} ${route.id}: ${route.pattern} (${route.kind})`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const lockPath = resolveFromCwd("spec/spec.lock.json");
|
|
35
|
+
const previousLock = await readLock(lockPath);
|
|
36
|
+
const newLock = await writeLock(lockPath, result.data);
|
|
37
|
+
|
|
38
|
+
console.log(`\n🔒 Lock 파일 갱신: ${lockPath}`);
|
|
39
|
+
console.log(` - 이전 해시: ${previousLock?.routesHash?.slice(0, 12) || "(없음)"}...`);
|
|
40
|
+
console.log(` - 새 해시: ${newLock.routesHash.slice(0, 12)}...`);
|
|
41
|
+
console.log(` - 갱신 시간: ${newLock.updatedAt}`);
|
|
42
|
+
|
|
43
|
+
console.log(`\n✅ spec-upsert 완료`);
|
|
44
|
+
console.log(`💡 다음 단계: bunx mandu generate`);
|
|
45
|
+
|
|
46
|
+
return true;
|
|
47
|
+
}
|
package/src/main.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { specUpsert } from "./commands/spec-upsert";
|
|
4
|
+
import { generateApply } from "./commands/generate-apply";
|
|
5
|
+
import { guardCheck } from "./commands/guard-check";
|
|
6
|
+
import { dev } from "./commands/dev";
|
|
7
|
+
import { init } from "./commands/init";
|
|
8
|
+
|
|
9
|
+
const HELP_TEXT = `
|
|
10
|
+
🥟 Mandu CLI - Agent-Native Fullstack Framework
|
|
11
|
+
|
|
12
|
+
Usage: bunx mandu <command> [options]
|
|
13
|
+
|
|
14
|
+
Commands:
|
|
15
|
+
init 새 프로젝트 생성
|
|
16
|
+
spec-upsert Spec 파일 검증 및 lock 갱신
|
|
17
|
+
generate Spec에서 코드 생성
|
|
18
|
+
guard Guard 규칙 검사
|
|
19
|
+
dev 개발 서버 실행
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
--name <name> init 시 프로젝트 이름 (기본: my-mandu-app)
|
|
23
|
+
--file <path> spec-upsert 시 사용할 spec 파일 경로
|
|
24
|
+
--port <port> dev 서버 포트 (기본: 3000)
|
|
25
|
+
--help, -h 도움말 표시
|
|
26
|
+
|
|
27
|
+
Examples:
|
|
28
|
+
bunx mandu init --name my-app
|
|
29
|
+
bunx mandu spec-upsert
|
|
30
|
+
bunx mandu generate
|
|
31
|
+
bunx mandu guard
|
|
32
|
+
bunx mandu dev --port 3000
|
|
33
|
+
|
|
34
|
+
Workflow:
|
|
35
|
+
1. init → 2. spec-upsert → 3. generate → 4. guard → 5. dev
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
function parseArgs(args: string[]): { command: string; options: Record<string, string> } {
|
|
39
|
+
const command = args[0] || "";
|
|
40
|
+
const options: Record<string, string> = {};
|
|
41
|
+
|
|
42
|
+
for (let i = 1; i < args.length; i++) {
|
|
43
|
+
const arg = args[i];
|
|
44
|
+
if (arg.startsWith("--")) {
|
|
45
|
+
const key = arg.slice(2);
|
|
46
|
+
const value = args[i + 1] && !args[i + 1].startsWith("--") ? args[++i] : "true";
|
|
47
|
+
options[key] = value;
|
|
48
|
+
} else if (arg === "-h") {
|
|
49
|
+
options["help"] = "true";
|
|
50
|
+
} else if (!options._positional) {
|
|
51
|
+
// First non-flag argument after command is positional (e.g., project name)
|
|
52
|
+
options._positional = arg;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { command, options };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function main(): Promise<void> {
|
|
60
|
+
const args = process.argv.slice(2);
|
|
61
|
+
const { command, options } = parseArgs(args);
|
|
62
|
+
|
|
63
|
+
if (options.help || command === "help" || !command) {
|
|
64
|
+
console.log(HELP_TEXT);
|
|
65
|
+
process.exit(0);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let success = true;
|
|
69
|
+
|
|
70
|
+
switch (command) {
|
|
71
|
+
case "init":
|
|
72
|
+
success = await init({
|
|
73
|
+
name: options.name || options._positional
|
|
74
|
+
});
|
|
75
|
+
break;
|
|
76
|
+
|
|
77
|
+
case "spec-upsert":
|
|
78
|
+
success = await specUpsert({ file: options.file });
|
|
79
|
+
break;
|
|
80
|
+
|
|
81
|
+
case "generate":
|
|
82
|
+
success = await generateApply();
|
|
83
|
+
break;
|
|
84
|
+
|
|
85
|
+
case "guard":
|
|
86
|
+
success = await guardCheck();
|
|
87
|
+
break;
|
|
88
|
+
|
|
89
|
+
case "dev":
|
|
90
|
+
await dev({ port: options.port ? Number(options.port) : undefined });
|
|
91
|
+
break;
|
|
92
|
+
|
|
93
|
+
default:
|
|
94
|
+
console.error(`❌ Unknown command: ${command}`);
|
|
95
|
+
console.log(HELP_TEXT);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!success) {
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
main().catch((error) => {
|
|
105
|
+
console.error("❌ 예상치 못한 오류:", error);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
});
|
package/src/util/fs.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { loadManifest, startServer, registerApiHandler, registerPageLoader } from "@mandu/core";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
const SPEC_PATH = path.resolve(import.meta.dir, "../../spec/routes.manifest.json");
|
|
5
|
+
|
|
6
|
+
async function main() {
|
|
7
|
+
console.log("🥟 Mandu Server Starting...\n");
|
|
8
|
+
|
|
9
|
+
const result = await loadManifest(SPEC_PATH);
|
|
10
|
+
|
|
11
|
+
if (!result.success || !result.data) {
|
|
12
|
+
console.error("❌ Spec 로드 실패:");
|
|
13
|
+
result.errors?.forEach((e) => console.error(` - ${e}`));
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
console.log(`✅ Spec 로드 완료: ${result.data.routes.length}개 라우트`);
|
|
18
|
+
|
|
19
|
+
for (const route of result.data.routes) {
|
|
20
|
+
if (route.kind === "api") {
|
|
21
|
+
const modulePath = path.resolve(import.meta.dir, "../../", route.module);
|
|
22
|
+
try {
|
|
23
|
+
const module = await import(modulePath);
|
|
24
|
+
registerApiHandler(route.id, module.default || module.handler);
|
|
25
|
+
console.log(` 📡 API: ${route.pattern} -> ${route.id}`);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
console.error(` ❌ API 핸들러 로드 실패: ${route.id}`, error);
|
|
28
|
+
}
|
|
29
|
+
} else if (route.kind === "page") {
|
|
30
|
+
const componentPath = path.resolve(import.meta.dir, "../../", route.componentModule!);
|
|
31
|
+
registerPageLoader(route.id, () => import(componentPath));
|
|
32
|
+
console.log(` 📄 Page: ${route.pattern} -> ${route.id}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
console.log("");
|
|
37
|
+
|
|
38
|
+
const server = startServer(result.data, {
|
|
39
|
+
port: Number(process.env.PORT) || 3000,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
process.on("SIGINT", () => {
|
|
43
|
+
console.log("\n🛑 서버 종료 중...");
|
|
44
|
+
server.stop();
|
|
45
|
+
process.exit(0);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { ReactElement } from "react";
|
|
3
|
+
|
|
4
|
+
export interface AppContext {
|
|
5
|
+
routeId: string;
|
|
6
|
+
url: string;
|
|
7
|
+
params: Record<string, string>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type RouteComponent = (props: { params: Record<string, string> }) => ReactElement;
|
|
11
|
+
|
|
12
|
+
const routeComponents: Record<string, RouteComponent> = {};
|
|
13
|
+
|
|
14
|
+
export function registerRoute(routeId: string, component: RouteComponent): void {
|
|
15
|
+
routeComponents[routeId] = component;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function createApp(context: AppContext): ReactElement {
|
|
19
|
+
const Component = routeComponents[context.routeId];
|
|
20
|
+
|
|
21
|
+
if (!Component) {
|
|
22
|
+
return (
|
|
23
|
+
<div>
|
|
24
|
+
<h1>404 - Route Not Found</h1>
|
|
25
|
+
<p>Route ID: {context.routeId}</p>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return <Component params={context.params} />;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getRegisteredRoutes(): string[] {
|
|
34
|
+
return Object.keys(routeComponents);
|
|
35
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PROJECT_NAME}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "mandu dev",
|
|
7
|
+
"generate": "mandu generate",
|
|
8
|
+
"guard": "mandu guard",
|
|
9
|
+
"spec": "mandu spec-upsert"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@mandujs/core": "^0.1.0",
|
|
13
|
+
"react": "^18.2.0",
|
|
14
|
+
"react-dom": "^18.2.0"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@mandujs/cli": "^0.1.0",
|
|
18
|
+
"@types/react": "^18.2.0",
|
|
19
|
+
"@types/react-dom": "^18.2.0",
|
|
20
|
+
"typescript": "^5.0.0"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"routes": [
|
|
4
|
+
{
|
|
5
|
+
"id": "home",
|
|
6
|
+
"pattern": "/",
|
|
7
|
+
"kind": "page",
|
|
8
|
+
"module": "apps/server/generated/routes/home.route.ts",
|
|
9
|
+
"componentModule": "apps/web/generated/routes/home.route.tsx"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"id": "health",
|
|
13
|
+
"pattern": "/api/health",
|
|
14
|
+
"kind": "api",
|
|
15
|
+
"module": "apps/server/generated/routes/health.route.ts"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"esModuleInterop": true,
|
|
7
|
+
"strict": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"jsx": "react-jsx",
|
|
10
|
+
"types": ["bun-types"]
|
|
11
|
+
},
|
|
12
|
+
"include": ["apps/**/*.ts", "apps/**/*.tsx"],
|
|
13
|
+
"exclude": ["node_modules"]
|
|
14
|
+
}
|