@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.
- package/dist/commands/build.js +72 -61
- package/dist/commands/dev.js +2 -37
- package/dist/emberkit-package-versions.js +4 -4
- package/dist/utils/load-config.js +107 -0
- package/package.json +2 -1
package/dist/commands/build.js
CHANGED
|
@@ -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
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
372
|
+
noExternal: ['virtual:emberkit-routes'],
|
|
362
373
|
},
|
|
363
374
|
};
|
|
364
375
|
await viteBuild(ssrConfig);
|
package/dist/commands/dev.js
CHANGED
|
@@ -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.
|
|
5
|
-
ui: "^
|
|
6
|
-
icons: "^
|
|
7
|
-
cli: "^0.
|
|
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.
|
|
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"
|