@emberkit/cli 0.6.8 → 0.7.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.
@@ -4,6 +4,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
4
4
  import { pathToFileURL } from "url";
5
5
  import { cliBrand } from "../brand.js";
6
6
  import { mergeEmberkitViteConfig } from "../utils/merge-emberkit-vite.js";
7
+ import { loadEmberKitConfig, loadViteConfig } from "../utils/load-config.js";
7
8
  const COLORS = {
8
9
  reset: "\x1b[0m",
9
10
  bright: "\x1b[1m",
@@ -29,48 +30,6 @@ function log(level, message) {
29
30
  const emberTag = `${COLORS.ember}[emberkit]${COLORS.reset}`;
30
31
  console.log(`${prefix} ${levelLabel} ${emberTag} ${message}`);
31
32
  }
32
- async function loadEmberKitConfig(root) {
33
- const configPaths = [
34
- join(root, "emberkit.config.ts"),
35
- join(root, "emberkit.config.js"),
36
- join(root, "emberkit.config.mjs"),
37
- ];
38
- for (const configPath of configPaths) {
39
- if (existsSync(configPath)) {
40
- try {
41
- const configUrl = pathToFileURL(configPath).href;
42
- const mod = await import(configUrl);
43
- return mod.default || mod;
44
- }
45
- catch {
46
- continue;
47
- }
48
- }
49
- }
50
- return null;
51
- }
52
- async function loadViteConfig(root) {
53
- const viteConfigPaths = [
54
- join(root, "vite.config.ts"),
55
- join(root, "vite.config.js"),
56
- ];
57
- for (const configPath of viteConfigPaths) {
58
- if (existsSync(configPath)) {
59
- try {
60
- const configUrl = pathToFileURL(configPath).href;
61
- const mod = await import(configUrl);
62
- const config = mod.default || mod;
63
- return typeof config === "function"
64
- ? config({ mode: "production", command: "build" })
65
- : config;
66
- }
67
- catch {
68
- continue;
69
- }
70
- }
71
- }
72
- return null;
73
- }
74
33
  export async function build(_args) {
75
34
  const root = process.cwd();
76
35
  console.log(`\n${cliBrand.logo()} ${COLORS.orange}EmberKit Build${COLORS.reset}\n`);
@@ -100,7 +59,7 @@ export async function build(_args) {
100
59
  log("info", "Building client bundle...");
101
60
  await buildClient(root, outDir, viteConfig, customLogger);
102
61
  log("info", "Building SSR bundle...");
103
- await buildSSR(root, outDir, viteConfig, customLogger);
62
+ await buildSSR(root, outDir, viteConfig, customLogger, emberkitConfig);
104
63
  log("info", "Generating SSR manifest...");
105
64
  await generateManifest(root, outDir, mode);
106
65
  if (mode === "hybrid") {
@@ -113,7 +72,7 @@ export async function build(_args) {
113
72
  log("info", "Building static site...");
114
73
  await buildClient(root, outDir, viteConfig, customLogger);
115
74
  log("info", "Building SSR bundle for pre-rendering...");
116
- await buildSSR(root, outDir, viteConfig, customLogger);
75
+ await buildSSR(root, outDir, viteConfig, customLogger, emberkitConfig);
117
76
  log("info", "Generating manifest...");
118
77
  await generateManifest(root, outDir, mode);
119
78
  log("info", "Pre-rendering all routes...");
@@ -129,11 +88,16 @@ export async function build(_args) {
129
88
  }
130
89
  }
131
90
  async function buildClient(root, outDir, viteConfig, customLogger) {
91
+ // Always ensure plugins are properly normalized as an array
92
+ const plugins = (viteConfig?.plugins && Array.isArray(viteConfig.plugins))
93
+ ? viteConfig.plugins
94
+ : (viteConfig?.plugins ? [viteConfig.plugins] : []);
132
95
  const clientConfig = {
133
96
  ...viteConfig,
134
97
  root,
135
98
  customLogger,
136
99
  logLevel: "silent",
100
+ plugins,
137
101
  build: {
138
102
  ...(viteConfig?.build || {}),
139
103
  outDir,
@@ -145,12 +109,30 @@ async function buildClient(root, outDir, viteConfig, customLogger) {
145
109
  },
146
110
  },
147
111
  },
112
+ ssr: undefined,
148
113
  };
149
114
  await viteBuild(clientConfig);
150
115
  }
151
- function getServerEntryShim() {
152
- return `import { routes } from 'virtual:emberkit-routes';
153
- import { createElement } from '@emberkit/core';
116
+ function siteConfigToHeadOptions(site) {
117
+ const config = site;
118
+ if (!config?.url) {
119
+ return "null";
120
+ }
121
+ return JSON.stringify({
122
+ siteUrl: config.url,
123
+ siteName: config.name,
124
+ titleSuffix: config.titleSuffix,
125
+ defaultDescription: config.description,
126
+ defaultOgImage: config.ogImage,
127
+ twitterSite: config.twitterSite,
128
+ });
129
+ }
130
+ function getServerEntryShim(site) {
131
+ const siteHeadOptions = siteConfigToHeadOptions(site);
132
+ return `import { routes, notFoundRoute, errorRoute } from 'virtual:emberkit-routes';
133
+ import { createElement, buildRouteHeadFromMetadata } from '@emberkit/core';
134
+
135
+ const siteHeadOptions = ${siteHeadOptions};
154
136
  import { readFileSync } from 'node:fs';
155
137
  import { join, dirname } from 'node:path';
156
138
  import { fileURLToPath } from 'node:url';
@@ -277,6 +259,7 @@ export async function render(url) {
277
259
 
278
260
  let appHtml = '';
279
261
  let headContent = '';
262
+ let status = 200;
280
263
 
281
264
  if (match) {
282
265
  try {
@@ -284,22 +267,49 @@ export async function render(url) {
284
267
  const Component = mod.default || mod;
285
268
 
286
269
  if (mod.metadata) {
287
- if (mod.metadata.title) {
288
- headContent += '<title>' + escapeHtml(mod.metadata.title) + '</title>\\n';
289
- }
290
- if (mod.metadata.description) {
291
- headContent += '<meta name="description" content="' + escapeHtml(mod.metadata.description) + '">\\n';
292
- }
270
+ headContent += buildRouteHeadFromMetadata(mod.metadata, pathname, siteHeadOptions ?? undefined) + '\\n';
293
271
  }
294
272
 
295
273
  const element = createElement(Component, { params: match.params });
296
274
  appHtml = renderToString(element);
297
275
  } catch (e) {
298
276
  console.error('[SSR] Failed to render route:', pathname, e);
299
- appHtml = '<div style="color: red; padding: 20px;">SSR Error: ' + escapeHtml(String(e)) + '</div>';
277
+ if (errorRoute) {
278
+ try {
279
+ status = 500;
280
+ const mod = await errorRoute();
281
+ const Component = mod.default || mod;
282
+ const errorInfo = {
283
+ status: 500,
284
+ message: e instanceof Error ? e.message : 'Internal Server Error',
285
+ error: e,
286
+ };
287
+ const element = createElement(Component, { error: errorInfo });
288
+ appHtml = renderToString(element);
289
+ } catch (fallbackError) {
290
+ console.error('[SSR] Failed to render 500 page:', fallbackError);
291
+ appHtml = '<div style="color: red; padding: 20px;">Internal Server Error</div>';
292
+ }
293
+ } else {
294
+ appHtml = '<div style="color: red; padding: 20px;">SSR Error: ' + escapeHtml(String(e)) + '</div>';
295
+ status = 500;
296
+ }
300
297
  }
301
298
  } else {
302
- appHtml = '<div style="padding: 20px;">404 - Page not found</div>';
299
+ status = 404;
300
+ if (notFoundRoute) {
301
+ try {
302
+ const mod = await notFoundRoute();
303
+ const Component = mod.default || mod;
304
+ const element = createElement(Component, {});
305
+ appHtml = renderToString(element);
306
+ } catch (e) {
307
+ console.error('[SSR] Failed to render 404 page:', e);
308
+ appHtml = '<div style="padding: 20px;">404 - Page not found</div>';
309
+ }
310
+ } else {
311
+ appHtml = '<div style="padding: 20px;">404 - Page not found</div>';
312
+ }
303
313
  }
304
314
 
305
315
  const templatePath = join(__dirname, '..', 'index.html');
@@ -317,11 +327,11 @@ export async function render(url) {
317
327
  template = template.replace('</head>', headContent + '</head>');
318
328
  }
319
329
 
320
- return template;
330
+ return { html: template, status };
321
331
  }
322
332
  `;
323
333
  }
324
- async function resolveSSREntry(root) {
334
+ async function resolveSSREntry(root, emberkitConfig) {
325
335
  const userEntryTs = join(root, "src", "entry-server.ts");
326
336
  const userEntryTsx = join(root, "src", "entry-server.tsx");
327
337
  if (existsSync(userEntryTs)) {
@@ -333,11 +343,12 @@ async function resolveSSREntry(root) {
333
343
  const cacheDir = join(root, "node_modules", ".cache", "emberkit");
334
344
  mkdirSync(cacheDir, { recursive: true });
335
345
  const shimPath = join(cacheDir, "server-entry.js");
336
- writeFileSync(shimPath, getServerEntryShim(), "utf-8");
346
+ const site = emberkitConfig?.site;
347
+ writeFileSync(shimPath, getServerEntryShim(site), "utf-8");
337
348
  return shimPath;
338
349
  }
339
- async function buildSSR(root, outDir, viteConfig, customLogger) {
340
- const ssrEntry = await resolveSSREntry(root);
350
+ async function buildSSR(root, outDir, viteConfig, customLogger, emberkitConfig) {
351
+ const ssrEntry = await resolveSSREntry(root, emberkitConfig);
341
352
  const ssrConfig = {
342
353
  ...viteConfig,
343
354
  root,
@@ -358,7 +369,7 @@ async function buildSSR(root, outDir, viteConfig, customLogger) {
358
369
  },
359
370
  },
360
371
  ssr: {
361
- noExternal: true,
372
+ noExternal: ['virtual:emberkit-routes'],
362
373
  },
363
374
  };
364
375
  await viteBuild(ssrConfig);
@@ -1,8 +1,6 @@
1
1
  import { createServer } from "vite";
2
- import { join } from "path";
3
- import { existsSync } from "fs";
4
- import { pathToFileURL } from "url";
5
2
  import { mergeEmberkitViteConfig } from "../utils/merge-emberkit-vite.js";
3
+ import { loadEmberKitConfig, loadViteConfig } from "../utils/load-config.js";
6
4
  const COLORS = {
7
5
  reset: "\x1b[0m",
8
6
  bright: "\x1b[1m",
@@ -45,46 +43,13 @@ function log(level, message, meta) {
45
43
  }
46
44
  console.log(output);
47
45
  }
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
46
  export async function dev(_args) {
82
47
  const root = process.cwd();
83
48
  console.clear();
84
49
  console.log(`${COLORS.orange}${EMBERKIT_ASCII}${COLORS.reset}`);
85
50
  log("info", "Initializing development server...");
86
51
  const emberkitConfig = await loadEmberKitConfig(root);
87
- const viteFileConfig = await loadViteConfig(root);
52
+ const viteFileConfig = await loadViteConfig(root, "serve");
88
53
  const viteConfig = mergeEmberkitViteConfig(emberkitConfig, viteFileConfig);
89
54
  if (emberkitConfig) {
90
55
  log("debug", "Loaded emberkit.config", { mode: emberkitConfig.mode || "hybrid" });
@@ -1,10 +1,10 @@
1
1
  // Semver ranges for @emberkit/* packages written into generated projects.
2
2
  // When releasing libraries, bump these to match packages/*/package.json "version".
3
3
  export const EMBERKIT_PACKAGE_VERSIONS = {
4
- core: "^0.3.8",
5
- ui: "^1.0.1",
6
- icons: "^1.0.8",
7
- cli: "^0.6.8",
4
+ core: "^0.6.0",
5
+ ui: "^4.0.0",
6
+ icons: "^4.0.0",
7
+ cli: "^0.7.0",
8
8
  edge: "^0.2.4",
9
9
  tsconfig: "^0.2.1",
10
10
  };
@@ -0,0 +1,107 @@
1
+ import { existsSync, mkdirSync } from "fs";
2
+ import { join } from "path";
3
+ import { pathToFileURL } from "url";
4
+ /**
5
+ * Loads a TypeScript or JavaScript config file in any Node environment.
6
+ *
7
+ * Direct `import()` of `.ts` files only works on runtimes with TypeScript
8
+ * support (e.g. tsx, ts-node, Node ≥ 22 with --experimental-strip-types).
9
+ * Cloudflare Pages CI runs plain Node 18/20, so `.ts` imports silently fail.
10
+ *
11
+ * We use esbuild (a transitive dep of Vite, always present) to bundle the
12
+ * config to a temporary `.mjs` file and import that instead.
13
+ */
14
+ async function transpileAndImport(filePath, root) {
15
+ // Use a cache dir that survives across the two viteBuild calls
16
+ const cacheDir = join(root, "node_modules", ".cache", "emberkit");
17
+ mkdirSync(cacheDir, { recursive: true });
18
+ const outFile = join(cacheDir, `config-${Date.now()}-${Math.random().toString(36).slice(2)}.mjs`);
19
+ try {
20
+ // esbuild is always available as a transitive dependency of Vite
21
+ const { build: esbuild } = await import("esbuild");
22
+ await esbuild({
23
+ entryPoints: [filePath],
24
+ bundle: true,
25
+ format: "esm",
26
+ platform: "node",
27
+ outfile: outFile,
28
+ // Preserve all package imports so they resolve from node_modules at runtime
29
+ packages: "external",
30
+ logLevel: "silent",
31
+ });
32
+ const mod = await import(pathToFileURL(outFile).href);
33
+ return (mod.default ?? mod);
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ /**
40
+ * Loads the `emberkit.config.ts` (or `.js` / `.mjs`) for a project root.
41
+ * Returns `null` when no config file is found or loading fails.
42
+ */
43
+ export async function loadEmberKitConfig(root) {
44
+ const candidates = [
45
+ join(root, "emberkit.config.ts"),
46
+ join(root, "emberkit.config.js"),
47
+ join(root, "emberkit.config.mjs"),
48
+ ];
49
+ for (const filePath of candidates) {
50
+ if (!existsSync(filePath))
51
+ continue;
52
+ const ext = filePath.split(".").pop();
53
+ // Plain JS/MJS files can be imported directly
54
+ if (ext === "js" || ext === "mjs") {
55
+ try {
56
+ const mod = await import(pathToFileURL(filePath).href);
57
+ return (mod.default ?? mod);
58
+ }
59
+ catch {
60
+ continue;
61
+ }
62
+ }
63
+ // TypeScript files need transpilation
64
+ const result = await transpileAndImport(filePath, root);
65
+ if (result !== null)
66
+ return result;
67
+ }
68
+ return null;
69
+ }
70
+ /**
71
+ * Loads the `vite.config.ts` (or `.js`) for a project root.
72
+ * Returns `null` when no config file is found or loading fails.
73
+ */
74
+ export async function loadViteConfig(root, command = "build") {
75
+ const candidates = [
76
+ join(root, "vite.config.ts"),
77
+ join(root, "vite.config.js"),
78
+ ];
79
+ for (const filePath of candidates) {
80
+ if (!existsSync(filePath))
81
+ continue;
82
+ const ext = filePath.split(".").pop();
83
+ let raw = null;
84
+ if (ext === "js") {
85
+ try {
86
+ const mod = await import(pathToFileURL(filePath).href);
87
+ raw = mod.default ?? mod;
88
+ }
89
+ catch {
90
+ continue;
91
+ }
92
+ }
93
+ else {
94
+ raw = await transpileAndImport(filePath, root);
95
+ }
96
+ if (raw === null)
97
+ continue;
98
+ const resolved = typeof raw === "function"
99
+ ? raw({
100
+ mode: command === "serve" ? "development" : "production",
101
+ command,
102
+ })
103
+ : raw;
104
+ return resolved;
105
+ }
106
+ return null;
107
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emberkit/cli",
3
- "version": "0.6.8",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "CLI tool for EmberKit projects",
@@ -51,6 +51,7 @@
51
51
  "devDependencies": {
52
52
  "@eslint/js": "^10.0.1",
53
53
  "@types/inquirer": "^9.0.3",
54
+ "esbuild": "^0.28.0",
54
55
  "eslint": "^10.0.0",
55
56
  "eslint-plugin-perfectionist": "^5.9.0",
56
57
  "typescript-eslint": "^8.59.3"