@hashrock/ono 0.1.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/src/cli.js ADDED
@@ -0,0 +1,1014 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * CLI Tool for mini-jsx
5
+ */
6
+
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
+
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
+ }
45
+
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
+ }
72
+ }
73
+
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 });
87
+
88
+ for (const entry of entries) {
89
+ const fullPath = path.join(dir, entry.name);
90
+
91
+ if (entry.isDirectory()) {
92
+ await walk(fullPath);
93
+ } else if (entry.isFile() && entry.name.endsWith(".jsx")) {
94
+ pages.push(fullPath);
95
+ }
96
+ }
97
+ }
98
+
99
+ try {
100
+ await walk(pagesDir);
101
+ return pages;
102
+ } catch (error) {
103
+ if (error.code === "ENOENT") {
104
+ return [];
105
+ }
106
+ throw error;
107
+ }
108
+ }
109
+
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);
118
+
119
+ // Find all HTML files
120
+ const htmlFiles = [];
121
+
122
+ async function findHTMLFiles(dir) {
123
+ const entries = await fs.readdir(dir, { withFileTypes: true });
124
+
125
+ for (const entry of entries) {
126
+ const fullPath = path.join(dir, entry.name);
127
+
128
+ if (entry.isDirectory()) {
129
+ await findHTMLFiles(fullPath);
130
+ } else if (entry.isFile() && entry.name.endsWith(".html")) {
131
+ htmlFiles.push(fullPath);
132
+ }
133
+ }
134
+ }
135
+
136
+ try {
137
+ await findHTMLFiles(outputDirAbs);
138
+
139
+ if (htmlFiles.length === 0) {
140
+ return false;
141
+ }
142
+
143
+ // Load UnoCSS config
144
+ const unoConfig = await loadUnoConfig(path.resolve(process.cwd(), "uno.config.js"));
145
+
146
+ // Generate CSS from all HTML files
147
+ const css = await generateCSSFromFiles(htmlFiles, unoConfig);
148
+
149
+ if (css) {
150
+ // Write CSS file
151
+ const cssPath = path.join(outputDirAbs, "uno.css");
152
+ await fs.writeFile(cssPath, css);
153
+
154
+ if (!silent) {
155
+ console.log(`\n⚔ Generated UnoCSS: uno.css`);
156
+ }
157
+
158
+ return true;
159
+ }
160
+
161
+ return false;
162
+ } catch (error) {
163
+ if (!silent) {
164
+ console.error(`Warning: UnoCSS generation failed: ${error.message}`);
165
+ }
166
+ return false;
167
+ }
168
+ }
169
+
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
+
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
+
189
+ let fileCount = 0;
190
+
191
+ async function copyRecursive(src, dest) {
192
+ const entries = await fs.readdir(src, { withFileTypes: true });
193
+
194
+ for (const entry of entries) {
195
+ const srcPath = path.join(src, entry.name);
196
+ const destPath = path.join(dest, entry.name);
197
+
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
+ }
210
+ }
211
+
212
+ await copyRecursive(publicDirAbs, outputDirAbs);
213
+ return fileCount;
214
+ }
215
+
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);
230
+
231
+ if (!silent) {
232
+ console.log(`Building ${inputFile}...`);
233
+ }
234
+
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));
250
+ } 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();
295
+ }
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
+ }
305
+
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);
328
+ } 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 };
346
+ }
347
+
348
+ async function main() {
349
+ const args = process.argv.slice(2);
350
+ const opts = parseArgs(args);
351
+
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
+
361
+ // Show help
362
+ if (args.length === 0 || opts.help) {
363
+ console.log(`
364
+ Ono - A lightweight JSX library for static site generation
365
+
366
+ 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
370
+
371
+ Arguments:
372
+ dir Project directory (default: current directory)
373
+ file Single JSX file to build/serve
374
+ pages Pages directory (default: pages/)
375
+
376
+ 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
382
+
383
+ 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
+ );
479
+ }
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
+
548
+ // Create style.css
549
+ const styleContent = `/* Custom styles for your Ono site */
550
+ /* UnoCSS utilities will be automatically generated in uno.css */
551
+
552
+ body {
553
+ -webkit-font-smoothing: antialiased;
554
+ -moz-osx-font-smoothing: grayscale;
555
+ }
556
+
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
+ );
592
+
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]");
762
+ 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
+ }
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);
1006
+ }
1007
+ } else {
1008
+ console.error(`Unknown command: ${command}`);
1009
+ console.error('Run "mini-jsx --help" for usage information');
1010
+ process.exit(1);
1011
+ }
1012
+ }
1013
+
1014
+ main();