@emberkit/cli 0.6.4 → 0.6.7

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/dist/cli.js CHANGED
@@ -3,6 +3,7 @@ import { getCliPackageVersion } from "./cli-package-version.js";
3
3
  import { dev } from "./commands/dev.js";
4
4
  import { build } from "./commands/build.js";
5
5
  import { preview } from "./commands/preview.js";
6
+ import { serve } from "./commands/serve.js";
6
7
  import { create, TEMPLATES } from "./commands/create.js";
7
8
  import { generate } from "./utils/generator.js";
8
9
  import { toKebabCase } from "./templates/index.js";
@@ -22,6 +23,10 @@ export async function runCLI(args) {
22
23
  case "preview":
23
24
  await preview(restArgs);
24
25
  break;
26
+ case "serve":
27
+ case "start":
28
+ await serve(restArgs);
29
+ break;
25
30
  case "create":
26
31
  await handleCreate(restArgs);
27
32
  break;
@@ -51,8 +56,9 @@ Usage: emberkit <command> [options]
51
56
  Commands:
52
57
  create [name] Create a new EmberKit project
53
58
  dev Start development server
54
- build Build for production
55
- preview Preview production build
59
+ build Build for production (SSR/hybrid/static/SPA)
60
+ preview Preview production build locally
61
+ serve Start production server (SSR/hybrid)
56
62
  generate <type> <name> Generate a file from a template
57
63
 
58
64
  Options:
@@ -1,24 +1,495 @@
1
- import { spawn } from "child_process";
2
- import { platform } from "os";
3
- export async function build(args) {
4
- console.log("🔨 Building for production...\n");
5
- const isWindows = platform() === "win32";
6
- const vite = spawn("vite", ["build", ...args], {
7
- stdio: "inherit",
8
- shell: isWindows,
1
+ import { build as viteBuild } from "vite";
2
+ import { join } from "path";
3
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } 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
+ function log(level, message) {
18
+ const timestamp = new Date().toLocaleTimeString("en-US", { hour12: false });
19
+ const prefix = `${COLORS.gray}[${timestamp}]${COLORS.reset}`;
20
+ const levelColors = {
21
+ info: COLORS.cyan,
22
+ warn: COLORS.yellow,
23
+ error: COLORS.red,
24
+ success: COLORS.green,
25
+ };
26
+ const levelLabel = `${levelColors[level]}${level.toUpperCase().padEnd(7)}${COLORS.reset}`;
27
+ const emberTag = `${COLORS.ember}[emberkit]${COLORS.reset}`;
28
+ console.log(`${prefix} ${levelLabel} ${emberTag} ${message}`);
29
+ }
30
+ async function loadEmberKitConfig(root) {
31
+ const configPaths = [
32
+ join(root, "emberkit.config.ts"),
33
+ join(root, "emberkit.config.js"),
34
+ join(root, "emberkit.config.mjs"),
35
+ ];
36
+ for (const configPath of configPaths) {
37
+ if (existsSync(configPath)) {
38
+ try {
39
+ const configUrl = pathToFileURL(configPath).href;
40
+ const mod = await import(configUrl);
41
+ return mod.default || mod;
42
+ }
43
+ catch {
44
+ continue;
45
+ }
46
+ }
47
+ }
48
+ return null;
49
+ }
50
+ async function loadViteConfig(root) {
51
+ const viteConfigPaths = [
52
+ join(root, "vite.config.ts"),
53
+ join(root, "vite.config.js"),
54
+ ];
55
+ for (const configPath of viteConfigPaths) {
56
+ if (existsSync(configPath)) {
57
+ try {
58
+ const configUrl = pathToFileURL(configPath).href;
59
+ const mod = await import(configUrl);
60
+ const config = mod.default || mod;
61
+ return typeof config === "function"
62
+ ? config({ mode: "production", command: "build" })
63
+ : config;
64
+ }
65
+ catch {
66
+ continue;
67
+ }
68
+ }
69
+ }
70
+ return null;
71
+ }
72
+ export async function build(_args) {
73
+ const root = process.cwd();
74
+ console.log(`\n${COLORS.orange}🔥 EmberKit Build${COLORS.reset}\n`);
75
+ const emberkitConfig = await loadEmberKitConfig(root);
76
+ const viteConfig = await loadViteConfig(root);
77
+ const mode = emberkitConfig?.mode || "hybrid";
78
+ const outDir = emberkitConfig?.build?.outDir || "dist";
79
+ log("info", `Build mode: ${COLORS.ember}${mode}${COLORS.reset}`);
80
+ log("info", `Output directory: ${COLORS.cyan}${outDir}${COLORS.reset}`);
81
+ const customLogger = {
82
+ info: () => { },
83
+ warn: (msg) => log("warn", msg),
84
+ error: (msg) => log("error", msg),
85
+ warnOnce: () => { },
86
+ clearScreen: () => { },
87
+ hasWarned: false,
88
+ hasErrorLogged: () => false,
89
+ };
90
+ try {
91
+ if (mode === "spa") {
92
+ log("info", "Building SPA (client-only)...");
93
+ await buildClient(root, outDir, viteConfig, customLogger);
94
+ log("success", "SPA build complete!");
95
+ }
96
+ else if (mode === "ssr" || mode === "hybrid") {
97
+ log("info", "Building client bundle...");
98
+ await buildClient(root, outDir, viteConfig, customLogger);
99
+ log("info", "Building SSR bundle...");
100
+ await buildSSR(root, outDir, viteConfig, customLogger);
101
+ log("info", "Generating SSR manifest...");
102
+ await generateManifest(root, outDir, mode);
103
+ if (mode === "hybrid") {
104
+ log("info", "Pre-rendering static routes...");
105
+ await prerenderStaticRoutes(root, outDir);
106
+ }
107
+ log("success", `${mode.toUpperCase()} build complete!`);
108
+ }
109
+ else if (mode === "static") {
110
+ log("info", "Building static site...");
111
+ await buildClient(root, outDir, viteConfig, customLogger);
112
+ log("info", "Building SSR bundle for pre-rendering...");
113
+ await buildSSR(root, outDir, viteConfig, customLogger);
114
+ log("info", "Generating manifest...");
115
+ await generateManifest(root, outDir, mode);
116
+ log("info", "Pre-rendering all routes...");
117
+ await prerenderStaticRoutes(root, outDir, true);
118
+ log("success", "Static build complete!");
119
+ }
120
+ console.log(`\n${COLORS.green}✨ Build finished successfully!${COLORS.reset}`);
121
+ console.log(`${COLORS.gray} Output: ${COLORS.cyan}./${outDir}${COLORS.reset}\n`);
122
+ }
123
+ catch (error) {
124
+ log("error", `Build failed: ${error instanceof Error ? error.message : String(error)}`);
125
+ process.exit(1);
126
+ }
127
+ }
128
+ async function buildClient(root, outDir, viteConfig, customLogger) {
129
+ const clientConfig = {
130
+ ...viteConfig,
131
+ root,
132
+ customLogger,
133
+ logLevel: "silent",
134
+ build: {
135
+ ...(viteConfig?.build || {}),
136
+ outDir,
137
+ emptyOutDir: true,
138
+ rollupOptions: {
139
+ ...(viteConfig?.build?.rollupOptions || {}),
140
+ output: {
141
+ manualChunks: undefined,
142
+ },
143
+ },
144
+ },
145
+ };
146
+ await viteBuild(clientConfig);
147
+ }
148
+ function getServerEntryShim() {
149
+ return `import { routes } from 'virtual:emberkit-routes';
150
+ import { createElement } from '@emberkit/core';
151
+ import { readFileSync } from 'node:fs';
152
+ import { join, dirname } from 'node:path';
153
+ import { fileURLToPath } from 'node:url';
154
+
155
+ const __dirname = dirname(fileURLToPath(import.meta.url));
156
+
157
+ const routeToRegex = (routePath) => {
158
+ const paramNames = [];
159
+ const regexStr = routePath
160
+ .replace(/:([^/]+)\\*/g, (_, name) => {
161
+ paramNames.push(name);
162
+ return '(.*)';
163
+ })
164
+ .replace(/:([^/]+)/g, (_, name) => {
165
+ paramNames.push(name);
166
+ return '([^/]+)';
9
167
  });
10
- return new Promise((resolve, reject) => {
11
- vite.on("exit", (code) => {
12
- if (code === 0) {
13
- console.log("\n✨ Build complete!");
14
- resolve();
168
+ return { regex: new RegExp('^' + regexStr + '$'), paramNames };
169
+ };
170
+
171
+ const matchRoute = (routeList, pathname) => {
172
+ const normalizedPath = pathname.replace(/\\/+$/, '') || '/';
173
+ const sortedRoutes = [...routeList].sort((a, b) => {
174
+ const aScore = a.path.includes(':') ? 0 : 1;
175
+ const bScore = b.path.includes(':') ? 0 : 1;
176
+ return bScore - aScore;
177
+ });
178
+ for (const route of sortedRoutes) {
179
+ const pattern = routeToRegex(route.path);
180
+ const match = normalizedPath.match(pattern.regex);
181
+ if (match) {
182
+ const params = {};
183
+ pattern.paramNames.forEach((name, i) => { params[name] = match[i + 1]; });
184
+ return { route, params };
185
+ }
186
+ }
187
+ return null;
188
+ };
189
+
190
+ const escapeHtml = (str) => {
191
+ if (typeof str !== 'string') return str;
192
+ return str
193
+ .replace(/&/g, '&amp;')
194
+ .replace(/</g, '&lt;')
195
+ .replace(/>/g, '&gt;')
196
+ .replace(/"/g, '&quot;')
197
+ .replace(/'/g, '&#039;');
198
+ };
199
+
200
+ const renderToString = (element) => {
201
+ if (!element && element !== 0) return '';
202
+ if (typeof element === 'string') return escapeHtml(element);
203
+ if (typeof element === 'number') return String(element);
204
+ if (Array.isArray(element)) return element.map(renderToString).join('');
205
+
206
+ if (typeof element !== 'object' || !element.type) return '';
207
+
208
+ let { type, props } = element;
209
+ props = props || {};
210
+
211
+ let depth = 0;
212
+ while (typeof type === 'function' && depth < 50) {
213
+ depth++;
214
+ try {
215
+ const result = type(props);
216
+ if (result && typeof result === 'object' && result.type) {
217
+ type = result.type;
218
+ props = result.props || {};
219
+ } else if (typeof result === 'string' || typeof result === 'number') {
220
+ return typeof result === 'string' ? escapeHtml(result) : String(result);
221
+ } else if (Array.isArray(result)) {
222
+ return result.map(renderToString).join('');
223
+ } else {
224
+ return '';
225
+ }
226
+ } catch (e) {
227
+ console.error('[SSR render error]', e);
228
+ return '';
229
+ }
230
+ }
231
+
232
+ if (type === 'Fragment' || type === 'React.Fragment') {
233
+ const children = Array.isArray(props.children) ? props.children : [props.children];
234
+ return children.filter(Boolean).map(renderToString).join('');
235
+ }
236
+
237
+ const SELF_CLOSING = new Set(['area','base','br','col','embed','hr','img','input','link','meta','source','track','wbr']);
238
+
239
+ const children = Array.isArray(props.children) ? props.children : (props.children ? [props.children] : []);
240
+ let childHtml = children.filter(c => c != null).map(renderToString).join('');
241
+
242
+ if (props.dangerouslySetInnerHTML && props.dangerouslySetInnerHTML.__html) {
243
+ childHtml = props.dangerouslySetInnerHTML.__html;
244
+ }
245
+
246
+ const attrs = Object.entries(props)
247
+ .filter(([k, v]) => k !== 'children' && k !== 'key' && k !== 'dangerouslySetInnerHTML' && v != null && typeof v !== 'function')
248
+ .map(([k, v]) => {
249
+ if (k === 'className') k = 'class';
250
+ if (v === true) return ' ' + k;
251
+ if (v === false) return '';
252
+ if (k === 'style' && typeof v === 'object') {
253
+ const styleStr = Object.entries(v)
254
+ .filter(([, sv]) => sv != null)
255
+ .map(([sp, sv]) => sp.replace(/([A-Z])/g, '-$1').toLowerCase() + ': ' + sv)
256
+ .join('; ');
257
+ return ' ' + k + '="' + escapeHtml(styleStr) + '"';
258
+ }
259
+ return ' ' + k + '="' + escapeHtml(String(v)) + '"';
260
+ })
261
+ .join('');
262
+
263
+ if (SELF_CLOSING.has(type)) {
264
+ return '<' + type + attrs + '/>';
265
+ }
266
+
267
+ return '<' + type + attrs + '>' + childHtml + '</' + type + '>';
268
+ };
269
+
270
+ export async function render(url) {
271
+ const pathname = url.split('?')[0];
272
+
273
+ const match = matchRoute(routes, pathname);
274
+
275
+ let appHtml = '';
276
+ let headContent = '';
277
+
278
+ if (match) {
279
+ try {
280
+ const mod = await match.route.component();
281
+ const Component = mod.default || mod;
282
+
283
+ if (mod.metadata) {
284
+ if (mod.metadata.title) {
285
+ headContent += '<title>' + escapeHtml(mod.metadata.title) + '</title>\\n';
286
+ }
287
+ if (mod.metadata.description) {
288
+ headContent += '<meta name="description" content="' + escapeHtml(mod.metadata.description) + '">\\n';
289
+ }
290
+ }
291
+
292
+ const element = createElement(Component, { params: match.params });
293
+ appHtml = renderToString(element);
294
+ } catch (e) {
295
+ console.error('[SSR] Failed to render route:', pathname, e);
296
+ appHtml = '<div style="color: red; padding: 20px;">SSR Error: ' + escapeHtml(String(e)) + '</div>';
297
+ }
298
+ } else {
299
+ appHtml = '<div style="padding: 20px;">404 - Page not found</div>';
300
+ }
301
+
302
+ const templatePath = join(__dirname, '..', 'index.html');
303
+ let template = readFileSync(templatePath, 'utf-8');
304
+
305
+ if (template.includes('<body id="app">')) {
306
+ template = template.replace('<body id="app">', '<body id="app">' + appHtml);
307
+ } else if (template.includes('<div id="app">')) {
308
+ template = template.replace('<div id="app"></div>', '<div id="app">' + appHtml + '</div>');
309
+ } else if (template.includes('<div id="app"/>')) {
310
+ template = template.replace('<div id="app"/>', '<div id="app">' + appHtml + '</div>');
311
+ }
312
+
313
+ if (headContent && template.includes('</head>')) {
314
+ template = template.replace('</head>', headContent + '</head>');
315
+ }
316
+
317
+ return template;
318
+ }
319
+ `;
320
+ }
321
+ async function resolveSSREntry(root) {
322
+ const userEntryTs = join(root, "src", "entry-server.ts");
323
+ const userEntryTsx = join(root, "src", "entry-server.tsx");
324
+ if (existsSync(userEntryTs)) {
325
+ return userEntryTs;
326
+ }
327
+ if (existsSync(userEntryTsx)) {
328
+ return userEntryTsx;
329
+ }
330
+ const cacheDir = join(root, "node_modules", ".cache", "emberkit");
331
+ mkdirSync(cacheDir, { recursive: true });
332
+ const shimPath = join(cacheDir, "server-entry.js");
333
+ writeFileSync(shimPath, getServerEntryShim(), "utf-8");
334
+ return shimPath;
335
+ }
336
+ async function buildSSR(root, outDir, viteConfig, customLogger) {
337
+ const ssrEntry = await resolveSSREntry(root);
338
+ const ssrConfig = {
339
+ ...viteConfig,
340
+ root,
341
+ customLogger,
342
+ logLevel: "silent",
343
+ build: {
344
+ ...(viteConfig?.build || {}),
345
+ outDir: join(outDir, "server"),
346
+ emptyOutDir: true,
347
+ ssr: true,
348
+ rollupOptions: {
349
+ ...(viteConfig?.build?.rollupOptions || {}),
350
+ input: ssrEntry,
351
+ output: {
352
+ format: "esm",
353
+ entryFileNames: "entry-server.js",
354
+ },
355
+ },
356
+ },
357
+ ssr: {
358
+ noExternal: true,
359
+ },
360
+ };
361
+ await viteBuild(ssrConfig);
362
+ }
363
+ async function generateManifest(root, outDir, mode) {
364
+ const routesDir = join(root, "src", "routes");
365
+ const routes = [];
366
+ if (existsSync(routesDir)) {
367
+ const files = await scanRouteFiles(routesDir);
368
+ for (const file of files) {
369
+ const relativePath = file.replace(routesDir, "").replace(/\\/g, "/");
370
+ if (relativePath.includes("_layout") || relativePath.includes("_error") || relativePath.includes("_loading")) {
371
+ continue;
372
+ }
373
+ if (relativePath.startsWith("/_api/") || relativePath.includes("/_api/")) {
374
+ continue;
375
+ }
376
+ let routePath = relativePath
377
+ .replace(/\.(tsx|ts|jsx|js|md|mdx)$/, "")
378
+ .replace(/(^|\/)index$/, "$1")
379
+ .replace(/\[\.\.\.(\w+)\]/g, ":$1*")
380
+ .replace(/\[([^\]]+)\]/g, ":$1");
381
+ if (routePath === "" || routePath === "/") {
382
+ routePath = "/";
15
383
  }
16
384
  else {
17
- reject(new Error(`Build failed with code ${code}`));
385
+ routePath = routePath.startsWith("/") ? routePath : "/" + routePath;
18
386
  }
19
- });
20
- vite.on("error", (error) => {
21
- reject(error);
22
- });
387
+ const isStatic = !routePath.includes(":");
388
+ routes.push({
389
+ path: routePath,
390
+ file: relativePath,
391
+ isStatic,
392
+ });
393
+ }
394
+ }
395
+ routes.sort((a, b) => {
396
+ const aScore = a.path.includes(":") ? 0 : 1;
397
+ const bScore = b.path.includes(":") ? 0 : 1;
398
+ return bScore - aScore;
23
399
  });
400
+ const indexHtmlPath = join(root, outDir, "index.html");
401
+ let template = "";
402
+ if (existsSync(indexHtmlPath)) {
403
+ template = readFileSync(indexHtmlPath, "utf-8");
404
+ }
405
+ const manifest = {
406
+ mode,
407
+ routes,
408
+ clientEntry: "/src/index.tsx",
409
+ template,
410
+ buildTime: new Date().toISOString(),
411
+ };
412
+ const manifestPath = join(root, outDir, "ssr-manifest.json");
413
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
414
+ }
415
+ async function scanRouteFiles(dir) {
416
+ const { readdir, stat } = await import("fs/promises");
417
+ const files = [];
418
+ const extensions = new Set(["tsx", "ts", "jsx", "js", "md", "mdx"]);
419
+ async function walk(currentDir) {
420
+ let entries;
421
+ try {
422
+ entries = await readdir(currentDir);
423
+ }
424
+ catch {
425
+ return;
426
+ }
427
+ for (const entry of entries) {
428
+ const fullPath = join(currentDir, entry);
429
+ const fileStat = await stat(fullPath);
430
+ if (fileStat.isDirectory()) {
431
+ await walk(fullPath);
432
+ }
433
+ else {
434
+ const ext = entry.split(".").pop() ?? "";
435
+ if (extensions.has(ext)) {
436
+ files.push(fullPath);
437
+ }
438
+ }
439
+ }
440
+ }
441
+ await walk(dir);
442
+ return files;
443
+ }
444
+ async function prerenderStaticRoutes(root, outDir, prerenderAll = false) {
445
+ const manifestPath = join(root, outDir, "ssr-manifest.json");
446
+ if (!existsSync(manifestPath)) {
447
+ log("warn", "No SSR manifest found, skipping pre-rendering");
448
+ return;
449
+ }
450
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
451
+ const serverEntryPath = join(root, outDir, "server", "entry-server.js");
452
+ if (!existsSync(serverEntryPath)) {
453
+ log("warn", "Server entry not found, skipping pre-rendering");
454
+ return;
455
+ }
456
+ let serverModule;
457
+ try {
458
+ const serverUrl = pathToFileURL(serverEntryPath).href;
459
+ serverModule = await import(serverUrl);
460
+ }
461
+ catch (e) {
462
+ log("warn", `Failed to load server module: ${e}`);
463
+ return;
464
+ }
465
+ const routesToPrerender = manifest.routes.filter((route) => prerenderAll || route.isStatic);
466
+ for (const route of routesToPrerender) {
467
+ if (route.path.includes(":")) {
468
+ continue;
469
+ }
470
+ try {
471
+ let html;
472
+ if (serverModule.render) {
473
+ html = await serverModule.render(route.path);
474
+ }
475
+ else if (serverModule.default && typeof serverModule.default === "function") {
476
+ html = "<!DOCTYPE html><html><head></head><body></body></html>";
477
+ }
478
+ else {
479
+ continue;
480
+ }
481
+ const outputPath = route.path === "/"
482
+ ? join(root, outDir, "index.html")
483
+ : join(root, outDir, route.path, "index.html");
484
+ const outputDir = outputPath.replace(/\/index\.html$/, "");
485
+ if (!existsSync(outputDir)) {
486
+ mkdirSync(outputDir, { recursive: true });
487
+ }
488
+ writeFileSync(outputPath, html, "utf-8");
489
+ console.log(` ${COLORS.green}✓${COLORS.reset} ${route.path}`);
490
+ }
491
+ catch (e) {
492
+ console.log(` ${COLORS.red}✗${COLORS.reset} ${route.path} - ${e}`);
493
+ }
494
+ }
24
495
  }
