@zenithbuild/cli 0.4.11 → 0.5.0-beta.2.15

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.
@@ -0,0 +1,278 @@
1
+ // ---------------------------------------------------------------------------
2
+ // dev-server.js — Zenith CLI V0
3
+ // ---------------------------------------------------------------------------
4
+ // Development server with in-memory compilation and file watching.
5
+ //
6
+ // - Compiles pages on demand
7
+ // - Rebuilds on file change
8
+ // - Injects HMR client script
9
+ // - Server route resolution uses manifest matching
10
+ //
11
+ // V0: Uses Node.js http module + fs.watch. No external deps.
12
+ // ---------------------------------------------------------------------------
13
+
14
+ import { createServer } from 'node:http';
15
+ import { watch } from 'node:fs';
16
+ import { readFile } from 'node:fs/promises';
17
+ import { join, extname } from 'node:path';
18
+ import { build } from './build.js';
19
+ import {
20
+ executeServerRoute,
21
+ injectSsrPayload,
22
+ loadRouteManifest,
23
+ resolveWithinDist,
24
+ toStaticFilePath
25
+ } from './preview.js';
26
+ import { resolveRequestRoute } from './server/resolve-request-route.js';
27
+
28
+ const MIME_TYPES = {
29
+ '.html': 'text/html',
30
+ '.js': 'application/javascript',
31
+ '.css': 'text/css',
32
+ '.json': 'application/json',
33
+ '.png': 'image/png',
34
+ '.jpg': 'image/jpeg',
35
+ '.svg': 'image/svg+xml'
36
+ };
37
+
38
+ const HMR_CLIENT_SCRIPT = `
39
+ <script>
40
+ // Zenith HMR Client V0
41
+ (function() {
42
+ const es = new EventSource('/__zenith_hmr');
43
+ es.onmessage = function(event) {
44
+ if (event.data === 'reload') {
45
+ window.location.reload();
46
+ }
47
+ };
48
+ es.onerror = function() {
49
+ setTimeout(function() { window.location.reload(); }, 1000);
50
+ };
51
+ })();
52
+ </script>`;
53
+
54
+ /**
55
+ * Create and start a development server.
56
+ *
57
+ * @param {{ pagesDir: string, outDir: string, port?: number, config?: object }} options
58
+ * @returns {Promise<{ server: import('http').Server, port: number, close: () => void }>}
59
+ */
60
+ export async function createDevServer(options) {
61
+ const {
62
+ pagesDir,
63
+ outDir,
64
+ port = 3000,
65
+ config = {}
66
+ } = options;
67
+
68
+ /** @type {import('http').ServerResponse[]} */
69
+ const hmrClients = [];
70
+ let _watcher = null;
71
+
72
+ // Initial build
73
+ await build({ pagesDir, outDir, config });
74
+
75
+ const server = createServer(async (req, res) => {
76
+ const url = new URL(req.url, `http://localhost:${port}`);
77
+ let pathname = url.pathname;
78
+
79
+ // HMR endpoint
80
+ if (pathname === '/__zenith_hmr') {
81
+ res.writeHead(200, {
82
+ 'Content-Type': 'text/event-stream',
83
+ 'Cache-Control': 'no-cache',
84
+ 'Connection': 'keep-alive'
85
+ });
86
+ // Flush headers by sending initial comment
87
+ res.write(': connected\n\n');
88
+ hmrClients.push(res);
89
+ req.on('close', () => {
90
+ const idx = hmrClients.indexOf(res);
91
+ if (idx !== -1) hmrClients.splice(idx, 1);
92
+ });
93
+ return;
94
+ }
95
+
96
+ if (pathname === '/__zenith/route-check') {
97
+ try {
98
+ const targetPath = String(url.searchParams.get('path') || '/');
99
+ const targetUrl = new URL(targetPath, `http://localhost:${port}`);
100
+ const routes = await loadRouteManifest(outDir);
101
+ const resolvedCheck = resolveRequestRoute(targetUrl, routes);
102
+ if (!resolvedCheck.matched || !resolvedCheck.route) {
103
+ res.writeHead(404, { 'Content-Type': 'application/json' });
104
+ res.end(JSON.stringify({ error: 'route_not_found' }));
105
+ return;
106
+ }
107
+
108
+ const checkResult = await executeServerRoute({
109
+ source: resolvedCheck.route.server_script || '',
110
+ sourcePath: resolvedCheck.route.server_script_path || '',
111
+ params: resolvedCheck.params,
112
+ requestUrl: targetUrl.toString(),
113
+ requestMethod: req.method || 'GET',
114
+ requestHeaders: req.headers,
115
+ routePattern: resolvedCheck.route.path,
116
+ routeFile: resolvedCheck.route.server_script_path || '',
117
+ routeId: resolvedCheck.route.route_id || ''
118
+ });
119
+ res.writeHead(200, { 'Content-Type': 'application/json' });
120
+ res.end(JSON.stringify(checkResult));
121
+ return;
122
+ } catch {
123
+ res.writeHead(500, { 'Content-Type': 'application/json' });
124
+ res.end(JSON.stringify({ error: 'route_check_failed' }));
125
+ return;
126
+ }
127
+ }
128
+
129
+ try {
130
+ const requestExt = extname(pathname);
131
+ if (requestExt && requestExt !== '.html') {
132
+ const assetPath = join(outDir, pathname);
133
+ const asset = await readFile(assetPath);
134
+ const mime = MIME_TYPES[requestExt] || 'application/octet-stream';
135
+ res.writeHead(200, { 'Content-Type': mime });
136
+ res.end(asset);
137
+ return;
138
+ }
139
+
140
+ const routes = await loadRouteManifest(outDir);
141
+ const resolved = resolveRequestRoute(url, routes);
142
+ let filePath = null;
143
+
144
+ if (resolved.matched && resolved.route) {
145
+ console.log(`[zenith] Request: ${pathname} | Route: ${resolved.route.path} | Params: ${JSON.stringify(resolved.params)}`);
146
+ const output = resolved.route.output.startsWith('/')
147
+ ? resolved.route.output.slice(1)
148
+ : resolved.route.output;
149
+ filePath = resolveWithinDist(outDir, output);
150
+ } else {
151
+ filePath = toStaticFilePath(outDir, pathname);
152
+ }
153
+
154
+ if (!filePath) {
155
+ throw new Error('not found');
156
+ }
157
+
158
+ let ssrPayload = null;
159
+ if (resolved.matched && resolved.route?.server_script && resolved.route.prerender !== true) {
160
+ let routeExecution = null;
161
+ try {
162
+ routeExecution = await executeServerRoute({
163
+ source: resolved.route.server_script,
164
+ sourcePath: resolved.route.server_script_path || '',
165
+ params: resolved.params,
166
+ requestUrl: url.toString(),
167
+ requestMethod: req.method || 'GET',
168
+ requestHeaders: req.headers,
169
+ routePattern: resolved.route.path,
170
+ routeFile: resolved.route.server_script_path || '',
171
+ routeId: resolved.route.route_id || ''
172
+ });
173
+ } catch (error) {
174
+ routeExecution = {
175
+ result: {
176
+ kind: 'deny',
177
+ status: 500,
178
+ message: error instanceof Error ? error.message : String(error)
179
+ },
180
+ trace: {
181
+ guard: 'none',
182
+ load: 'deny'
183
+ }
184
+ };
185
+ }
186
+
187
+ const trace = routeExecution?.trace || { guard: 'none', load: 'none' };
188
+ const routeId = resolved.route.route_id || '';
189
+ console.log(`[Zenith] guard(${routeId || resolved.route.path}) -> ${trace.guard}`);
190
+ console.log(`[Zenith] load(${routeId || resolved.route.path}) -> ${trace.load}`);
191
+
192
+ const result = routeExecution?.result;
193
+ if (result && result.kind === 'redirect') {
194
+ const status = Number.isInteger(result.status) ? result.status : 302;
195
+ res.writeHead(status, {
196
+ Location: result.location,
197
+ 'Cache-Control': 'no-store'
198
+ });
199
+ res.end('');
200
+ return;
201
+ }
202
+ if (result && result.kind === 'deny') {
203
+ const status = Number.isInteger(result.status) ? result.status : 403;
204
+ res.writeHead(status, { 'Content-Type': 'text/plain; charset=utf-8' });
205
+ res.end(result.message || (status === 401 ? 'Unauthorized' : 'Forbidden'));
206
+ return;
207
+ }
208
+ if (result && result.kind === 'data' && result.data && typeof result.data === 'object' && !Array.isArray(result.data)) {
209
+ ssrPayload = result.data;
210
+ }
211
+ }
212
+
213
+ let content = await readFile(filePath, 'utf8');
214
+ if (ssrPayload) {
215
+ content = injectSsrPayload(content, ssrPayload);
216
+ }
217
+ content = content.replace('</body>', `${HMR_CLIENT_SCRIPT}</body>`);
218
+ res.writeHead(200, { 'Content-Type': 'text/html' });
219
+ res.end(content);
220
+ } catch {
221
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
222
+ res.end('404 Not Found');
223
+ }
224
+ });
225
+
226
+ /**
227
+ * Broadcast HMR reload to all connected clients.
228
+ */
229
+ function _broadcastReload() {
230
+ for (const client of hmrClients) {
231
+ try {
232
+ client.write('data: reload\n\n');
233
+ } catch {
234
+ // client disconnected
235
+ }
236
+ }
237
+ }
238
+
239
+ /**
240
+ * Start watching the pages directory for changes.
241
+ */
242
+ function _startWatcher() {
243
+ try {
244
+ _watcher = watch(pagesDir, { recursive: true }, async (eventType, filename) => {
245
+ if (!filename) return;
246
+
247
+ // Rebuild
248
+ await build({ pagesDir, outDir, config });
249
+ _broadcastReload();
250
+ });
251
+ } catch {
252
+ // fs.watch may not support recursive on all platforms
253
+ }
254
+ }
255
+
256
+ return new Promise((resolve) => {
257
+ server.listen(port, () => {
258
+ const actualPort = server.address().port;
259
+ _startWatcher();
260
+
261
+ resolve({
262
+ server,
263
+ port: actualPort,
264
+ close: () => {
265
+ if (_watcher) {
266
+ _watcher.close();
267
+ _watcher = null;
268
+ }
269
+ for (const client of hmrClients) {
270
+ try { client.end(); } catch { }
271
+ }
272
+ hmrClients.length = 0;
273
+ server.close();
274
+ }
275
+ });
276
+ });
277
+ });
278
+ }
package/dist/index.js ADDED
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env node
2
+ // ---------------------------------------------------------------------------
3
+ // index.js — Zenith CLI V0 Entry Point
4
+ // ---------------------------------------------------------------------------
5
+ // Commands:
6
+ // zenith dev — Development server + HMR
7
+ // zenith build — Static site generation to /dist
8
+ // zenith preview — Serve /dist statically
9
+ //
10
+ // Minimal arg parsing. No heavy dependencies.
11
+ // ---------------------------------------------------------------------------
12
+
13
+ import { resolve, join, dirname } from 'node:path';
14
+ import { existsSync, readFileSync } from 'node:fs';
15
+ import { fileURLToPath } from 'node:url';
16
+ import { createLogger } from './ui/logger.js';
17
+
18
+ const COMMANDS = ['dev', 'build', 'preview'];
19
+ const DEFAULT_VERSION = '0.0.0';
20
+ const __filename = fileURLToPath(import.meta.url);
21
+ const __dirname = dirname(__filename);
22
+
23
+ function getCliVersion() {
24
+ try {
25
+ const pkgPath = join(__dirname, '..', 'package.json');
26
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
27
+ return typeof pkg.version === 'string' ? pkg.version : DEFAULT_VERSION;
28
+ } catch {
29
+ return DEFAULT_VERSION;
30
+ }
31
+ }
32
+
33
+ function printUsage(logger) {
34
+ logger.heading('V0');
35
+ logger.print('Usage:');
36
+ logger.print(' zenith dev [port|--port <port>] Start development server');
37
+ logger.print(' zenith build Build static site to /dist');
38
+ logger.print(' zenith preview [port|--port <port>] Preview /dist statically');
39
+ logger.print('');
40
+ logger.print('Options:');
41
+ logger.print(' -h, --help Show this help message');
42
+ logger.print(' -v, --version Print Zenith CLI version');
43
+ logger.print('');
44
+ }
45
+
46
+ function resolvePort(args, fallback) {
47
+ if (!Array.isArray(args) || args.length === 0) {
48
+ return fallback;
49
+ }
50
+
51
+ const flagIndex = args.findIndex((arg) => arg === '--port' || arg === '-p');
52
+ if (flagIndex >= 0 && args[flagIndex + 1]) {
53
+ const parsed = Number.parseInt(args[flagIndex + 1], 10);
54
+ if (Number.isFinite(parsed)) {
55
+ return parsed;
56
+ }
57
+ }
58
+
59
+ const positional = args.find((arg) => /^[0-9]+$/.test(arg));
60
+ if (positional) {
61
+ const parsed = Number.parseInt(positional, 10);
62
+ if (Number.isFinite(parsed)) {
63
+ return parsed;
64
+ }
65
+ }
66
+
67
+ return fallback;
68
+ }
69
+
70
+ /**
71
+ * Load zenith.config.js from project root.
72
+ *
73
+ * @param {string} projectRoot
74
+ * @returns {Promise<object>}
75
+ */
76
+ async function loadConfig(projectRoot) {
77
+ const configPath = join(projectRoot, 'zenith.config.js');
78
+ try {
79
+ const mod = await import(configPath);
80
+ return mod.default || {};
81
+ } catch {
82
+ return {};
83
+ }
84
+ }
85
+
86
+ /**
87
+ * CLI entry point.
88
+ *
89
+ * @param {string[]} args - Process arguments (without node and script paths)
90
+ * @param {string} [cwd] - Working directory override
91
+ */
92
+ export async function cli(args, cwd) {
93
+ const logger = createLogger(process);
94
+ const command = args[0];
95
+ const cliVersion = getCliVersion();
96
+
97
+ if (args.includes('--version') || args.includes('-v')) {
98
+ logger.print(`zenith ${cliVersion}`);
99
+ process.exit(0);
100
+ }
101
+
102
+ if (args.includes('--help') || args.includes('-h')) {
103
+ printUsage(logger);
104
+ process.exit(0);
105
+ }
106
+
107
+ if (!command || !COMMANDS.includes(command)) {
108
+ printUsage(logger);
109
+ process.exit(command ? 1 : 0);
110
+ }
111
+
112
+ const projectRoot = resolve(cwd || process.cwd());
113
+ const rootPagesDir = join(projectRoot, 'pages');
114
+ const srcPagesDir = join(projectRoot, 'src', 'pages');
115
+ const pagesDir = existsSync(rootPagesDir) ? rootPagesDir : srcPagesDir;
116
+ const outDir = join(projectRoot, 'dist');
117
+ const config = await loadConfig(projectRoot);
118
+
119
+ if (command === 'build') {
120
+ const { build } = await import('./build.js');
121
+ logger.info('Building...');
122
+ const result = await build({ pagesDir, outDir, config });
123
+ logger.success(`Built ${result.pages} page(s), ${result.assets.length} asset(s)`);
124
+ logger.summary([{ label: 'Output', value: './dist' }]);
125
+ }
126
+
127
+ if (command === 'dev') {
128
+ const { createDevServer } = await import('./dev-server.js');
129
+ const port = resolvePort(args.slice(1), 3000);
130
+ logger.info('Starting dev server...');
131
+ const dev = await createDevServer({ pagesDir, outDir, port, config });
132
+ logger.success(`Dev server running at http://localhost:${dev.port}`);
133
+
134
+ // Graceful shutdown
135
+ process.on('SIGINT', () => {
136
+ dev.close();
137
+ process.exit(0);
138
+ });
139
+ process.on('SIGTERM', () => {
140
+ dev.close();
141
+ process.exit(0);
142
+ });
143
+ }
144
+
145
+ if (command === 'preview') {
146
+ const { createPreviewServer } = await import('./preview.js');
147
+ const port = resolvePort(args.slice(1), 4000);
148
+ logger.info('Starting preview server...');
149
+ const preview = await createPreviewServer({ distDir: outDir, port });
150
+ logger.success(`Preview server running at http://localhost:${preview.port}`);
151
+
152
+ process.on('SIGINT', () => {
153
+ preview.close();
154
+ process.exit(0);
155
+ });
156
+ process.on('SIGTERM', () => {
157
+ preview.close();
158
+ process.exit(0);
159
+ });
160
+ }
161
+ }
162
+
163
+ // Auto-run if called directly
164
+ const isDirectRun = process.argv[1] && (
165
+ process.argv[1].endsWith('/index.js') ||
166
+ process.argv[1].endsWith('/zenith')
167
+ );
168
+
169
+ if (isDirectRun) {
170
+ cli(process.argv.slice(2)).catch((error) => {
171
+ const logger = createLogger(process);
172
+ logger.error(error);
173
+ process.exit(1);
174
+ });
175
+ }