@mandujs/cli 0.12.2 → 0.13.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/README.ko.md +234 -234
- package/README.md +354 -354
- package/package.json +2 -2
- package/src/commands/contract.ts +173 -173
- package/src/commands/dev.ts +8 -68
- package/src/commands/doctor.ts +27 -27
- package/src/commands/guard-arch.ts +303 -303
- package/src/commands/guard-check.ts +3 -3
- package/src/commands/monitor.ts +300 -300
- package/src/commands/openapi.ts +107 -107
- package/src/commands/registry.ts +367 -357
- package/src/commands/routes.ts +228 -228
- package/src/commands/start.ts +184 -0
- package/src/errors/codes.ts +35 -35
- package/src/errors/index.ts +2 -2
- package/src/errors/messages.ts +143 -143
- package/src/hooks/index.ts +17 -17
- package/src/hooks/preaction.ts +256 -256
- package/src/main.ts +37 -34
- package/src/terminal/banner.ts +166 -166
- package/src/terminal/help.ts +306 -306
- package/src/terminal/index.ts +71 -71
- package/src/terminal/output.ts +295 -295
- package/src/terminal/palette.ts +30 -30
- package/src/terminal/progress.ts +327 -327
- package/src/terminal/stream-writer.ts +214 -214
- package/src/terminal/table.ts +354 -354
- package/src/terminal/theme.ts +142 -142
- package/src/util/bun.ts +6 -6
- package/src/util/fs.ts +23 -23
- package/src/util/handlers.ts +96 -0
- package/src/util/manifest.ts +52 -52
- package/src/util/output.ts +22 -22
- package/src/util/port.ts +71 -71
- package/templates/default/AGENTS.md +96 -96
- package/templates/default/app/api/health/route.ts +13 -13
- package/templates/default/app/globals.css +49 -49
- package/templates/default/app/layout.tsx +27 -27
- package/templates/default/app/page.tsx +38 -38
- package/templates/default/package.json +1 -0
- package/templates/default/src/client/shared/lib/utils.ts +16 -16
- package/templates/default/src/client/shared/ui/button.tsx +57 -57
- package/templates/default/src/client/shared/ui/card.tsx +78 -78
- package/templates/default/src/client/shared/ui/index.ts +21 -21
- package/templates/default/src/client/shared/ui/input.tsx +24 -24
- package/templates/default/tests/example.test.ts +58 -58
- package/templates/default/tests/helpers.ts +52 -52
- package/templates/default/tests/setup.ts +9 -9
- package/templates/default/tsconfig.json +12 -14
- package/templates/default/apps/server/main.ts +0 -67
- package/templates/default/apps/web/entry.tsx +0 -35
package/src/terminal/theme.ts
CHANGED
|
@@ -1,142 +1,142 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DNA-009: Mandu CLI Theme System
|
|
3
|
-
*
|
|
4
|
-
* Chalk-based dynamic color theme with NO_COLOR/FORCE_COLOR support
|
|
5
|
-
* Inspired by OpenClaw's terminal/theme.ts
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { MANDU_PALETTE } from "./palette.js";
|
|
9
|
-
|
|
10
|
-
// Bun's native console supports colors, but we need a simple wrapper
|
|
11
|
-
// for consistent theming across the CLI
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Check if rich output (colors) is supported
|
|
15
|
-
*/
|
|
16
|
-
function checkRichSupport(): boolean {
|
|
17
|
-
// NO_COLOR takes precedence (accessibility standard)
|
|
18
|
-
if (process.env.NO_COLOR) {
|
|
19
|
-
const forceColor = process.env.FORCE_COLOR?.trim();
|
|
20
|
-
if (forceColor !== "1" && forceColor !== "true") {
|
|
21
|
-
return false;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Check TTY
|
|
26
|
-
if (!process.stdout.isTTY) {
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Check TERM
|
|
31
|
-
const term = process.env.TERM;
|
|
32
|
-
if (term === "dumb") {
|
|
33
|
-
return false;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return true;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const richSupported = checkRichSupport();
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* ANSI escape code wrapper
|
|
43
|
-
*/
|
|
44
|
-
function ansi(code: string) {
|
|
45
|
-
return richSupported ? `\x1b[${code}m` : "";
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Convert hex to ANSI 256 color (approximation)
|
|
50
|
-
*/
|
|
51
|
-
function hexToAnsi256(hex: string): number {
|
|
52
|
-
const r = parseInt(hex.slice(1, 3), 16);
|
|
53
|
-
const g = parseInt(hex.slice(3, 5), 16);
|
|
54
|
-
const b = parseInt(hex.slice(5, 7), 16);
|
|
55
|
-
|
|
56
|
-
// Convert to 6x6x6 color cube
|
|
57
|
-
const ri = Math.round((r / 255) * 5);
|
|
58
|
-
const gi = Math.round((g / 255) * 5);
|
|
59
|
-
const bi = Math.round((b / 255) * 5);
|
|
60
|
-
|
|
61
|
-
return 16 + 36 * ri + 6 * gi + bi;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Create a color function from hex
|
|
66
|
-
*/
|
|
67
|
-
function hex(hexColor: string): (text: string) => string {
|
|
68
|
-
if (!richSupported) return (text) => text;
|
|
69
|
-
|
|
70
|
-
const colorCode = hexToAnsi256(hexColor);
|
|
71
|
-
return (text) => `\x1b[38;5;${colorCode}m${text}\x1b[0m`;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Create a bold color function
|
|
76
|
-
*/
|
|
77
|
-
function boldHex(hexColor: string): (text: string) => string {
|
|
78
|
-
if (!richSupported) return (text) => text;
|
|
79
|
-
|
|
80
|
-
const colorCode = hexToAnsi256(hexColor);
|
|
81
|
-
return (text) => `\x1b[1;38;5;${colorCode}m${text}\x1b[0m`;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Mandu CLI Theme
|
|
86
|
-
*/
|
|
87
|
-
export const theme = {
|
|
88
|
-
// Brand colors
|
|
89
|
-
accent: hex(MANDU_PALETTE.accent),
|
|
90
|
-
accentBright: hex(MANDU_PALETTE.accentBright),
|
|
91
|
-
accentDim: hex(MANDU_PALETTE.accentDim),
|
|
92
|
-
|
|
93
|
-
// Semantic colors
|
|
94
|
-
info: hex(MANDU_PALETTE.info),
|
|
95
|
-
success: hex(MANDU_PALETTE.success),
|
|
96
|
-
warn: hex(MANDU_PALETTE.warn),
|
|
97
|
-
error: hex(MANDU_PALETTE.error),
|
|
98
|
-
|
|
99
|
-
// Neutral
|
|
100
|
-
muted: hex(MANDU_PALETTE.muted),
|
|
101
|
-
dim: hex(MANDU_PALETTE.dim),
|
|
102
|
-
|
|
103
|
-
// Composite styles
|
|
104
|
-
heading: boldHex(MANDU_PALETTE.accent),
|
|
105
|
-
command: hex(MANDU_PALETTE.accentBright),
|
|
106
|
-
option: hex(MANDU_PALETTE.warn),
|
|
107
|
-
path: hex(MANDU_PALETTE.info),
|
|
108
|
-
|
|
109
|
-
// Basic styles
|
|
110
|
-
bold: richSupported ? (text: string) => `\x1b[1m${text}\x1b[0m` : (text: string) => text,
|
|
111
|
-
italic: richSupported ? (text: string) => `\x1b[3m${text}\x1b[0m` : (text: string) => text,
|
|
112
|
-
underline: richSupported ? (text: string) => `\x1b[4m${text}\x1b[0m` : (text: string) => text,
|
|
113
|
-
|
|
114
|
-
// Reset
|
|
115
|
-
reset: richSupported ? "\x1b[0m" : "",
|
|
116
|
-
} as const;
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Check if rich output is available
|
|
120
|
-
*/
|
|
121
|
-
export function isRich(): boolean {
|
|
122
|
-
return richSupported;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Conditionally apply color based on rich mode
|
|
127
|
-
*/
|
|
128
|
-
export function colorize(
|
|
129
|
-
rich: boolean,
|
|
130
|
-
colorFn: (text: string) => string,
|
|
131
|
-
text: string
|
|
132
|
-
): string {
|
|
133
|
-
return rich ? colorFn(text) : text;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Strip ANSI codes from string (for width calculation)
|
|
138
|
-
*/
|
|
139
|
-
export function stripAnsi(text: string): string {
|
|
140
|
-
// eslint-disable-next-line no-control-regex
|
|
141
|
-
return text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
142
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* DNA-009: Mandu CLI Theme System
|
|
3
|
+
*
|
|
4
|
+
* Chalk-based dynamic color theme with NO_COLOR/FORCE_COLOR support
|
|
5
|
+
* Inspired by OpenClaw's terminal/theme.ts
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { MANDU_PALETTE } from "./palette.js";
|
|
9
|
+
|
|
10
|
+
// Bun's native console supports colors, but we need a simple wrapper
|
|
11
|
+
// for consistent theming across the CLI
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Check if rich output (colors) is supported
|
|
15
|
+
*/
|
|
16
|
+
function checkRichSupport(): boolean {
|
|
17
|
+
// NO_COLOR takes precedence (accessibility standard)
|
|
18
|
+
if (process.env.NO_COLOR) {
|
|
19
|
+
const forceColor = process.env.FORCE_COLOR?.trim();
|
|
20
|
+
if (forceColor !== "1" && forceColor !== "true") {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Check TTY
|
|
26
|
+
if (!process.stdout.isTTY) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Check TERM
|
|
31
|
+
const term = process.env.TERM;
|
|
32
|
+
if (term === "dumb") {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const richSupported = checkRichSupport();
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* ANSI escape code wrapper
|
|
43
|
+
*/
|
|
44
|
+
function ansi(code: string) {
|
|
45
|
+
return richSupported ? `\x1b[${code}m` : "";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Convert hex to ANSI 256 color (approximation)
|
|
50
|
+
*/
|
|
51
|
+
function hexToAnsi256(hex: string): number {
|
|
52
|
+
const r = parseInt(hex.slice(1, 3), 16);
|
|
53
|
+
const g = parseInt(hex.slice(3, 5), 16);
|
|
54
|
+
const b = parseInt(hex.slice(5, 7), 16);
|
|
55
|
+
|
|
56
|
+
// Convert to 6x6x6 color cube
|
|
57
|
+
const ri = Math.round((r / 255) * 5);
|
|
58
|
+
const gi = Math.round((g / 255) * 5);
|
|
59
|
+
const bi = Math.round((b / 255) * 5);
|
|
60
|
+
|
|
61
|
+
return 16 + 36 * ri + 6 * gi + bi;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Create a color function from hex
|
|
66
|
+
*/
|
|
67
|
+
function hex(hexColor: string): (text: string) => string {
|
|
68
|
+
if (!richSupported) return (text) => text;
|
|
69
|
+
|
|
70
|
+
const colorCode = hexToAnsi256(hexColor);
|
|
71
|
+
return (text) => `\x1b[38;5;${colorCode}m${text}\x1b[0m`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Create a bold color function
|
|
76
|
+
*/
|
|
77
|
+
function boldHex(hexColor: string): (text: string) => string {
|
|
78
|
+
if (!richSupported) return (text) => text;
|
|
79
|
+
|
|
80
|
+
const colorCode = hexToAnsi256(hexColor);
|
|
81
|
+
return (text) => `\x1b[1;38;5;${colorCode}m${text}\x1b[0m`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Mandu CLI Theme
|
|
86
|
+
*/
|
|
87
|
+
export const theme = {
|
|
88
|
+
// Brand colors
|
|
89
|
+
accent: hex(MANDU_PALETTE.accent),
|
|
90
|
+
accentBright: hex(MANDU_PALETTE.accentBright),
|
|
91
|
+
accentDim: hex(MANDU_PALETTE.accentDim),
|
|
92
|
+
|
|
93
|
+
// Semantic colors
|
|
94
|
+
info: hex(MANDU_PALETTE.info),
|
|
95
|
+
success: hex(MANDU_PALETTE.success),
|
|
96
|
+
warn: hex(MANDU_PALETTE.warn),
|
|
97
|
+
error: hex(MANDU_PALETTE.error),
|
|
98
|
+
|
|
99
|
+
// Neutral
|
|
100
|
+
muted: hex(MANDU_PALETTE.muted),
|
|
101
|
+
dim: hex(MANDU_PALETTE.dim),
|
|
102
|
+
|
|
103
|
+
// Composite styles
|
|
104
|
+
heading: boldHex(MANDU_PALETTE.accent),
|
|
105
|
+
command: hex(MANDU_PALETTE.accentBright),
|
|
106
|
+
option: hex(MANDU_PALETTE.warn),
|
|
107
|
+
path: hex(MANDU_PALETTE.info),
|
|
108
|
+
|
|
109
|
+
// Basic styles
|
|
110
|
+
bold: richSupported ? (text: string) => `\x1b[1m${text}\x1b[0m` : (text: string) => text,
|
|
111
|
+
italic: richSupported ? (text: string) => `\x1b[3m${text}\x1b[0m` : (text: string) => text,
|
|
112
|
+
underline: richSupported ? (text: string) => `\x1b[4m${text}\x1b[0m` : (text: string) => text,
|
|
113
|
+
|
|
114
|
+
// Reset
|
|
115
|
+
reset: richSupported ? "\x1b[0m" : "",
|
|
116
|
+
} as const;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Check if rich output is available
|
|
120
|
+
*/
|
|
121
|
+
export function isRich(): boolean {
|
|
122
|
+
return richSupported;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Conditionally apply color based on rich mode
|
|
127
|
+
*/
|
|
128
|
+
export function colorize(
|
|
129
|
+
rich: boolean,
|
|
130
|
+
colorFn: (text: string) => string,
|
|
131
|
+
text: string
|
|
132
|
+
): string {
|
|
133
|
+
return rich ? colorFn(text) : text;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Strip ANSI codes from string (for width calculation)
|
|
138
|
+
*/
|
|
139
|
+
export function stripAnsi(text: string): string {
|
|
140
|
+
// eslint-disable-next-line no-control-regex
|
|
141
|
+
return text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
142
|
+
}
|
package/src/util/bun.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export function importFresh<T = unknown>(modulePath: string): Promise<T> {
|
|
2
|
-
const url = Bun.pathToFileURL(modulePath);
|
|
3
|
-
const cacheBusted = new URL(url.href);
|
|
4
|
-
cacheBusted.searchParams.set("t", Date.now().toString());
|
|
5
|
-
return import(cacheBusted.href) as Promise<T>;
|
|
6
|
-
}
|
|
1
|
+
export function importFresh<T = unknown>(modulePath: string): Promise<T> {
|
|
2
|
+
const url = Bun.pathToFileURL(modulePath);
|
|
3
|
+
const cacheBusted = new URL(url.href);
|
|
4
|
+
cacheBusted.searchParams.set("t", Date.now().toString());
|
|
5
|
+
return import(cacheBusted.href) as Promise<T>;
|
|
6
|
+
}
|
package/src/util/fs.ts
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
import path from "path";
|
|
2
|
-
import fs from "fs/promises";
|
|
1
|
+
import path from "path";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
3
|
|
|
4
4
|
export function resolveFromCwd(...paths: string[]): string {
|
|
5
5
|
return path.resolve(process.cwd(), ...paths);
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
export function getRootDir(): string {
|
|
9
|
-
return process.cwd();
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export async function pathExists(targetPath: string): Promise<boolean> {
|
|
13
|
-
try {
|
|
14
|
-
await fs.access(targetPath);
|
|
15
|
-
return true;
|
|
16
|
-
} catch {
|
|
17
|
-
return false;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export async function isDirectory(targetPath: string): Promise<boolean> {
|
|
22
|
-
try {
|
|
23
|
-
const stat = await fs.stat(targetPath);
|
|
24
|
-
return stat.isDirectory();
|
|
25
|
-
} catch {
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
8
|
+
export function getRootDir(): string {
|
|
9
|
+
return process.cwd();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function pathExists(targetPath: string): Promise<boolean> {
|
|
13
|
+
try {
|
|
14
|
+
await fs.access(targetPath);
|
|
15
|
+
return true;
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function isDirectory(targetPath: string): Promise<boolean> {
|
|
22
|
+
try {
|
|
23
|
+
const stat = await fs.stat(targetPath);
|
|
24
|
+
return stat.isDirectory();
|
|
25
|
+
} catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import {
|
|
2
|
+
registerApiHandler,
|
|
3
|
+
registerPageLoader,
|
|
4
|
+
registerPageHandler,
|
|
5
|
+
registerLayoutLoader,
|
|
6
|
+
needsHydration,
|
|
7
|
+
type RoutesManifest,
|
|
8
|
+
} from "@mandujs/core";
|
|
9
|
+
import path from "path";
|
|
10
|
+
|
|
11
|
+
export interface RegisterHandlersOptions {
|
|
12
|
+
/** 모듈 import 함수 (dev: importFresh, start: 표준 import) */
|
|
13
|
+
importFn: (modulePath: string) => Promise<any>;
|
|
14
|
+
/** 이미 등록된 layout 경로 추적용 Set */
|
|
15
|
+
registeredLayouts: Set<string>;
|
|
16
|
+
/** 리로드 시 layout 캐시 클리어 */
|
|
17
|
+
isReload?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 매니페스트 라우트를 서버 핸들러로 등록
|
|
22
|
+
* dev.ts와 start.ts에서 공유
|
|
23
|
+
*/
|
|
24
|
+
export async function registerManifestHandlers(
|
|
25
|
+
manifest: RoutesManifest,
|
|
26
|
+
rootDir: string,
|
|
27
|
+
options: RegisterHandlersOptions
|
|
28
|
+
): Promise<void> {
|
|
29
|
+
const { importFn, registeredLayouts, isReload = false } = options;
|
|
30
|
+
|
|
31
|
+
if (isReload) {
|
|
32
|
+
registeredLayouts.clear();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
for (const route of manifest.routes) {
|
|
36
|
+
if (route.kind === "api") {
|
|
37
|
+
const modulePath = path.resolve(rootDir, route.module);
|
|
38
|
+
try {
|
|
39
|
+
const module = await importFn(modulePath);
|
|
40
|
+
let handler = module.default || module.handler || module;
|
|
41
|
+
|
|
42
|
+
// ManduFilling 인스턴스를 핸들러 함수로 래핑
|
|
43
|
+
if (handler && typeof handler.handle === "function") {
|
|
44
|
+
console.log(` 🔄 ManduFilling 래핑: ${route.id}`);
|
|
45
|
+
const filling = handler;
|
|
46
|
+
handler = async (req: Request, params?: Record<string, string>) => {
|
|
47
|
+
return filling.handle(req, params);
|
|
48
|
+
};
|
|
49
|
+
} else {
|
|
50
|
+
console.log(
|
|
51
|
+
` ⚠️ 핸들러 타입: ${typeof handler}, handle: ${typeof handler?.handle}`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
registerApiHandler(route.id, handler);
|
|
56
|
+
console.log(` 📡 API: ${route.pattern} -> ${route.id}`);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error(` ❌ API 핸들러 로드 실패: ${route.id}`, error);
|
|
59
|
+
}
|
|
60
|
+
} else if (route.kind === "page" && route.componentModule) {
|
|
61
|
+
const componentPath = path.resolve(rootDir, route.componentModule);
|
|
62
|
+
const isIsland = needsHydration(route);
|
|
63
|
+
const hasLayout = route.layoutChain && route.layoutChain.length > 0;
|
|
64
|
+
|
|
65
|
+
// Layout 로더 등록
|
|
66
|
+
if (route.layoutChain) {
|
|
67
|
+
for (const layoutPath of route.layoutChain) {
|
|
68
|
+
if (!registeredLayouts.has(layoutPath)) {
|
|
69
|
+
const absLayoutPath = path.resolve(rootDir, layoutPath);
|
|
70
|
+
registerLayoutLoader(layoutPath, async () => {
|
|
71
|
+
return importFn(absLayoutPath);
|
|
72
|
+
});
|
|
73
|
+
registeredLayouts.add(layoutPath);
|
|
74
|
+
console.log(` 🎨 Layout: ${layoutPath}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// slotModule이 있으면 PageHandler 사용 (filling.loader 지원)
|
|
80
|
+
if (route.slotModule) {
|
|
81
|
+
registerPageHandler(route.id, async () => {
|
|
82
|
+
const module = await importFn(componentPath);
|
|
83
|
+
return module.default;
|
|
84
|
+
});
|
|
85
|
+
console.log(
|
|
86
|
+
` 📄 Page: ${route.pattern} -> ${route.id} (with loader)${isIsland ? " 🏝️" : ""}${hasLayout ? " 🎨" : ""}`
|
|
87
|
+
);
|
|
88
|
+
} else {
|
|
89
|
+
registerPageLoader(route.id, () => importFn(componentPath));
|
|
90
|
+
console.log(
|
|
91
|
+
` 📄 Page: ${route.pattern} -> ${route.id}${isIsland ? " 🏝️" : ""}${hasLayout ? " 🎨" : ""}`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
package/src/util/manifest.ts
CHANGED
|
@@ -1,52 +1,52 @@
|
|
|
1
|
-
import path from "path";
|
|
2
|
-
import {
|
|
3
|
-
generateManifest,
|
|
4
|
-
loadManifest,
|
|
5
|
-
type RoutesManifest,
|
|
6
|
-
type FSScannerConfig,
|
|
7
|
-
} from "@mandujs/core";
|
|
8
|
-
import { isDirectory } from "./fs";
|
|
9
|
-
|
|
10
|
-
export type ManifestSource = "fs" | "spec";
|
|
11
|
-
|
|
12
|
-
export interface ResolvedManifest {
|
|
13
|
-
manifest: RoutesManifest;
|
|
14
|
-
source: ManifestSource;
|
|
15
|
-
warnings: string[];
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export async function resolveManifest(
|
|
19
|
-
rootDir: string,
|
|
20
|
-
options: { fsRoutes?: FSScannerConfig; outputPath?: string } = {}
|
|
21
|
-
): Promise<ResolvedManifest> {
|
|
22
|
-
const appDir = path.resolve(rootDir, "app");
|
|
23
|
-
const hasApp = await isDirectory(appDir);
|
|
24
|
-
|
|
25
|
-
if (hasApp) {
|
|
26
|
-
const result = await generateManifest(rootDir, {
|
|
27
|
-
scanner: options.fsRoutes,
|
|
28
|
-
outputPath: options.outputPath,
|
|
29
|
-
skipLegacy: true,
|
|
30
|
-
});
|
|
31
|
-
return {
|
|
32
|
-
manifest: result.manifest,
|
|
33
|
-
source: "fs",
|
|
34
|
-
warnings: result.warnings,
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const specPath = path.join(rootDir, "spec", "routes.manifest.json");
|
|
39
|
-
if (await Bun.file(specPath).exists()) {
|
|
40
|
-
const result = await loadManifest(specPath);
|
|
41
|
-
if (!result.success) {
|
|
42
|
-
throw new Error(result.errors?.join(", ") || "Failed to load routes manifest");
|
|
43
|
-
}
|
|
44
|
-
return {
|
|
45
|
-
manifest: result.data!,
|
|
46
|
-
source: "spec",
|
|
47
|
-
warnings: [],
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
throw new Error("No routes found. Create app/ routes or spec/routes.manifest.json");
|
|
52
|
-
}
|
|
1
|
+
import path from "path";
|
|
2
|
+
import {
|
|
3
|
+
generateManifest,
|
|
4
|
+
loadManifest,
|
|
5
|
+
type RoutesManifest,
|
|
6
|
+
type FSScannerConfig,
|
|
7
|
+
} from "@mandujs/core";
|
|
8
|
+
import { isDirectory } from "./fs";
|
|
9
|
+
|
|
10
|
+
export type ManifestSource = "fs" | "spec";
|
|
11
|
+
|
|
12
|
+
export interface ResolvedManifest {
|
|
13
|
+
manifest: RoutesManifest;
|
|
14
|
+
source: ManifestSource;
|
|
15
|
+
warnings: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function resolveManifest(
|
|
19
|
+
rootDir: string,
|
|
20
|
+
options: { fsRoutes?: FSScannerConfig; outputPath?: string } = {}
|
|
21
|
+
): Promise<ResolvedManifest> {
|
|
22
|
+
const appDir = path.resolve(rootDir, "app");
|
|
23
|
+
const hasApp = await isDirectory(appDir);
|
|
24
|
+
|
|
25
|
+
if (hasApp) {
|
|
26
|
+
const result = await generateManifest(rootDir, {
|
|
27
|
+
scanner: options.fsRoutes,
|
|
28
|
+
outputPath: options.outputPath,
|
|
29
|
+
skipLegacy: true,
|
|
30
|
+
});
|
|
31
|
+
return {
|
|
32
|
+
manifest: result.manifest,
|
|
33
|
+
source: "fs",
|
|
34
|
+
warnings: result.warnings,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const specPath = path.join(rootDir, "spec", "routes.manifest.json");
|
|
39
|
+
if (await Bun.file(specPath).exists()) {
|
|
40
|
+
const result = await loadManifest(specPath);
|
|
41
|
+
if (!result.success) {
|
|
42
|
+
throw new Error(result.errors?.join(", ") || "Failed to load routes manifest");
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
manifest: result.data!,
|
|
46
|
+
source: "spec",
|
|
47
|
+
warnings: [],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
throw new Error("No routes found. Create app/ routes or spec/routes.manifest.json");
|
|
52
|
+
}
|
package/src/util/output.ts
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import { getOutputMode } from "../terminal/output";
|
|
2
|
-
|
|
3
|
-
export type OutputFormat = "console" | "agent" | "json";
|
|
4
|
-
|
|
5
|
-
function normalizeFormat(value?: string): OutputFormat | undefined {
|
|
6
|
-
if (!value) return undefined;
|
|
7
|
-
const normalized = value.toLowerCase();
|
|
8
|
-
if (normalized === "console" || normalized === "agent" || normalized === "json") {
|
|
9
|
-
return normalized;
|
|
10
|
-
}
|
|
11
|
-
return undefined;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function resolveOutputFormat(explicit?: OutputFormat): OutputFormat {
|
|
15
|
-
const env = process.env;
|
|
16
|
-
|
|
17
|
-
const direct = normalizeFormat(explicit) ?? normalizeFormat(env.MANDU_OUTPUT);
|
|
18
|
-
if (direct) return direct;
|
|
19
|
-
|
|
20
|
-
const mode = getOutputMode();
|
|
21
|
-
return mode === "json" ? "json" : "console";
|
|
22
|
-
}
|
|
1
|
+
import { getOutputMode } from "../terminal/output";
|
|
2
|
+
|
|
3
|
+
export type OutputFormat = "console" | "agent" | "json";
|
|
4
|
+
|
|
5
|
+
function normalizeFormat(value?: string): OutputFormat | undefined {
|
|
6
|
+
if (!value) return undefined;
|
|
7
|
+
const normalized = value.toLowerCase();
|
|
8
|
+
if (normalized === "console" || normalized === "agent" || normalized === "json") {
|
|
9
|
+
return normalized;
|
|
10
|
+
}
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function resolveOutputFormat(explicit?: OutputFormat): OutputFormat {
|
|
15
|
+
const env = process.env;
|
|
16
|
+
|
|
17
|
+
const direct = normalizeFormat(explicit) ?? normalizeFormat(env.MANDU_OUTPUT);
|
|
18
|
+
if (direct) return direct;
|
|
19
|
+
|
|
20
|
+
const mode = getOutputMode();
|
|
21
|
+
return mode === "json" ? "json" : "console";
|
|
22
|
+
}
|