@@ -1,23 +1,203 @@
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
+ const EMBERKIT_ASCII = `
6
+ ╔═══════════════════════════════════════╗
7
+ ║ ║
8
+ ║ 🔥 E M B E R K I T 🔥 ║
9
+ ║ ║
10
+ ║ ░▒▓█ DEV SERVER █▓▒░ ║
11
+ ║ ║
12
+ ╚═══════════════════════════════════════╝
13
+ `;
14
+ const COLORS = {
15
+ reset: "\x1b[0m",
16
+ bright: "\x1b[1m",
17
+ dim: "\x1b[2m",
18
+ orange: "\x1b[38;5;208m",
19
+ ember: "\x1b[38;5;202m",
20
+ gray: "\x1b[38;5;240m",
21
+ green: "\x1b[38;5;40m",
22
+ cyan: "\x1b[38;5;51m",
23
+ yellow: "\x1b[38;5;220m",
24
+ red: "\x1b[38;5;196m",
25
+ };
26
+ function log(level, message, meta) {
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
+ debug: COLORS.gray,
35
+ };
36
+ const levelLabel = `${levelColors[level]}${level.toUpperCase().padEnd(7)}${COLORS.reset}`;
37
+ const emberTag = `${COLORS.ember}[emberkit]${COLORS.reset}`;
38
+ let output = `${prefix} ${levelLabel} ${emberTag} ${message}`;
39
+ if (meta && Object.keys(meta).length > 0) {
40
+ const metaStr = Object.entries(meta)
41
+ .map(([k, v]) => `${COLORS.gray}${k}=${COLORS.reset}${COLORS.dim}${v}${COLORS.reset}`)
42
+ .join(" ");
43
+ output += ` ${metaStr}`;
44
+ }
45
+ console.log(output);
46
+ }
47
+ async function loadEmberKitConfig(root) {
48
+ const configPath = join(root, "emberkit.config.ts");
49
+ const configPathJs = join(root, "emberkit.config.js");
50
+ const finalPath = existsSync(configPath) ? configPath : existsSync(configPathJs) ? configPathJs : null;
51
+ if (!finalPath) {
52
+ return null;
53
+ }
54
+ try {
55
+ const configUrl = pathToFileURL(finalPath).href;
56
+ const mod = await import(configUrl);
57
+ return mod.default || mod;
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ }
63
+ async function loadViteConfig(root) {
64
+ const viteConfigPath = join(root, "vite.config.ts");
65
+ const viteConfigPathJs = join(root, "vite.config.js");
66
+ const finalPath = existsSync(viteConfigPath) ? viteConfigPath : existsSync(viteConfigPathJs) ? viteConfigPathJs : null;
67
+ if (!finalPath) {
68
+ return null;
69
+ }
70
+ try {
71
+ const configUrl = pathToFileURL(finalPath).href;
72
+ const mod = await import(configUrl);
73
+ const config = mod.default || mod;
74
+ return typeof config === "function" ? config({ mode: "development", command: "serve" }) : config;
75
+ }
76
+ catch {
77
+ return null;
78
+ }
79
+ }
80
+ export async function dev(_args) {
81
+ const root = process.cwd();
82
+ console.clear();
83
+ console.log(`${COLORS.orange}${EMBERKIT_ASCII}${COLORS.reset}`);
84
+ log("info", "Initializing development server...");
85
+ const emberkitConfig = await loadEmberKitConfig(root);
86
+ const viteConfig = await loadViteConfig(root);
87
+ if (emberkitConfig) {
88
+ log("debug", "Loaded emberkit.config", { mode: emberkitConfig.mode || "hybrid" });
89
+ }
90
+ const serverPort = emberkitConfig?.server?.port
91
+ || viteConfig?.server?.port
92
+ || 3000;
93
+ const serverHost = emberkitConfig?.server?.host
94
+ || viteConfig?.server?.host
95
+ || "localhost";
96
+ const customLogger = {
97
+ info: (msg) => {
98
+ if (!msg.includes("VITE") && !msg.includes("vite")) {
99
+ log("info", msg);
14
100
  }
15
- else {
16
- reject(new Error(`Vite exited with code ${code}`));
101
+ },
102
+ warn: (msg) => log("warn", msg),
103
+ error: (msg) => log("error", msg),
104
+ warnOnce: (msg) => log("warn", msg),
105
+ clearScreen: () => { },
106
+ hasWarned: false,
107
+ hasErrorLogged: (_error) => false,
108
+ };
109
+ try {
110
+ const mergedConfig = {
111
+ ...viteConfig,
112
+ root,
113
+ customLogger,
114
+ clearScreen: false,
115
+ server: {
116
+ ...(viteConfig?.server || {}),
117
+ port: serverPort,
118
+ host: serverHost,
119
+ },
120
+ logLevel: "silent",
121
+ };
122
+ const server = await createServer(mergedConfig);
123
+ await server.listen();
124
+ const info = server.config.server;
125
+ const protocol = info.https ? "https" : "http";
126
+ const host = typeof info.host === "string" ? info.host : "localhost";
127
+ const port = info.port || serverPort;
128
+ const address = `${protocol}://${host}:${port}`;
129
+ console.log("");
130
+ log("success", "Server is running!");
131
+ console.log("");
132
+ console.log(` ${COLORS.bright}${COLORS.orange}➜${COLORS.reset} ${COLORS.bright}Local:${COLORS.reset} ${COLORS.cyan}${address}${COLORS.reset}`);
133
+ if (host === "0.0.0.0" || host === "::") {
134
+ const os = await import("os");
135
+ const interfaces = os.networkInterfaces();
136
+ for (const name of Object.keys(interfaces)) {
137
+ for (const iface of interfaces[name] || []) {
138
+ if (iface.family === "IPv4" && !iface.internal) {
139
+ console.log(` ${COLORS.bright}${COLORS.orange}➜${COLORS.reset} ${COLORS.bright}Network:${COLORS.reset} ${COLORS.cyan}${protocol}://${iface.address}:${port}${COLORS.reset}`);
140
+ }
141
+ }
17
142
  }
143
+ }
144
+ const mode = emberkitConfig?.mode || "hybrid";
145
+ console.log("");
146
+ console.log(` ${COLORS.gray}Mode:${COLORS.reset} ${COLORS.ember}${mode}${COLORS.reset}`);
147
+ console.log(` ${COLORS.gray}Press${COLORS.reset} ${COLORS.dim}h + enter${COLORS.reset} ${COLORS.gray}to show help${COLORS.reset}`);
148
+ console.log("");
149
+ server.watcher.on("change", (file) => {
150
+ const relativePath = file.replace(root, "").replace(/^\//, "");
151
+ log("info", `File changed: ${COLORS.cyan}${relativePath}${COLORS.reset}`);
18
152
  });
19
- vite.on("error", (error) => {
20
- reject(error);
153
+ process.stdin.setRawMode?.(false);
154
+ process.stdin.resume();
155
+ process.stdin.setEncoding("utf8");
156
+ let inputBuffer = "";
157
+ process.stdin.on("data", async (key) => {
158
+ if (key === "\u0003") {
159
+ log("info", "Shutting down...");
160
+ await server.close();
161
+ process.exit(0);
162
+ }
163
+ if (key === "\r" || key === "\n") {
164
+ const cmd = inputBuffer.trim().toLowerCase();
165
+ inputBuffer = "";
166
+ if (cmd === "h" || cmd === "help") {
167
+ console.log("");
168
+ console.log(` ${COLORS.bright}${COLORS.orange}EmberKit Dev Server Commands${COLORS.reset}`);
169
+ console.log(` ${COLORS.gray}─────────────────────────────${COLORS.reset}`);
170
+ console.log(` ${COLORS.cyan}r${COLORS.reset} Restart server`);
171
+ console.log(` ${COLORS.cyan}u${COLORS.reset} Show server URL`);
172
+ console.log(` ${COLORS.cyan}c${COLORS.reset} Clear console`);
173
+ console.log(` ${COLORS.cyan}q${COLORS.reset} Quit`);
174
+ console.log("");
175
+ }
176
+ else if (cmd === "r" || cmd === "restart") {
177
+ log("info", "Restarting server...");
178
+ await server.restart();
179
+ log("success", "Server restarted!");
180
+ }
181
+ else if (cmd === "u" || cmd === "url") {
182
+ console.log(` ${COLORS.bright}${COLORS.orange}➜${COLORS.reset} ${COLORS.cyan}${address}${COLORS.reset}`);
183
+ }
184
+ else if (cmd === "c" || cmd === "clear") {
185
+ console.clear();
186
+ console.log(`${COLORS.orange}${EMBERKIT_ASCII}${COLORS.reset}`);
187
+ }
188
+ else if (cmd === "q" || cmd === "quit") {
189
+ log("info", "Shutting down...");
190
+ await server.close();
191
+ process.exit(0);
192
+ }
193
+ }
194
+ else {
195
+ inputBuffer += key;
196
+ }
21
197
  });
22
- });
198
+ }
199
+ catch (error) {
200
+ log("error", `Failed to start server: ${error instanceof Error ? error.message : String(error)}`);
201
+ process.exit(1);
202
+ }
23
203
  }
@@ -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
+ ║ 🔥 E M B E R K I T 🔥 ║
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
+ ║ 🔥 E M B E R K I T 🔥 ║
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
+ }
@@ -1,11 +1,10 @@
1
1
  // Semver ranges for @emberkit/* packages written into generated projects.
2
- // Regenerated by scripts/sync-cli-template-versions.mjs from packages/*/package.json.
3
- // That script also sets packages/ui workspace:^ deps for @emberkit/core and @emberkit/icons.
2
+ // When releasing libraries, bump these to match packages/*/package.json "version".
4
3
  export const EMBERKIT_PACKAGE_VERSIONS = {
5
- core: "^0.2.8",
6
- ui: "^0.3.4",
7
- icons: "^0.2.6",
8
- cli: "^0.6.4",
9
- edge: "^0.2.3",
4
+ core: "^0.3.5",
5
+ ui: "^1.0.0",
6
+ icons: "^1.0.5",
7
+ cli: "^0.6.7",
8
+ edge: "^0.2.4",
10
9
  tsconfig: "^0.2.1",
11
10
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emberkit/cli",
3
- "version": "0.6.4",
3
+ "version": "0.6.7",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "CLI tool for EmberKit projects",
@@ -37,7 +37,16 @@
37
37
  }
38
38
  },
39
39
  "dependencies": {
40
- "inquirer": "^9.2.0"
40
+ "inquirer": "^9.2.0",
41
+ "vite": "^6.0.0"
42
+ },
43
+ "peerDependencies": {
44
+ "vite": "^5.0.0 || ^6.0.0"
45
+ },
46
+ "peerDependenciesMeta": {
47
+ "vite": {
48
+ "optional": false
49
+ }
41
50
  },
42
51
  "devDependencies": {
43
52
  "@eslint/js": "^10.0.1",