@hashrock/ono 0.1.1 → 0.1.2

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/src/cli.js CHANGED
@@ -1,1014 +1,247 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * CLI Tool for mini-jsx
4
+ * Ono CLI - Minimalist SSG framework
5
5
  */
6
+ import { resolve } from "node:path";
7
+ import { copyFile, mkdir, readdir, stat } from "node:fs/promises";
8
+ import { existsSync } from "node:fs";
9
+ import { join, relative } from "node:path";
10
+ import { loadUnoConfig } from "./unocss.js";
11
+ import { createDevServer } from "./server.js";
12
+ import { buildFile, buildFiles, generateUnoCSS } from "./builder.js";
13
+ import { watchFile, watchFiles, createWebSocketServer } from "./watcher.js";
6
14
 
7
- import fs from "node:fs/promises";
8
- import path from "node:path";
9
- import http from "node:http";
10
- import net from "node:net";
11
- import { bundle } from "./bundler.js";
12
- import { renderToString } from "./renderer.js";
13
- import { WebSocketServer } from "ws";
14
- import { generateCSSFromFiles, loadUnoConfig } from "./unocss.js";
15
+ const args = process.argv.slice(2);
16
+ const command = args[0];
15
17
 
16
- /**
17
- * Find an available port starting from the given port
18
- * @param {number} startPort - Port to start searching from
19
- * @param {number} maxAttempts - Maximum number of ports to try
20
- * @returns {Promise<number>} Available port number
21
- */
22
- async function findAvailablePort(startPort, maxAttempts = 10) {
23
- for (let i = 0; i < maxAttempts; i++) {
24
- const port = startPort + i;
25
- try {
26
- await new Promise((resolve, reject) => {
27
- const server = net.createServer();
28
- server.once("error", reject);
29
- server.once("listening", () => {
30
- server.close();
31
- resolve();
32
- });
33
- server.listen(port);
34
- });
35
- return port;
36
- } catch (err) {
37
- if (err.code !== "EADDRINUSE") {
38
- throw err;
39
- }
40
- // Port is in use, try next one
41
- }
42
- }
43
- throw new Error(`Could not find available port in range ${startPort}-${startPort + maxAttempts - 1}`);
44
- }
18
+ async function copyPublicFiles(outputDir = "dist") {
19
+ const publicDir = resolve(process.cwd(), "public");
20
+ const outDir = resolve(process.cwd(), outputDir);
45
21
 
46
- /**
47
- * Parse command line arguments into options object
48
- * @param {string[]} args - Command line arguments
49
- * @returns {object} Parsed options
50
- */
51
- function parseArgs(args) {
52
- const options = {
53
- _: [], // Positional arguments
54
- };
55
-
56
- for (let i = 0; i < args.length; i++) {
57
- const arg = args[i];
58
-
59
- if (arg === "--watch" || arg === "-w") {
60
- options.watch = true;
61
- } else if (arg === "--port" || arg === "-p") {
62
- options.port = parseInt(args[++i]);
63
- } else if (arg === "--output" || arg === "-o") {
64
- options.output = args[++i];
65
- } else if (arg === "--help" || arg === "-h") {
66
- options.help = true;
67
- } else if (arg === "--version" || arg === "-v") {
68
- options.version = true;
69
- } else if (!arg.startsWith("-")) {
70
- options._.push(arg);
71
- }
22
+ if (!existsSync(publicDir)) {
23
+ return;
72
24
  }
73
25
 
74
- return options;
75
- }
76
-
77
- /**
78
- * Discover all JSX files in the pages directory
79
- * @param {string} pagesDir - Path to pages directory
80
- * @returns {Promise<string[]>} Array of JSX file paths
81
- */
82
- async function discoverPages(pagesDir) {
83
- const pages = [];
84
-
85
- async function walk(dir) {
86
- const entries = await fs.readdir(dir, { withFileTypes: true });
26
+ async function copyRecursive(src, dest) {
27
+ const entries = await readdir(src, { withFileTypes: true });
87
28
 
88
29
  for (const entry of entries) {
89
- const fullPath = path.join(dir, entry.name);
30
+ const srcPath = join(src, entry.name);
31
+ const destPath = join(dest, entry.name);
90
32
 
91
33
  if (entry.isDirectory()) {
92
- await walk(fullPath);
93
- } else if (entry.isFile() && entry.name.endsWith(".jsx")) {
94
- pages.push(fullPath);
34
+ await mkdir(destPath, { recursive: true });
35
+ await copyRecursive(srcPath, destPath);
36
+ } else {
37
+ await mkdir(dest, { recursive: true });
38
+ await copyFile(srcPath, destPath);
95
39
  }
96
40
  }
97
41
  }
98
42
 
99
- try {
100
- await walk(pagesDir);
101
- return pages;
102
- } catch (error) {
103
- if (error.code === "ENOENT") {
104
- return [];
105
- }
106
- throw error;
107
- }
43
+ await copyRecursive(publicDir, outDir);
108
44
  }
109
45
 
110
- /**
111
- * Generate UnoCSS file from built HTML files
112
- * @param {string} outputDir - Output directory containing HTML files
113
- * @param {boolean} silent - Suppress console output
114
- * @returns {Promise<boolean>} True if CSS was generated
115
- */
116
- async function generateUnoCSSFile(outputDir, silent = false) {
117
- const outputDirAbs = path.resolve(process.cwd(), outputDir);
46
+ async function runBuildCommand() {
47
+ // Check for help flag
48
+ if (args.includes("--help") || args.includes("-h")) {
49
+ console.log(`
50
+ Usage: ono build [input] [options]
118
51
 
119
- // Find all HTML files
120
- const htmlFiles = [];
52
+ Build JSX files to static HTML
121
53
 
122
- async function findHTMLFiles(dir) {
123
- const entries = await fs.readdir(dir, { withFileTypes: true });
54
+ Arguments:
55
+ input File or directory to build (default: pages)
124
56
 
125
- for (const entry of entries) {
126
- const fullPath = path.join(dir, entry.name);
57
+ Options:
58
+ --output <dir> Output directory (default: dist)
59
+ --help Show this help message
127
60
 
128
- if (entry.isDirectory()) {
129
- await findHTMLFiles(fullPath);
130
- } else if (entry.isFile() && entry.name.endsWith(".html")) {
131
- htmlFiles.push(fullPath);
132
- }
133
- }
61
+ Examples:
62
+ ono build Build all pages in pages/ directory
63
+ ono build pages/index.jsx Build a single file
64
+ ono build --output build Build to build/ directory
65
+ `);
66
+ process.exit(0);
134
67
  }
135
68
 
136
- try {
137
- await findHTMLFiles(outputDirAbs);
138
-
139
- if (htmlFiles.length === 0) {
140
- return false;
141
- }
69
+ // Filter out options to get the input argument
70
+ const nonOptionArgs = args.slice(1).filter(arg => !arg.startsWith("--"));
71
+ const input = nonOptionArgs[0] || "pages";
72
+ const outputDir = args.includes("--output")
73
+ ? args[args.indexOf("--output") + 1]
74
+ : "dist";
142
75
 
143
- // Load UnoCSS config
144
- const unoConfig = await loadUnoConfig(path.resolve(process.cwd(), "uno.config.js"));
76
+ const unocssConfig = await loadUnoConfig();
145
77
 
146
- // Generate CSS from all HTML files
147
- const css = await generateCSSFromFiles(htmlFiles, unoConfig);
78
+ // Check if input is a directory or a file
79
+ const inputPath = resolve(process.cwd(), input);
80
+ const inputStat = await stat(inputPath);
148
81
 
149
- if (css) {
150
- // Write CSS file
151
- const cssPath = path.join(outputDirAbs, "uno.css");
152
- await fs.writeFile(cssPath, css);
82
+ if (inputStat.isDirectory()) {
83
+ // Build all files in directory
84
+ await buildFiles(input, { outputDir, unocssConfig });
85
+ } else {
86
+ // Build single file
87
+ await buildFile(input, { outputDir, unocssConfig });
88
+ }
153
89
 
154
- if (!silent) {
155
- console.log(`\n⚔ Generated UnoCSS: uno.css`);
156
- }
90
+ // Copy public files
91
+ await copyPublicFiles(outputDir);
157
92
 
158
- return true;
159
- }
93
+ // Generate UnoCSS
94
+ await generateUnoCSS({ outputDir, unocssConfig });
160
95
 
161
- return false;
162
- } catch (error) {
163
- if (!silent) {
164
- console.error(`Warning: UnoCSS generation failed: ${error.message}`);
165
- }
166
- return false;
167
- }
96
+ console.log("\n✨ Build complete!");
168
97
  }
169
98
 
170
- /**
171
- * Copy public directory to output directory
172
- * @param {string} publicDir - Path to public directory
173
- * @param {string} outputDir - Path to output directory
174
- * @param {boolean} silent - Suppress console output
175
- * @returns {Promise<number>} Number of files copied
176
- */
177
- async function copyPublicFiles(publicDir, outputDir, silent = false) {
178
- const publicDirAbs = path.resolve(process.cwd(), publicDir);
179
- const outputDirAbs = path.resolve(process.cwd(), outputDir);
180
99
 
181
- try {
182
- // Check if public directory exists
183
- await fs.access(publicDirAbs);
184
- } catch (error) {
185
- // Public directory doesn't exist, skip
186
- return 0;
187
- }
188
100
 
189
- let fileCount = 0;
101
+ async function runDevCommand() {
102
+ const input = args[1] || "pages";
103
+ const port = args.includes("--port")
104
+ ? parseInt(args[args.indexOf("--port") + 1])
105
+ : 3000;
106
+ const outputDir = args.includes("--output")
107
+ ? args[args.indexOf("--output") + 1]
108
+ : "dist";
190
109
 
191
- async function copyRecursive(src, dest) {
192
- const entries = await fs.readdir(src, { withFileTypes: true });
110
+ const unocssConfig = await loadUnoConfig();
193
111
 
194
- for (const entry of entries) {
195
- const srcPath = path.join(src, entry.name);
196
- const destPath = path.join(dest, entry.name);
112
+ // Initial build
113
+ const inputPath = resolve(process.cwd(), input);
114
+ const inputStat = await stat(inputPath);
197
115
 
198
- if (entry.isDirectory()) {
199
- await fs.mkdir(destPath, { recursive: true });
200
- await copyRecursive(srcPath, destPath);
201
- } else {
202
- await fs.copyFile(srcPath, destPath);
203
- fileCount++;
204
- if (!silent) {
205
- const relativePath = path.relative(publicDirAbs, srcPath);
206
- console.log(`šŸ“„ Copied: public/${relativePath}`);
207
- }
208
- }
209
- }
116
+ if (inputStat.isDirectory()) {
117
+ await buildFiles(input, { outputDir, unocssConfig });
118
+ } else {
119
+ await buildFile(input, { outputDir, unocssConfig });
210
120
  }
211
121
 
212
- await copyRecursive(publicDirAbs, outputDirAbs);
213
- return fileCount;
214
- }
122
+ await copyPublicFiles(outputDir);
123
+ await generateUnoCSS({ outputDir, unocssConfig });
215
124
 
216
- /**
217
- * Build a JSX file to HTML
218
- * @param {string} inputFile - Path to input JSX file
219
- * @param {object} options - Build options
220
- * @param {boolean} options.liveReload - Inject live reload script
221
- * @param {boolean} options.silent - Suppress console output
222
- * @param {number} options.wsPort - WebSocket port for live reload
223
- * @param {string} options.outputDir - Custom output directory
224
- * @param {string} options.pagesDir - Pages directory for relative path calculation
225
- * @returns {Promise<{outputPath: string, html: string}>}
226
- */
227
- async function buildFile(inputFile, options = {}) {
228
- const { liveReload = false, silent = false, wsPort = 35729, outputDir = "dist", pagesDir = null } = options;
229
- const absolutePath = path.resolve(process.cwd(), inputFile);
125
+ // Create WebSocket server for live reload
126
+ const { wss, port: wsPort } = createWebSocketServer();
230
127
 
231
- if (!silent) {
232
- console.log(`Building ${inputFile}...`);
233
- }
128
+ // Start dev server
129
+ const mode = inputStat.isDirectory() ? "pages" : "single";
130
+ const indexFile = inputStat.isFile()
131
+ ? relative(
132
+ outputDir,
133
+ (await buildFile(input, { outputDir, unocssConfig, silent: true }))
134
+ .outputPath
135
+ )
136
+ : "index.html";
234
137
 
235
- // Bundle the JSX file
236
- const bundledCode = await bundle(absolutePath);
237
-
238
- // Create a temporary module to execute
239
- // We need to inject our runtime inline (no external dependencies)
240
- const codeWithRuntime = `
241
- // Inline JSX Runtime - No external dependencies required
242
- function flattenChildren(children) {
243
- const result = [];
244
- for (const child of children) {
245
- if (child === null || child === undefined || typeof child === 'boolean') {
246
- continue;
247
- }
248
- if (Array.isArray(child)) {
249
- result.push(...flattenChildren(child));
138
+ let serverPort = port;
139
+ try {
140
+ const { server, app, port: actualPort } = await createDevServer({
141
+ outputDir,
142
+ port,
143
+ mode,
144
+ indexFile,
145
+ });
146
+ serverPort = actualPort;
147
+ } catch (error) {
148
+ if (error.code === "EADDRINUSE") {
149
+ serverPort = port + 1;
150
+ console.log(
151
+ `ā„¹ļø Port ${port} is busy, using port ${serverPort} instead`
152
+ );
153
+ await createDevServer({
154
+ outputDir,
155
+ port: serverPort,
156
+ mode,
157
+ indexFile,
158
+ });
250
159
  } else {
251
- result.push(child);
252
- }
253
- }
254
- return result;
255
- }
256
-
257
- function h(tag, props, ...children) {
258
- return {
259
- tag,
260
- props: props || {},
261
- children: flattenChildren(children)
262
- };
263
- }
264
-
265
- ${bundledCode}
266
- `;
267
-
268
- // Write to temporary file
269
- const tmpFile = path.join(process.cwd(), ".mini-jsx-tmp.js");
270
- await fs.writeFile(tmpFile, codeWithRuntime);
271
-
272
- // Import and execute
273
- const moduleUrl = `${tmpFile}?t=${Date.now()}`;
274
- const module = await import(moduleUrl);
275
- const App = module.default;
276
-
277
- if (!App) {
278
- throw new Error("No default export found in entry file");
279
- }
280
-
281
- // Render to HTML
282
- const vnode = typeof App === "function" ? App({}) : App;
283
- let html = renderToString(vnode);
284
-
285
- // Inject live reload script if requested
286
- if (liveReload) {
287
- const liveReloadScript = `
288
- <script>
289
- (function() {
290
- const ws = new WebSocket('ws://localhost:${wsPort}');
291
- ws.onmessage = function(event) {
292
- if (event.data === 'reload') {
293
- console.log('Reloading...');
294
- window.location.reload();
160
+ throw error;
295
161
  }
296
- };
297
- ws.onclose = function() {
298
- console.log('Live reload disconnected. Retrying...');
299
- setTimeout(function() { window.location.reload(); }, 1000);
300
- };
301
- })();
302
- </script>`;
303
- html = html.replace("</body>", `${liveReloadScript}\n</body>`);
304
162
  }
305
163
 
306
- // Add DOCTYPE
307
- const fullHtml = `<!DOCTYPE html>\n${html}`;
308
-
309
- // Output to specified directory
310
- const outDir = path.resolve(process.cwd(), outputDir);
311
- await fs.mkdir(outDir, { recursive: true });
312
-
313
- // Output file name - preserve directory structure if pagesDir is specified
314
- let outputPath;
315
- let relativeOutput;
316
-
317
- if (pagesDir) {
318
- // Preserve the directory structure from pages folder
319
- const pagesDirAbs = path.resolve(process.cwd(), pagesDir);
320
- const relativePath = path.relative(pagesDirAbs, absolutePath);
321
- const outputRelative = relativePath.replace(/\.jsx$/, ".html");
322
- outputPath = path.join(outDir, outputRelative);
323
-
324
- // Create subdirectories if needed
325
- await fs.mkdir(path.dirname(outputPath), { recursive: true });
326
-
327
- relativeOutput = path.relative(process.cwd(), outputPath);
164
+ // Watch for changes
165
+ if (inputStat.isDirectory()) {
166
+ await watchFiles(input, {
167
+ outputDir,
168
+ unocssConfig,
169
+ wss,
170
+ onRebuild: async () => {
171
+ await copyPublicFiles(outputDir);
172
+ },
173
+ });
328
174
  } else {
329
- // Single file mode - just use basename
330
- const inputBasename = path.basename(inputFile, ".jsx");
331
- const outputFilename = `${inputBasename}.html`;
332
- outputPath = path.join(outDir, outputFilename);
333
- relativeOutput = path.relative(process.cwd(), outputPath);
334
- }
335
-
336
- await fs.writeFile(outputPath, fullHtml);
337
-
338
- // Clean up temp file
339
- await fs.unlink(tmpFile);
340
-
341
- if (!silent) {
342
- console.log(`āœ“ Built successfully: ${relativeOutput}`);
343
- }
344
-
345
- return { outputPath, html: fullHtml };
175
+ await watchFile(input, {
176
+ outputDir,
177
+ unocssConfig,
178
+ wss,
179
+ onRebuild: async () => {
180
+ await copyPublicFiles(outputDir);
181
+ },
182
+ });
183
+ }
184
+
185
+ console.log(`\nšŸš€ Server running at http://localhost:${serverPort}`);
186
+ console.log(`šŸ“ Serving: ${input}/ → ${outputDir}/`);
346
187
  }
347
188
 
348
- async function main() {
349
- const args = process.argv.slice(2);
350
- const opts = parseArgs(args);
351
189
 
352
- // Show version
353
- if (opts.version) {
354
- const pkg = JSON.parse(
355
- await fs.readFile(new URL("../package.json", import.meta.url), "utf-8")
356
- );
357
- console.log(`ono v${pkg.version}`);
358
- process.exit(0);
359
- }
360
190
 
361
- // Show help
362
- if (args.length === 0 || opts.help) {
363
- console.log(`
364
- Ono - A lightweight JSX library for static site generation
191
+ function showHelp() {
192
+ console.log(`
193
+ Ono - Minimalist SSG Framework
365
194
 
366
195
  Usage:
367
- ono init [dir] Initialize a new Ono project
368
- ono build <file|dir> [options] Build JSX file(s) to HTML
369
- ono dev <file|dir> [options] Start dev server with live reload
196
+ ono <command> [options]
370
197
 
371
- Arguments:
372
- dir Project directory (default: current directory)
373
- file Single JSX file to build/serve
374
- pages Pages directory (default: pages/)
198
+ Commands:
199
+ build [input] Build JSX files to static HTML
200
+ input: file or directory (default: pages)
201
+
202
+ dev [input] Build, watch, and serve with live reload
203
+ input: file or directory (default: pages)
375
204
 
376
205
  Options:
377
- -w, --watch Watch for changes and rebuild (build only)
378
- -p, --port <port> Port number (default: 3000) (dev only)
379
- -o, --output <dir> Output directory (default: dist)
380
- -h, --help Show this help message
381
- -v, --version Show version number
206
+ --output <dir> Output directory (default: dist)
207
+ --port <number> Server port (default: 3000)
208
+ --help Show this help message
382
209
 
383
210
  Examples:
384
- # Initialize new project
385
- ono init
386
- ono init my-project
387
-
388
- # Single file mode
389
- ono build example/index.jsx
390
- ono build example/index.jsx --watch
391
- ono dev example/index.jsx
392
-
393
- # Pages mode (build all .jsx files in directory)
394
- ono build pages
395
- ono build pages --watch
396
- ono dev pages
397
- ono dev # Same as: ono dev pages
398
-
399
- # Custom options
400
- ono build pages -o public
401
- ono dev pages -p 8080
402
- `);
403
- process.exit(0);
404
- }
405
-
406
- const command = opts._[0];
407
- const inputFile = opts._[1];
408
-
409
- if (command === "init") {
410
- const projectDir = inputFile || ".";
411
- const projectPath = path.resolve(process.cwd(), projectDir);
412
-
413
- try {
414
- // Check if directory exists and is not empty
415
- try {
416
- const files = await fs.readdir(projectPath);
417
- if (files.length > 0 && projectDir !== ".") {
418
- console.error(`Error: Directory ${projectDir} is not empty`);
419
- process.exit(1);
420
- }
421
- } catch (error) {
422
- if (error.code === "ENOENT") {
423
- // Directory doesn't exist, create it
424
- await fs.mkdir(projectPath, { recursive: true });
425
- } else {
426
- throw error;
427
- }
428
- }
429
-
430
- console.log(`\nšŸš€ Initializing Ono project in ${projectDir === "." ? "current directory" : projectDir}...\n`);
431
-
432
- // Create directory structure
433
- await fs.mkdir(path.join(projectPath, "pages"), { recursive: true });
434
- await fs.mkdir(path.join(projectPath, "components"), { recursive: true });
435
- await fs.mkdir(path.join(projectPath, "public", "css"), { recursive: true });
436
-
437
- // Create Layout.jsx
438
- const layoutContent = `// Layout component with slot support
439
- export default function Layout(props) {
440
- return (
441
- <html lang="en">
442
- <head>
443
- <meta charset="UTF-8" />
444
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
445
- <title>{props.title || "Ono Site"}</title>
446
- <link rel="stylesheet" href="/uno.css" />
447
- <link rel="stylesheet" href="/css/style.css" />
448
- </head>
449
- <body class="font-sans max-w-800px mx-auto px-8 py-8 leading-relaxed">
450
- <header class="mb-8 pb-4 border-b-2 border-gray-200">
451
- <h1 class="text-3xl font-bold">{props.title}</h1>
452
- {props.header}
453
- </header>
454
- <main>
455
- {props.children}
456
- </main>
457
- <footer class="mt-12 pt-4 border-t border-gray-200 text-secondary text-sm">
458
- {props.footer || <p>Ā© 2025 Ono</p>}
459
- </footer>
460
- </body>
461
- </html>
462
- );
463
- }
464
- `;
465
- await fs.writeFile(path.join(projectPath, "components", "Layout.jsx"), layoutContent);
466
-
467
- // Create Hello.jsx component
468
- const helloContent = `export default function Hello(props) {
469
- return (
470
- <div class="p-6 bg-blue-50 rounded-lg border-2 border-blue-200">
471
- <h2 class="text-2xl font-semibold text-blue-800 mb-2">
472
- Hello, {props.name || "World"}!
473
- </h2>
474
- <p class="text-blue-600">
475
- This is a reusable component. Edit components/Hello.jsx to customize it.
476
- </p>
477
- </div>
478
- );
211
+ ono build Build all pages in pages/ directory
212
+ ono build pages/index.jsx Build a single file
213
+ ono dev Start dev server with live reload
214
+ ono dev --port 8080 Start dev server on port 8080
215
+ `);
479
216
  }
480
- `;
481
- await fs.writeFile(path.join(projectPath, "components", "Hello.jsx"), helloContent);
482
-
483
- // Create index.jsx
484
- const indexContent = `import Layout from "../components/Layout.jsx";
485
- import Hello from "../components/Hello.jsx";
486
-
487
- export default function Home() {
488
- return (
489
- <Layout title="Welcome to Ono">
490
- <Hello name="Developer" />
491
-
492
- <div class="mt-8 space-y-6">
493
- <section>
494
- <h2 class="text-2xl font-bold mb-4">Getting Started</h2>
495
- <p class="mb-4 text-lg">
496
- You've successfully initialized an Ono project! Here's what you can do next:
497
- </p>
498
- <ul class="list-disc ml-6 space-y-2">
499
- <li>Edit <code class="bg-gray-100 px-2 py-1 rounded text-sm">pages/index.jsx</code> to customize this page</li>
500
- <li>Create new pages in the <code class="bg-gray-100 px-2 py-1 rounded text-sm">pages/</code> directory</li>
501
- <li>Add components to <code class="bg-gray-100 px-2 py-1 rounded text-sm">components/</code></li>
502
- <li>Put static assets in <code class="bg-gray-100 px-2 py-1 rounded text-sm">public/</code></li>
503
- </ul>
504
- </section>
505
-
506
- <section>
507
- <h2 class="text-2xl font-bold mb-4">Commands</h2>
508
- <div class="space-y-2">
509
- <div class="p-4 bg-gray-50 rounded border border-gray-200">
510
- <code class="text-green-600 font-semibold">ono dev</code>
511
- <p class="text-sm text-secondary mt-1">Start development server with live reload</p>
512
- </div>
513
- <div class="p-4 bg-gray-50 rounded border border-gray-200">
514
- <code class="text-green-600 font-semibold">ono build pages</code>
515
- <p class="text-sm text-secondary mt-1">Build your site to the dist/ directory</p>
516
- </div>
517
- </div>
518
- </section>
519
-
520
- <section>
521
- <h2 class="text-2xl font-bold mb-4">Features</h2>
522
- <div class="grid grid-cols-2 gap-4">
523
- <div class="p-4 border border-gray-200 rounded">
524
- <h3 class="font-semibold mb-2">šŸŽØ UnoCSS</h3>
525
- <p class="text-sm text-secondary">Atomic CSS with Tailwind-compatible utilities</p>
526
- </div>
527
- <div class="p-4 border border-gray-200 rounded">
528
- <h3 class="font-semibold mb-2">⚔ Live Reload</h3>
529
- <p class="text-sm text-secondary">Instant updates during development</p>
530
- </div>
531
- <div class="p-4 border border-gray-200 rounded">
532
- <h3 class="font-semibold mb-2">šŸ“¦ Component Based</h3>
533
- <p class="text-sm text-secondary">Reusable JSX components</p>
534
- </div>
535
- <div class="p-4 border border-gray-200 rounded">
536
- <h3 class="font-semibold mb-2">šŸš€ Static Output</h3>
537
- <p class="text-sm text-secondary">Fast, SEO-friendly HTML</p>
538
- </div>
539
- </div>
540
- </section>
541
- </div>
542
- </Layout>
543
- );
544
- }
545
- `;
546
- await fs.writeFile(path.join(projectPath, "pages", "index.jsx"), indexContent);
547
217
 
548
- // Create style.css
549
- const styleContent = `/* Custom styles for your Ono site */
550
- /* UnoCSS utilities will be automatically generated in uno.css */
218
+ // Main CLI handler
219
+ (async () => {
220
+ try {
221
+ if (!command || command === "--help" || command === "-h") {
222
+ showHelp();
223
+ process.exit(0);
224
+ }
551
225
 
552
- body {
553
- -webkit-font-smoothing: antialiased;
554
- -moz-osx-font-smoothing: grayscale;
555
- }
226
+ switch (command) {
227
+ case "build":
228
+ await runBuildCommand();
229
+ break;
556
230
 
557
- /* Add your custom styles here */
558
- `;
559
- await fs.writeFile(path.join(projectPath, "public", "css", "style.css"), styleContent);
560
-
561
- // Create .gitignore
562
- const gitignoreContent = `# Build output
563
- dist/
564
- .mini-jsx-tmp.js
565
-
566
- # Dependencies
567
- node_modules/
568
-
569
- # Environment variables
570
- .env
571
- .env.local
572
- `;
573
- await fs.writeFile(path.join(projectPath, ".gitignore"), gitignoreContent);
574
-
575
- // Create package.json
576
- const projectName = projectDir === "." ? path.basename(projectPath) : projectDir;
577
- const packageJsonContent = {
578
- name: projectName,
579
- version: "0.1.0",
580
- type: "module",
581
- scripts: {
582
- dev: "ono dev",
583
- build: "ono build pages",
584
- "build:watch": "ono build pages --watch"
585
- },
586
- devDependencies: {}
587
- };
588
- await fs.writeFile(
589
- path.join(projectPath, "package.json"),
590
- JSON.stringify(packageJsonContent, null, 2) + "\n"
591
- );
231
+ case "dev":
232
+ await runDevCommand();
233
+ break;
592
234
 
593
- console.log("āœ… Created project structure:");
594
- console.log(" pages/");
595
- console.log(" ā”œā”€ā”€ index.jsx");
596
- console.log(" components/");
597
- console.log(" ā”œā”€ā”€ Layout.jsx");
598
- console.log(" ā”œā”€ā”€ Hello.jsx");
599
- console.log(" public/");
600
- console.log(" ā”œā”€ā”€ css/");
601
- console.log(" │ └── style.css");
602
- console.log(" package.json");
603
- console.log(" .gitignore");
604
- console.log("");
605
- console.log("šŸŽ‰ Project initialized successfully!");
606
- console.log("");
607
- console.log("Next steps:");
608
- if (projectDir !== ".") {
609
- console.log(` cd ${projectDir}`);
610
- }
611
- console.log(" npm run dev # Start development server");
612
- console.log(" npm run build # Build for production");
613
- console.log("");
614
- console.log("Or use ono directly:");
615
- console.log(" ono dev # Start development server");
616
- console.log(" ono build pages # Build for production");
617
- console.log("");
618
- } catch (error) {
619
- console.error(`Error: ${error.message}`);
620
- if (error.stack) {
621
- console.error(error.stack);
622
- }
623
- process.exit(1);
624
- }
625
- } else if (command === "build") {
626
- const buildOptions = {
627
- outputDir: opts.output || "dist",
628
- };
629
-
630
- try {
631
- // Check if inputFile is a directory (pages mode)
632
- const isDirectory = inputFile && (await fs.stat(path.resolve(process.cwd(), inputFile)).catch(() => null))?.isDirectory();
633
-
634
- if (isDirectory || inputFile === "pages" || !inputFile) {
635
- // Pages mode - build all pages in directory
636
- const pagesDir = inputFile || "pages";
637
- const pagesDirAbs = path.resolve(process.cwd(), pagesDir);
638
-
639
- const pages = await discoverPages(pagesDirAbs);
640
-
641
- if (pages.length === 0) {
642
- console.error(`Error: No JSX files found in ${pagesDir}/`);
643
- process.exit(1);
644
- }
645
-
646
- console.log(`Found ${pages.length} page(s) in ${pagesDir}/\n`);
647
-
648
- if (opts.watch) {
649
- // Initial build all pages
650
- for (const page of pages) {
651
- await buildFile(page, { ...buildOptions, pagesDir: pagesDirAbs });
652
- }
653
-
654
- // Copy public files
655
- const publicCount = await copyPublicFiles("public", buildOptions.outputDir);
656
- if (publicCount > 0) {
657
- console.log(`\nšŸ“¦ Copied ${publicCount} file(s) from public/\n`);
658
- }
659
-
660
- // Generate UnoCSS
661
- await generateUnoCSSFile(buildOptions.outputDir);
662
-
663
- console.log(`šŸ‘€ Watching for changes in ${pagesDir}/ and public/...`);
664
- console.log("Press Ctrl+C to stop\n");
665
-
666
- const { watch } = await import("node:fs");
667
-
668
- // Watch pages directory
669
- watch(pagesDirAbs, { recursive: true }, async (_eventType, filename) => {
670
- if (filename && filename.endsWith(".jsx")) {
671
- const changedFile = path.join(pagesDirAbs, filename);
672
- try {
673
- await buildFile(changedFile, { ...buildOptions, pagesDir: pagesDirAbs, silent: true });
674
- await generateUnoCSSFile(buildOptions.outputDir, true);
675
- console.log(`āœ“ Rebuilt: ${filename}`);
676
- } catch (error) {
677
- console.error(`āœ— Build error: ${error.message}`);
678
- }
679
- }
680
- });
681
-
682
- // Watch public directory
683
- const publicDirAbs = path.resolve(process.cwd(), "public");
684
- try {
685
- await fs.access(publicDirAbs);
686
- watch(publicDirAbs, { recursive: true }, async (_eventType, filename) => {
687
- if (filename) {
688
- try {
689
- await copyPublicFiles("public", buildOptions.outputDir, true);
690
- console.log(`āœ“ Copied: public/${filename}`);
691
- } catch (error) {
692
- console.error(`āœ— Copy error: ${error.message}`);
693
- }
694
- }
695
- });
696
- } catch {
697
- // Public directory doesn't exist, skip watching
698
- }
699
-
700
- process.on("SIGINT", () => {
701
- console.log("\n\nšŸ‘‹ Shutting down...");
702
- process.exit(0);
703
- });
704
- } else {
705
- // Build all pages
706
- for (const page of pages) {
707
- await buildFile(page, { ...buildOptions, pagesDir: pagesDirAbs });
708
- }
709
-
710
- // Copy public files
711
- const publicCount = await copyPublicFiles("public", buildOptions.outputDir);
712
- if (publicCount > 0) {
713
- console.log(`\nšŸ“¦ Copied ${publicCount} file(s) from public/`);
714
- }
715
-
716
- // Generate UnoCSS
717
- await generateUnoCSSFile(buildOptions.outputDir);
718
- }
719
- } else if (inputFile) {
720
- // Single file mode
721
- if (opts.watch) {
722
- // Initial build
723
- await buildFile(inputFile, buildOptions);
724
-
725
- // Generate UnoCSS
726
- await generateUnoCSSFile(buildOptions.outputDir);
727
-
728
- // Watch for changes
729
- const absolutePath = path.resolve(process.cwd(), inputFile);
730
- const watchDir = path.dirname(absolutePath);
731
-
732
- console.log(`\nšŸ‘€ Watching for changes in ${watchDir}...`);
733
- console.log("Press Ctrl+C to stop\n");
734
-
735
- const { watch } = await import("node:fs");
736
- watch(watchDir, { recursive: true }, async (_eventType, filename) => {
737
- if (filename && filename.endsWith(".jsx")) {
738
- try {
739
- await buildFile(inputFile, { ...buildOptions, silent: true });
740
- await generateUnoCSSFile(buildOptions.outputDir, true);
741
- console.log(`āœ“ Rebuilt: ${filename}`);
742
- } catch (error) {
743
- console.error(`āœ— Build error: ${error.message}`);
744
- }
745
- }
746
- });
747
-
748
- process.on("SIGINT", () => {
749
- console.log("\n\nšŸ‘‹ Shutting down...");
750
- process.exit(0);
751
- });
752
- } else {
753
- // Single build
754
- await buildFile(inputFile, buildOptions);
755
-
756
- // Generate UnoCSS
757
- await generateUnoCSSFile(buildOptions.outputDir);
758
- }
759
- } else {
760
- console.error("Error: Please specify a file or pages directory to build");
761
- console.error("Usage: mini-jsx build <file|pages> [options]");
235
+ default:
236
+ console.error(`Unknown command: ${command}`);
237
+ console.error('Run "ono --help" for usage information');
762
238
  process.exit(1);
763
- }
764
- } catch (error) {
765
- console.error(`Error: ${error.message}`);
766
- if (error.stack) {
767
- console.error(error.stack);
768
- }
769
- process.exit(1);
770
239
  }
771
- } else if (command === "dev") {
772
- const requestedPort = opts.port || 3000;
773
- const outputDir = opts.output || "dist";
774
-
775
- try {
776
- // Check if inputFile is a directory (pages mode)
777
- const isDirectory = inputFile && (await fs.stat(path.resolve(process.cwd(), inputFile)).catch(() => null))?.isDirectory();
778
- const isPages = isDirectory || inputFile === "pages" || !inputFile;
779
-
780
- // Find available ports
781
- const wsPort = await findAvailablePort(35729);
782
- const httpPort = await findAvailablePort(requestedPort);
783
-
784
- if (wsPort !== 35729) {
785
- console.log(`ā„¹ļø WebSocket port 35729 is busy, using port ${wsPort} instead`);
786
- }
787
- if (httpPort !== requestedPort) {
788
- console.log(`ā„¹ļø Port ${requestedPort} is busy, using port ${httpPort} instead`);
789
- }
790
-
791
- if (isPages) {
792
- // Pages mode
793
- const pagesDir = inputFile || "pages";
794
- const pagesDirAbs = path.resolve(process.cwd(), pagesDir);
795
- const pages = await discoverPages(pagesDirAbs);
796
-
797
- if (pages.length === 0) {
798
- console.error(`Error: No JSX files found in ${pagesDir}/`);
799
- process.exit(1);
800
- }
801
-
802
- console.log(`Found ${pages.length} page(s) in ${pagesDir}/\n`);
803
-
804
- // Initial build all pages
805
- for (const page of pages) {
806
- await buildFile(page, { liveReload: true, wsPort, outputDir, pagesDir: pagesDirAbs });
807
- }
808
-
809
- // Copy public files
810
- const publicCount = await copyPublicFiles("public", outputDir);
811
- if (publicCount > 0) {
812
- console.log(`\nšŸ“¦ Copied ${publicCount} file(s) from public/\n`);
813
- }
814
-
815
- // Generate UnoCSS
816
- await generateUnoCSSFile(outputDir);
817
-
818
- // Setup WebSocket server
819
- const wss = new WebSocketServer({ port: wsPort });
820
- const clients = new Set();
821
-
822
- wss.on("connection", (ws) => {
823
- clients.add(ws);
824
- ws.on("close", () => clients.delete(ws));
825
- });
826
-
827
- function notifyClients() {
828
- clients.forEach((client) => {
829
- if (client.readyState === 1) {
830
- client.send("reload");
831
- }
832
- });
833
- }
834
-
835
- // Watch for changes
836
- console.log(`šŸ‘€ Watching for changes in ${pagesDir}/ and public/...`);
837
-
838
- const { watch } = await import("node:fs");
839
-
840
- // Watch pages directory
841
- watch(pagesDirAbs, { recursive: true }, async (_eventType, filename) => {
842
- if (filename && filename.endsWith(".jsx")) {
843
- const changedFile = path.join(pagesDirAbs, filename);
844
- try {
845
- await buildFile(changedFile, { liveReload: true, silent: true, wsPort, outputDir, pagesDir: pagesDirAbs });
846
- await generateUnoCSSFile(outputDir, true);
847
- console.log(`āœ“ Rebuilt: ${filename}`);
848
- notifyClients();
849
- } catch (error) {
850
- console.error(`āœ— Build error: ${error.message}`);
851
- }
852
- }
853
- });
854
-
855
- // Watch public directory
856
- const publicDirAbs = path.resolve(process.cwd(), "public");
857
- try {
858
- await fs.access(publicDirAbs);
859
- watch(publicDirAbs, { recursive: true }, async (_eventType, filename) => {
860
- if (filename) {
861
- try {
862
- await copyPublicFiles("public", outputDir, true);
863
- console.log(`āœ“ Copied: public/${filename}`);
864
- notifyClients();
865
- } catch (error) {
866
- console.error(`āœ— Copy error: ${error.message}`);
867
- }
868
- }
869
- });
870
- } catch {
871
- // Public directory doesn't exist, skip watching
872
- }
873
-
874
- // Create HTTP server
875
- const outDir = path.resolve(process.cwd(), outputDir);
876
-
877
- const server = http.createServer(async (req, res) => {
878
- let requestPath = req.url === "/" ? "/index.html" : req.url;
879
- let filePath = path.join(outDir, requestPath);
880
-
881
- try {
882
- const content = await fs.readFile(filePath);
883
- const ext = path.extname(filePath);
884
- const contentTypes = {
885
- ".html": "text/html",
886
- ".css": "text/css",
887
- ".js": "text/javascript",
888
- ".json": "application/json",
889
- ".png": "image/png",
890
- ".jpg": "image/jpeg",
891
- ".gif": "image/gif",
892
- ".svg": "image/svg+xml",
893
- };
894
-
895
- res.writeHead(200, { "Content-Type": contentTypes[ext] || "text/plain" });
896
- res.end(content);
897
- } catch (error) {
898
- res.writeHead(404);
899
- res.end("Not found");
900
- }
901
- });
902
-
903
- server.listen(httpPort, () => {
904
- console.log(`\nšŸš€ Server running at http://localhost:${httpPort}`);
905
- console.log(`šŸ“ Serving: ${pagesDir}/ → ${outputDir}/\n`);
906
- });
907
- } else {
908
- // Single file mode
909
- if (!inputFile) {
910
- console.error("Error: Please specify a file or pages directory to serve");
911
- console.error("Usage: mini-jsx dev <file|pages> [options]");
912
- process.exit(1);
913
- }
914
-
915
- // Initial build with live reload
916
- await buildFile(inputFile, { liveReload: true, wsPort, outputDir });
917
-
918
- // Generate UnoCSS
919
- await generateUnoCSSFile(outputDir);
920
-
921
- // Setup WebSocket server for live reload
922
- const wss = new WebSocketServer({ port: wsPort });
923
- const clients = new Set();
924
-
925
- wss.on("connection", (ws) => {
926
- clients.add(ws);
927
- ws.on("close", () => clients.delete(ws));
928
- });
929
-
930
- function notifyClients() {
931
- clients.forEach((client) => {
932
- if (client.readyState === 1) {
933
- client.send("reload");
934
- }
935
- });
936
- }
937
-
938
- // Watch for changes
939
- const absolutePath = path.resolve(process.cwd(), inputFile);
940
- const watchDir = path.dirname(absolutePath);
941
-
942
- console.log(`\nšŸ‘€ Watching for changes in ${watchDir}...`);
943
-
944
- const { watch } = await import("node:fs");
945
- watch(watchDir, { recursive: true }, async (_eventType, filename) => {
946
- if (filename && filename.endsWith(".jsx")) {
947
- try {
948
- await buildFile(inputFile, { liveReload: true, silent: true, wsPort, outputDir });
949
- await generateUnoCSSFile(outputDir, true);
950
- console.log(`āœ“ Rebuilt: ${filename}`);
951
- notifyClients();
952
- } catch (error) {
953
- console.error(`āœ— Build error: ${error.message}`);
954
- }
955
- }
956
- });
957
-
958
- // Create HTTP server
959
- const outDir = path.resolve(process.cwd(), outputDir);
960
- const inputBasename = path.basename(inputFile, ".jsx");
961
- const outputFilename = `${inputBasename}.html`;
962
- const relativeOutput = path.relative(process.cwd(), path.join(outDir, outputFilename));
963
-
964
- const server = http.createServer(async (req, res) => {
965
- let filePath = path.join(outDir, req.url === "/" ? outputFilename : req.url);
966
-
967
- try {
968
- const content = await fs.readFile(filePath);
969
- const ext = path.extname(filePath);
970
- const contentTypes = {
971
- ".html": "text/html",
972
- ".css": "text/css",
973
- ".js": "text/javascript",
974
- ".json": "application/json",
975
- ".png": "image/png",
976
- ".jpg": "image/jpeg",
977
- ".gif": "image/gif",
978
- ".svg": "image/svg+xml",
979
- };
980
-
981
- res.writeHead(200, { "Content-Type": contentTypes[ext] || "text/plain" });
982
- res.end(content);
983
- } catch (error) {
984
- res.writeHead(404);
985
- res.end("Not found");
986
- }
987
- });
988
-
989
- server.listen(httpPort, () => {
990
- console.log(`\nšŸš€ Server running at http://localhost:${httpPort}`);
991
- console.log(`šŸ“ Serving: ${relativeOutput}\n`);
992
- });
993
- }
994
-
995
- // Keep process running
996
- process.on("SIGINT", () => {
997
- console.log("\n\nšŸ‘‹ Shutting down...");
998
- process.exit(0);
999
- });
1000
- } catch (error) {
1001
- console.error(`Error: ${error.message}`);
1002
- if (error.stack) {
1003
- console.error(error.stack);
1004
- }
1005
- process.exit(1);
240
+ } catch (error) {
241
+ console.error("Error:", error.message);
242
+ if (process.env.DEBUG) {
243
+ console.error(error);
1006
244
  }
1007
- } else {
1008
- console.error(`Unknown command: ${command}`);
1009
- console.error('Run "mini-jsx --help" for usage information');
1010
245
  process.exit(1);
1011
246
  }
1012
- }
1013
-
1014
- main();
247
+ })();