@emberkit/cli 0.6.5 → 0.6.8

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.
@@ -1,23 +1,205 @@
1
- import { spawn } from "child_process";
2
- import { platform } from "os";
3
- export async function dev(args) {
4
- console.log("🔥 Starting EmberKit dev server...\n");
5
- const isWindows = platform() === "win32";
6
- const vite = spawn("vite", args, {
7
- stdio: "inherit",
8
- shell: isWindows,
9
- });
10
- return new Promise((resolve, reject) => {
11
- vite.on("exit", (code) => {
12
- if (code === 0) {
13
- resolve();
1
+ import { createServer } from "vite";
2
+ import { join } from "path";
3
+ import { existsSync } from "fs";
4
+ import { pathToFileURL } from "url";
5
+ import { mergeEmberkitViteConfig } from "../utils/merge-emberkit-vite.js";
6
+ const COLORS = {
7
+ reset: "\x1b[0m",
8
+ bright: "\x1b[1m",
9
+ dim: "\x1b[2m",
10
+ orange: "\x1b[38;5;208m",
11
+ ember: "\x1b[38;5;202m",
12
+ gray: "\x1b[38;5;240m",
13
+ green: "\x1b[38;5;40m",
14
+ cyan: "\x1b[38;5;51m",
15
+ yellow: "\x1b[38;5;220m",
16
+ red: "\x1b[38;5;196m",
17
+ };
18
+ const EMBERKIT_ASCII = `
19
+ ╔═══════════════════════════════════════╗
20
+ ║ ║
21
+ ║ ${COLORS.orange}◆${COLORS.reset} E M B E R K I T ${COLORS.orange}◆${COLORS.reset} ║
22
+ ║ ║
23
+ ║ ░▒▓█ DEV SERVER █▓▒░ ║
24
+ ║ ║
25
+ ╚═══════════════════════════════════════╝
26
+ `;
27
+ function log(level, message, meta) {
28
+ const timestamp = new Date().toLocaleTimeString("en-US", { hour12: false });
29
+ const prefix = `${COLORS.gray}[${timestamp}]${COLORS.reset}`;
30
+ const levelColors = {
31
+ info: COLORS.cyan,
32
+ warn: COLORS.yellow,
33
+ error: COLORS.red,
34
+ success: COLORS.green,
35
+ debug: COLORS.gray,
36
+ };
37
+ const levelLabel = `${levelColors[level]}${level.toUpperCase().padEnd(7)}${COLORS.reset}`;
38
+ const emberTag = `${COLORS.ember}[emberkit]${COLORS.reset}`;
39
+ let output = `${prefix} ${levelLabel} ${emberTag} ${message}`;
40
+ if (meta && Object.keys(meta).length > 0) {
41
+ const metaStr = Object.entries(meta)
42
+ .map(([k, v]) => `${COLORS.gray}${k}=${COLORS.reset}${COLORS.dim}${v}${COLORS.reset}`)
43
+ .join(" ");
44
+ output += ` ${metaStr}`;
45
+ }
46
+ console.log(output);
47
+ }
48
+ async function loadEmberKitConfig(root) {
49
+ const configPath = join(root, "emberkit.config.ts");
50
+ const configPathJs = join(root, "emberkit.config.js");
51
+ const finalPath = existsSync(configPath) ? configPath : existsSync(configPathJs) ? configPathJs : null;
52
+ if (!finalPath) {
53
+ return null;
54
+ }
55
+ try {
56
+ const configUrl = pathToFileURL(finalPath).href;
57
+ const mod = await import(configUrl);
58
+ return mod.default || mod;
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ }
64
+ async function loadViteConfig(root) {
65
+ const viteConfigPath = join(root, "vite.config.ts");
66
+ const viteConfigPathJs = join(root, "vite.config.js");
67
+ const finalPath = existsSync(viteConfigPath) ? viteConfigPath : existsSync(viteConfigPathJs) ? viteConfigPathJs : null;
68
+ if (!finalPath) {
69
+ return null;
70
+ }
71
+ try {
72
+ const configUrl = pathToFileURL(finalPath).href;
73
+ const mod = await import(configUrl);
74
+ const config = mod.default || mod;
75
+ return typeof config === "function" ? config({ mode: "development", command: "serve" }) : config;
76
+ }
77
+ catch {
78
+ return null;
79
+ }
80
+ }
81
+ export async function dev(_args) {
82
+ const root = process.cwd();
83
+ console.clear();
84
+ console.log(`${COLORS.orange}${EMBERKIT_ASCII}${COLORS.reset}`);
85
+ log("info", "Initializing development server...");
86
+ const emberkitConfig = await loadEmberKitConfig(root);
87
+ const viteFileConfig = await loadViteConfig(root);
88
+ const viteConfig = mergeEmberkitViteConfig(emberkitConfig, viteFileConfig);
89
+ if (emberkitConfig) {
90
+ log("debug", "Loaded emberkit.config", { mode: emberkitConfig.mode || "hybrid" });
91
+ }
92
+ const serverPort = emberkitConfig?.server?.port
93
+ || viteConfig?.server?.port
94
+ || 3000;
95
+ const serverHost = emberkitConfig?.server?.host
96
+ || viteConfig?.server?.host
97
+ || "localhost";
98
+ const customLogger = {
99
+ info: (msg) => {
100
+ if (!msg.includes("VITE") && !msg.includes("vite")) {
101
+ log("info", msg);
14
102
  }
15
- else {
16
- reject(new Error(`Vite exited with code ${code}`));
103
+ },
104
+ warn: (msg) => log("warn", msg),
105
+ error: (msg) => log("error", msg),
106
+ warnOnce: (msg) => log("warn", msg),
107
+ clearScreen: () => { },
108
+ hasWarned: false,
109
+ hasErrorLogged: (_error) => false,
110
+ };
111
+ try {
112
+ const mergedConfig = {
113
+ ...viteConfig,
114
+ root,
115
+ customLogger,
116
+ clearScreen: false,
117
+ server: {
118
+ ...(viteConfig?.server || {}),
119
+ port: serverPort,
120
+ host: serverHost,
121
+ },
122
+ logLevel: "silent",
123
+ };
124
+ const server = await createServer(mergedConfig);
125
+ await server.listen();
126
+ const info = server.config.server;
127
+ const protocol = info.https ? "https" : "http";
128
+ const host = typeof info.host === "string" ? info.host : "localhost";
129
+ const port = info.port || serverPort;
130
+ const address = `${protocol}://${host}:${port}`;
131
+ console.log("");
132
+ log("success", "Server is running!");
133
+ console.log("");
134
+ console.log(` ${COLORS.bright}${COLORS.orange}➜${COLORS.reset} ${COLORS.bright}Local:${COLORS.reset} ${COLORS.cyan}${address}${COLORS.reset}`);
135
+ if (host === "0.0.0.0" || host === "::") {
136
+ const os = await import("os");
137
+ const interfaces = os.networkInterfaces();
138
+ for (const name of Object.keys(interfaces)) {
139
+ for (const iface of interfaces[name] || []) {
140
+ if (iface.family === "IPv4" && !iface.internal) {
141
+ console.log(` ${COLORS.bright}${COLORS.orange}➜${COLORS.reset} ${COLORS.bright}Network:${COLORS.reset} ${COLORS.cyan}${protocol}://${iface.address}:${port}${COLORS.reset}`);
142
+ }
143
+ }
17
144
  }
145
+ }
146
+ const mode = emberkitConfig?.mode || "hybrid";
147
+ console.log("");
148
+ console.log(` ${COLORS.gray}Mode:${COLORS.reset} ${COLORS.ember}${mode}${COLORS.reset}`);
149
+ console.log(` ${COLORS.gray}Press${COLORS.reset} ${COLORS.dim}h + enter${COLORS.reset} ${COLORS.gray}to show help${COLORS.reset}`);
150
+ console.log("");
151
+ server.watcher.on("change", (file) => {
152
+ const relativePath = file.replace(root, "").replace(/^\//, "");
153
+ log("info", `File changed: ${COLORS.cyan}${relativePath}${COLORS.reset}`);
18
154
  });
19
- vite.on("error", (error) => {
20
- reject(error);
155
+ process.stdin.setRawMode?.(false);
156
+ process.stdin.resume();
157
+ process.stdin.setEncoding("utf8");
158
+ let inputBuffer = "";
159
+ process.stdin.on("data", async (key) => {
160
+ if (key === "\u0003") {
161
+ log("info", "Shutting down...");
162
+ await server.close();
163
+ process.exit(0);
164
+ }
165
+ if (key === "\r" || key === "\n") {
166
+ const cmd = inputBuffer.trim().toLowerCase();
167
+ inputBuffer = "";
168
+ if (cmd === "h" || cmd === "help") {
169
+ console.log("");
170
+ console.log(` ${COLORS.bright}${COLORS.orange}EmberKit Dev Server Commands${COLORS.reset}`);
171
+ console.log(` ${COLORS.gray}─────────────────────────────${COLORS.reset}`);
172
+ console.log(` ${COLORS.cyan}r${COLORS.reset} Restart server`);
173
+ console.log(` ${COLORS.cyan}u${COLORS.reset} Show server URL`);
174
+ console.log(` ${COLORS.cyan}c${COLORS.reset} Clear console`);
175
+ console.log(` ${COLORS.cyan}q${COLORS.reset} Quit`);
176
+ console.log("");
177
+ }
178
+ else if (cmd === "r" || cmd === "restart") {
179
+ log("info", "Restarting server...");
180
+ await server.restart();
181
+ log("success", "Server restarted!");
182
+ }
183
+ else if (cmd === "u" || cmd === "url") {
184
+ console.log(` ${COLORS.bright}${COLORS.orange}➜${COLORS.reset} ${COLORS.cyan}${address}${COLORS.reset}`);
185
+ }
186
+ else if (cmd === "c" || cmd === "clear") {
187
+ console.clear();
188
+ console.log(`${COLORS.orange}${EMBERKIT_ASCII}${COLORS.reset}`);
189
+ }
190
+ else if (cmd === "q" || cmd === "quit") {
191
+ log("info", "Shutting down...");
192
+ await server.close();
193
+ process.exit(0);
194
+ }
195
+ }
196
+ else {
197
+ inputBuffer += key;
198
+ }
21
199
  });
22
- });
200
+ }
201
+ catch (error) {
202
+ log("error", `Failed to start server: ${error instanceof Error ? error.message : String(error)}`);
203
+ process.exit(1);
204
+ }
23
205
  }
@@ -1,23 +1,180 @@
1
- import { spawn } from "child_process";
2
- import { platform } from "os";
3
- export async function preview(args) {
4
- console.log("👀 Previewing production build...\n");
5
- const isWindows = platform() === "win32";
6
- const vite = spawn("vite", ["preview", ...args], {
7
- stdio: "inherit",
8
- shell: isWindows,
9
- });
10
- return new Promise((resolve, reject) => {
11
- vite.on("exit", (code) => {
12
- if (code === 0) {
13
- resolve();
1
+ import { createServer as createHttpServer } from "http";
2
+ import { join, extname } from "path";
3
+ import { existsSync, readFileSync, statSync } from "fs";
4
+ import { pathToFileURL } from "url";
5
+ const COLORS = {
6
+ reset: "\x1b[0m",
7
+ bright: "\x1b[1m",
8
+ dim: "\x1b[2m",
9
+ orange: "\x1b[38;5;208m",
10
+ ember: "\x1b[38;5;202m",
11
+ gray: "\x1b[38;5;240m",
12
+ green: "\x1b[38;5;40m",
13
+ cyan: "\x1b[38;5;51m",
14
+ yellow: "\x1b[38;5;220m",
15
+ red: "\x1b[38;5;196m",
16
+ };
17
+ const EMBERKIT_ASCII = `
18
+ ╔═══════════════════════════════════════╗
19
+ ║ ║
20
+ ║ ${COLORS.orange}◆${COLORS.reset} E M B E R K I T ${COLORS.orange}◆${COLORS.reset} ║
21
+ ║ ║
22
+ ║ ░▒▓█ PREVIEW SERVER █▓▒░ ║
23
+ ║ ║
24
+ ╚═══════════════════════════════════════╝
25
+ `;
26
+ function log(level, message) {
27
+ const timestamp = new Date().toLocaleTimeString("en-US", { hour12: false });
28
+ const prefix = `${COLORS.gray}[${timestamp}]${COLORS.reset}`;
29
+ const levelColors = {
30
+ info: COLORS.cyan,
31
+ warn: COLORS.yellow,
32
+ error: COLORS.red,
33
+ success: COLORS.green,
34
+ request: COLORS.dim,
35
+ };
36
+ const levelLabel = `${levelColors[level]}${level.toUpperCase().padEnd(7)}${COLORS.reset}`;
37
+ const emberTag = `${COLORS.ember}[emberkit]${COLORS.reset}`;
38
+ console.log(`${prefix} ${levelLabel} ${emberTag} ${message}`);
39
+ }
40
+ const MIME_TYPES = {
41
+ ".html": "text/html",
42
+ ".js": "application/javascript",
43
+ ".mjs": "application/javascript",
44
+ ".css": "text/css",
45
+ ".json": "application/json",
46
+ ".png": "image/png",
47
+ ".jpg": "image/jpeg",
48
+ ".jpeg": "image/jpeg",
49
+ ".gif": "image/gif",
50
+ ".svg": "image/svg+xml",
51
+ ".ico": "image/x-icon",
52
+ ".woff": "font/woff",
53
+ ".woff2": "font/woff2",
54
+ ".ttf": "font/ttf",
55
+ ".eot": "application/vnd.ms-fontobject",
56
+ ".otf": "font/otf",
57
+ ".webp": "image/webp",
58
+ ".webm": "video/webm",
59
+ ".mp4": "video/mp4",
60
+ ".txt": "text/plain",
61
+ ".xml": "application/xml",
62
+ };
63
+ async function loadEmberKitConfig(root) {
64
+ const configPaths = [
65
+ join(root, "emberkit.config.ts"),
66
+ join(root, "emberkit.config.js"),
67
+ join(root, "emberkit.config.mjs"),
68
+ ];
69
+ for (const configPath of configPaths) {
70
+ if (existsSync(configPath)) {
71
+ try {
72
+ const configUrl = pathToFileURL(configPath).href;
73
+ const mod = await import(configUrl);
74
+ return mod.default || mod;
14
75
  }
15
- else {
16
- reject(new Error(`Preview exited with code ${code}`));
76
+ catch {
77
+ continue;
78
+ }
79
+ }
80
+ }
81
+ return null;
82
+ }
83
+ export async function preview(_args) {
84
+ const root = process.cwd();
85
+ console.clear();
86
+ console.log(`${COLORS.orange}${EMBERKIT_ASCII}${COLORS.reset}`);
87
+ const emberkitConfig = await loadEmberKitConfig(root);
88
+ const mode = emberkitConfig?.mode || "hybrid";
89
+ const outDir = emberkitConfig?.build?.outDir || "dist";
90
+ const distPath = join(root, outDir);
91
+ if (!existsSync(distPath)) {
92
+ log("error", `Build directory not found: ${distPath}`);
93
+ log("info", "Run 'emberkit build' first to create a production build.");
94
+ process.exit(1);
95
+ }
96
+ const manifestPath = join(distPath, "ssr-manifest.json");
97
+ let manifest = null;
98
+ if (existsSync(manifestPath)) {
99
+ manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
100
+ log("info", `Loaded SSR manifest (mode: ${manifest?.mode || mode})`);
101
+ }
102
+ const serverEntryPath = join(distPath, "server", "entry-server.js");
103
+ let serverModule = null;
104
+ if ((mode === "ssr" || mode === "hybrid") && existsSync(serverEntryPath)) {
105
+ try {
106
+ const serverUrl = pathToFileURL(serverEntryPath).href;
107
+ serverModule = await import(serverUrl);
108
+ log("info", "Loaded SSR server module");
109
+ }
110
+ catch (e) {
111
+ log("warn", `Could not load SSR module: ${e}`);
112
+ }
113
+ }
114
+ const port = emberkitConfig?.server?.port || 4173;
115
+ const host = emberkitConfig?.server?.host || "localhost";
116
+ const server = createHttpServer(async (req, res) => {
117
+ const url = req.url ?? "/";
118
+ const urlObj = new URL(url, `http://${req.headers.host || "localhost"}`);
119
+ const pathname = urlObj.pathname;
120
+ const serveStaticFile = (filePath) => {
121
+ if (existsSync(filePath) && statSync(filePath).isFile()) {
122
+ const ext = extname(filePath);
123
+ const mimeType = MIME_TYPES[ext] || "application/octet-stream";
124
+ res.setHeader("Content-Type", mimeType);
125
+ res.setHeader("Cache-Control", "public, max-age=31536000");
126
+ res.end(readFileSync(filePath));
127
+ return true;
17
128
  }
18
- });
19
- vite.on("error", (error) => {
20
- reject(error);
21
- });
129
+ return false;
130
+ };
131
+ const staticPath = join(distPath, pathname);
132
+ if (serveStaticFile(staticPath)) {
133
+ log("request", `${COLORS.green}200${COLORS.reset} ${pathname} ${COLORS.dim}(static)${COLORS.reset}`);
134
+ return;
135
+ }
136
+ if (pathname !== "/" && !pathname.includes(".")) {
137
+ const htmlPath = join(distPath, pathname, "index.html");
138
+ if (serveStaticFile(htmlPath)) {
139
+ log("request", `${COLORS.green}200${COLORS.reset} ${pathname} ${COLORS.dim}(prerendered)${COLORS.reset}`);
140
+ return;
141
+ }
142
+ }
143
+ if ((mode === "ssr" || mode === "hybrid") && serverModule) {
144
+ if (req.headers.accept?.includes("text/html")) {
145
+ try {
146
+ const html = await serverModule.render(url);
147
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
148
+ res.end(html);
149
+ log("request", `${COLORS.green}200${COLORS.reset} ${pathname} ${COLORS.dim}(ssr)${COLORS.reset}`);
150
+ return;
151
+ }
152
+ catch (e) {
153
+ log("error", `SSR render failed: ${e}`);
154
+ }
155
+ }
156
+ }
157
+ const indexPath = join(distPath, "index.html");
158
+ if (existsSync(indexPath)) {
159
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
160
+ res.end(readFileSync(indexPath));
161
+ log("request", `${COLORS.green}200${COLORS.reset} ${pathname} ${COLORS.dim}(fallback)${COLORS.reset}`);
162
+ return;
163
+ }
164
+ res.statusCode = 404;
165
+ res.setHeader("Content-Type", "text/html");
166
+ res.end("<h1>404 - Not Found</h1>");
167
+ log("request", `${COLORS.yellow}404${COLORS.reset} ${pathname}`);
168
+ });
169
+ server.listen(port, host, () => {
170
+ log("success", "Preview server is running!");
171
+ console.log("");
172
+ console.log(` ${COLORS.bright}${COLORS.orange}➜${COLORS.reset} ${COLORS.bright}Local:${COLORS.reset} ${COLORS.cyan}http://${host}:${port}${COLORS.reset}`);
173
+ console.log("");
174
+ console.log(` ${COLORS.gray}Mode:${COLORS.reset} ${COLORS.ember}${manifest?.mode || mode}${COLORS.reset}`);
175
+ console.log(` ${COLORS.gray}Dir:${COLORS.reset} ${COLORS.dim}${outDir}${COLORS.reset}`);
176
+ console.log(` ${COLORS.gray}Press${COLORS.reset} ${COLORS.dim}Ctrl+C${COLORS.reset} ${COLORS.gray}to stop${COLORS.reset}`);
177
+ console.log("");
22
178
  });
179
+ return new Promise(() => { });
23
180
  }
@@ -0,0 +1,242 @@
1
+ import { createServer as createHttpServer } from "http";
2
+ import { join, extname } from "path";
3
+ import { existsSync, readFileSync, statSync } from "fs";
4
+ import { pathToFileURL } from "url";
5
+ const COLORS = {
6
+ reset: "\x1b[0m",
7
+ bright: "\x1b[1m",
8
+ dim: "\x1b[2m",
9
+ orange: "\x1b[38;5;208m",
10
+ ember: "\x1b[38;5;202m",
11
+ gray: "\x1b[38;5;240m",
12
+ green: "\x1b[38;5;40m",
13
+ cyan: "\x1b[38;5;51m",
14
+ yellow: "\x1b[38;5;220m",
15
+ red: "\x1b[38;5;196m",
16
+ };
17
+ const EMBERKIT_ASCII = `
18
+ ╔═══════════════════════════════════════╗
19
+ ║ ║
20
+ ║ ${COLORS.orange}◆${COLORS.reset} E M B E R K I T ${COLORS.orange}◆${COLORS.reset} ║
21
+ ║ ║
22
+ ║ ░▒▓█ PRODUCTION SERVER █▓▒░ ║
23
+ ║ ║
24
+ ╚═══════════════════════════════════════╝
25
+ `;
26
+ function log(level, message) {
27
+ const timestamp = new Date().toLocaleTimeString("en-US", { hour12: false });
28
+ const prefix = `${COLORS.gray}[${timestamp}]${COLORS.reset}`;
29
+ const levelColors = {
30
+ info: COLORS.cyan,
31
+ warn: COLORS.yellow,
32
+ error: COLORS.red,
33
+ success: COLORS.green,
34
+ request: COLORS.dim,
35
+ };
36
+ const levelLabel = `${levelColors[level]}${level.toUpperCase().padEnd(7)}${COLORS.reset}`;
37
+ const emberTag = `${COLORS.ember}[emberkit]${COLORS.reset}`;
38
+ console.log(`${prefix} ${levelLabel} ${emberTag} ${message}`);
39
+ }
40
+ const MIME_TYPES = {
41
+ ".html": "text/html",
42
+ ".js": "application/javascript",
43
+ ".mjs": "application/javascript",
44
+ ".css": "text/css",
45
+ ".json": "application/json",
46
+ ".png": "image/png",
47
+ ".jpg": "image/jpeg",
48
+ ".jpeg": "image/jpeg",
49
+ ".gif": "image/gif",
50
+ ".svg": "image/svg+xml",
51
+ ".ico": "image/x-icon",
52
+ ".woff": "font/woff",
53
+ ".woff2": "font/woff2",
54
+ ".ttf": "font/ttf",
55
+ ".eot": "application/vnd.ms-fontobject",
56
+ ".otf": "font/otf",
57
+ ".webp": "image/webp",
58
+ ".webm": "video/webm",
59
+ ".mp4": "video/mp4",
60
+ ".txt": "text/plain",
61
+ ".xml": "application/xml",
62
+ ".map": "application/json",
63
+ };
64
+ function routeToRegex(routePath) {
65
+ const paramNames = [];
66
+ const regexStr = routePath
67
+ .replace(/:([^/]+)\*/g, (_, name) => {
68
+ paramNames.push(name);
69
+ return "(.*)";
70
+ })
71
+ .replace(/:([^/]+)/g, (_, name) => {
72
+ paramNames.push(name);
73
+ return "([^/]+)";
74
+ });
75
+ return { regex: new RegExp("^" + regexStr + "$"), paramNames };
76
+ }
77
+ function matchRoute(routes, pathname) {
78
+ const normalizedPath = pathname.replace(/\/+$/, "") || "/";
79
+ const sortedRoutes = [...routes].sort((a, b) => {
80
+ const aScore = a.path.includes(":") ? 0 : 1;
81
+ const bScore = b.path.includes(":") ? 0 : 1;
82
+ return bScore - aScore;
83
+ });
84
+ for (const route of sortedRoutes) {
85
+ const pattern = routeToRegex(route.path);
86
+ const match = normalizedPath.match(pattern.regex);
87
+ if (match) {
88
+ const params = {};
89
+ pattern.paramNames.forEach((name, i) => {
90
+ params[name] = match[i + 1];
91
+ });
92
+ return { route, params };
93
+ }
94
+ }
95
+ return null;
96
+ }
97
+ export async function serve(args) {
98
+ const portArg = args.find((a) => a.startsWith("--port="));
99
+ const hostArg = args.find((a) => a.startsWith("--host="));
100
+ const dirArg = args.find((a) => a.startsWith("--dir="));
101
+ const root = process.cwd();
102
+ const outDir = dirArg?.split("=")[1] || "dist";
103
+ const distPath = join(root, outDir);
104
+ console.log(`${COLORS.orange}${EMBERKIT_ASCII}${COLORS.reset}`);
105
+ if (!existsSync(distPath)) {
106
+ log("error", `Build directory not found: ${distPath}`);
107
+ log("info", "Run 'emberkit build' first to create a production build.");
108
+ process.exit(1);
109
+ }
110
+ const manifestPath = join(distPath, "ssr-manifest.json");
111
+ let manifest = null;
112
+ if (existsSync(manifestPath)) {
113
+ manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
114
+ log("info", `Loaded SSR manifest (mode: ${manifest?.mode})`);
115
+ }
116
+ else {
117
+ log("warn", "No SSR manifest found, serving as static site");
118
+ }
119
+ const serverEntryPath = join(distPath, "server", "entry-server.js");
120
+ let serverModule = null;
121
+ if (manifest && (manifest.mode === "ssr" || manifest.mode === "hybrid") && existsSync(serverEntryPath)) {
122
+ try {
123
+ const serverUrl = pathToFileURL(serverEntryPath).href;
124
+ serverModule = await import(serverUrl);
125
+ log("info", "Loaded SSR server module");
126
+ }
127
+ catch (e) {
128
+ log("warn", `Could not load SSR module: ${e}`);
129
+ }
130
+ }
131
+ const port = parseInt(portArg?.split("=")[1] || "3000", 10);
132
+ const host = hostArg?.split("=")[1] || "0.0.0.0";
133
+ let requestCount = 0;
134
+ const startTime = Date.now();
135
+ const server = createHttpServer(async (req, res) => {
136
+ requestCount++;
137
+ const reqStart = Date.now();
138
+ const url = req.url ?? "/";
139
+ const urlObj = new URL(url, `http://${req.headers.host || "localhost"}`);
140
+ const pathname = urlObj.pathname;
141
+ res.setHeader("X-Powered-By", "EmberKit");
142
+ const serveStaticFile = (filePath) => {
143
+ if (existsSync(filePath) && statSync(filePath).isFile()) {
144
+ const ext = extname(filePath);
145
+ const mimeType = MIME_TYPES[ext] || "application/octet-stream";
146
+ res.setHeader("Content-Type", mimeType);
147
+ if (ext === ".html") {
148
+ res.setHeader("Cache-Control", "no-cache");
149
+ }
150
+ else if (pathname.includes("/assets/")) {
151
+ res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
152
+ }
153
+ else {
154
+ res.setHeader("Cache-Control", "public, max-age=86400");
155
+ }
156
+ res.end(readFileSync(filePath));
157
+ return true;
158
+ }
159
+ return false;
160
+ };
161
+ const staticPath = join(distPath, pathname);
162
+ if (serveStaticFile(staticPath)) {
163
+ const ms = Date.now() - reqStart;
164
+ log("request", `${COLORS.green}200${COLORS.reset} ${pathname} ${COLORS.dim}(static, ${ms}ms)${COLORS.reset}`);
165
+ return;
166
+ }
167
+ if (pathname !== "/" && !pathname.includes(".")) {
168
+ const htmlPath = join(distPath, pathname, "index.html");
169
+ if (serveStaticFile(htmlPath)) {
170
+ const ms = Date.now() - reqStart;
171
+ log("request", `${COLORS.green}200${COLORS.reset} ${pathname} ${COLORS.dim}(prerendered, ${ms}ms)${COLORS.reset}`);
172
+ return;
173
+ }
174
+ }
175
+ if (manifest && (manifest.mode === "ssr" || manifest.mode === "hybrid") && serverModule) {
176
+ if (req.headers.accept?.includes("text/html")) {
177
+ const routeMatch = matchRoute(manifest.routes, pathname);
178
+ if (routeMatch && (manifest.mode === "ssr" || !routeMatch.route.isStatic)) {
179
+ try {
180
+ if (serverModule.render) {
181
+ const html = await serverModule.render(url, routeMatch.params);
182
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
183
+ res.setHeader("Cache-Control", "no-cache");
184
+ res.end(typeof html === "string" ? html : html?.html ?? "");
185
+ const ms = Date.now() - reqStart;
186
+ log("request", `${COLORS.green}200${COLORS.reset} ${pathname} ${COLORS.dim}(ssr, ${ms}ms)${COLORS.reset}`);
187
+ return;
188
+ }
189
+ }
190
+ catch (e) {
191
+ log("error", `SSR render failed for ${pathname}: ${e}`);
192
+ }
193
+ }
194
+ }
195
+ }
196
+ const indexPath = join(distPath, "index.html");
197
+ if (existsSync(indexPath)) {
198
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
199
+ res.setHeader("Cache-Control", "no-cache");
200
+ res.end(readFileSync(indexPath));
201
+ const ms = Date.now() - reqStart;
202
+ log("request", `${COLORS.green}200${COLORS.reset} ${pathname} ${COLORS.dim}(spa fallback, ${ms}ms)${COLORS.reset}`);
203
+ return;
204
+ }
205
+ res.statusCode = 404;
206
+ res.setHeader("Content-Type", "text/html");
207
+ res.end("<h1>404 - Not Found</h1>");
208
+ const ms = Date.now() - reqStart;
209
+ log("request", `${COLORS.yellow}404${COLORS.reset} ${pathname} ${COLORS.dim}(${ms}ms)${COLORS.reset}`);
210
+ });
211
+ server.listen(port, host, () => {
212
+ log("success", "Production server is running!");
213
+ console.log("");
214
+ console.log(` ${COLORS.bright}${COLORS.orange}➜${COLORS.reset} ${COLORS.bright}Local:${COLORS.reset} ${COLORS.cyan}http://localhost:${port}${COLORS.reset}`);
215
+ if (host === "0.0.0.0" || host === "::") {
216
+ import("os").then((os) => {
217
+ const interfaces = os.networkInterfaces();
218
+ for (const name of Object.keys(interfaces)) {
219
+ for (const iface of interfaces[name] || []) {
220
+ if (iface.family === "IPv4" && !iface.internal) {
221
+ console.log(` ${COLORS.bright}${COLORS.orange}➜${COLORS.reset} ${COLORS.bright}Network:${COLORS.reset} ${COLORS.cyan}http://${iface.address}:${port}${COLORS.reset}`);
222
+ }
223
+ }
224
+ }
225
+ });
226
+ }
227
+ console.log("");
228
+ console.log(` ${COLORS.gray}Mode:${COLORS.reset} ${COLORS.ember}${manifest?.mode || "static"}${COLORS.reset}`);
229
+ console.log(` ${COLORS.gray}Dir:${COLORS.reset} ${COLORS.dim}${outDir}${COLORS.reset}`);
230
+ console.log(` ${COLORS.gray}Routes:${COLORS.reset} ${COLORS.dim}${manifest?.routes.length || 0}${COLORS.reset}`);
231
+ console.log(` ${COLORS.gray}Press${COLORS.reset} ${COLORS.dim}Ctrl+C${COLORS.reset} ${COLORS.gray}to stop${COLORS.reset}`);
232
+ console.log("");
233
+ });
234
+ process.on("SIGINT", () => {
235
+ const uptime = Math.round((Date.now() - startTime) / 1000);
236
+ console.log("");
237
+ log("info", `Shutting down... (served ${requestCount} requests in ${uptime}s)`);
238
+ server.close();
239
+ process.exit(0);
240
+ });
241
+ return new Promise(() => { });
242
+ }