vaderjs 2.3.16 → 2.3.18
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/config/index.ts +2 -2
- package/main.ts +388 -113
- package/package.json +1 -1
package/config/index.ts
CHANGED
package/main.ts
CHANGED
|
@@ -5,6 +5,7 @@ import fs from "fs/promises";
|
|
|
5
5
|
import fsSync from "fs";
|
|
6
6
|
import path from "path";
|
|
7
7
|
import { initProject } from "vaderjs/cli";
|
|
8
|
+
import { rimraf } from "rimraf";
|
|
8
9
|
|
|
9
10
|
const colors = {
|
|
10
11
|
reset: "\x1b[0m",
|
|
@@ -58,7 +59,6 @@ const VADER_SRC_PATH = path.join(
|
|
|
58
59
|
const TEMP_SRC_DIR = path.join(PROJECT_ROOT, ".vader_temp_src");
|
|
59
60
|
|
|
60
61
|
let config: any = {};
|
|
61
|
-
let htmlInjections: string[] = [];
|
|
62
62
|
|
|
63
63
|
// --- Plugin Support ---
|
|
64
64
|
|
|
@@ -72,7 +72,7 @@ interface Plugin {
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
interface PluginAPI {
|
|
75
|
-
injectHTML(html: string): void;
|
|
75
|
+
injectHTML(html: string, id?: string): void;
|
|
76
76
|
addWatchPath(path: string): void;
|
|
77
77
|
config: any;
|
|
78
78
|
isDev: boolean;
|
|
@@ -84,11 +84,72 @@ interface PluginAPI {
|
|
|
84
84
|
|
|
85
85
|
let plugins: Plugin[] = [];
|
|
86
86
|
|
|
87
|
+
// Track HTML injections with IDs to prevent duplicates
|
|
88
|
+
class HTMLInjectionManager {
|
|
89
|
+
private injections: Map<string, string> = new Map();
|
|
90
|
+
|
|
91
|
+
add(html: string, id?: string): void {
|
|
92
|
+
// Generate an ID if not provided (based on content hash)
|
|
93
|
+
const injectionId = id || this.generateId(html);
|
|
94
|
+
|
|
95
|
+
// Only add if not already present
|
|
96
|
+
if (!this.injections.has(injectionId)) {
|
|
97
|
+
this.injections.set(injectionId, html);
|
|
98
|
+
logger.info(`Added HTML injection: ${injectionId}`);
|
|
99
|
+
} else {
|
|
100
|
+
logger.info(`Skipping duplicate HTML injection: ${injectionId}`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private generateId(html: string): string {
|
|
105
|
+
// Simple ID generation based on content
|
|
106
|
+
// For CSS links, extract the href
|
|
107
|
+
const hrefMatch = html.match(/href=["']([^"']+)["']/);
|
|
108
|
+
if (hrefMatch) {
|
|
109
|
+
return `link:${hrefMatch[1]}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// For script tags, extract the src
|
|
113
|
+
const srcMatch = html.match(/src=["']([^"']+)["']/);
|
|
114
|
+
if (srcMatch) {
|
|
115
|
+
return `script:${srcMatch[1]}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// For meta tags, extract name or property
|
|
119
|
+
const nameMatch = html.match(/(?:name|property)=["']([^"']+)["']/);
|
|
120
|
+
if (nameMatch) {
|
|
121
|
+
return `meta:${nameMatch[1]}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Fallback to hash of content
|
|
125
|
+
let hash = 0;
|
|
126
|
+
for (let i = 0; i < html.length; i++) {
|
|
127
|
+
hash = ((hash << 5) - hash) + html.charCodeAt(i);
|
|
128
|
+
hash |= 0;
|
|
129
|
+
}
|
|
130
|
+
return `injection:${Math.abs(hash)}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
getAll(): string[] {
|
|
134
|
+
return Array.from(this.injections.values());
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
clear(): void {
|
|
138
|
+
this.injections.clear();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
has(id: string): boolean {
|
|
142
|
+
return this.injections.has(id);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const htmlInjectionManager = new HTMLInjectionManager();
|
|
147
|
+
|
|
87
148
|
// Create plugin API helper
|
|
88
149
|
function createPluginAPI(): PluginAPI {
|
|
89
150
|
return {
|
|
90
|
-
injectHTML: (html: string) => {
|
|
91
|
-
|
|
151
|
+
injectHTML: (html: string, id?: string) => {
|
|
152
|
+
htmlInjectionManager.add(html, id);
|
|
92
153
|
},
|
|
93
154
|
addWatchPath: (watchPath: string) => {
|
|
94
155
|
if (fsSync.existsSync(watchPath)) {
|
|
@@ -257,33 +318,65 @@ async function ensureJSConfig() {
|
|
|
257
318
|
class FileWatcher {
|
|
258
319
|
watchers = new Map<string, any>();
|
|
259
320
|
callbacks: ((file: string) => void)[] = [];
|
|
321
|
+
private debounceTimer: NodeJS.Timeout | null = null;
|
|
322
|
+
private pendingFiles = new Set<string>();
|
|
323
|
+
private rebuildInProgress = false;
|
|
260
324
|
|
|
261
325
|
watch(dir: string) {
|
|
262
326
|
if (!fsSync.existsSync(dir)) return;
|
|
263
327
|
|
|
264
|
-
|
|
265
|
-
|
|
328
|
+
try {
|
|
329
|
+
const watcher = fsSync.watch(dir, { recursive: true }, (event, filename) => {
|
|
330
|
+
if (!filename) return;
|
|
266
331
|
|
|
267
|
-
|
|
332
|
+
const file = path.join(dir, filename);
|
|
268
333
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
334
|
+
if (
|
|
335
|
+
file.includes("node_modules") ||
|
|
336
|
+
file.includes("dist") ||
|
|
337
|
+
file.includes(".git") ||
|
|
338
|
+
file.includes(".vader_temp_src")
|
|
339
|
+
)
|
|
340
|
+
return;
|
|
275
341
|
|
|
276
|
-
|
|
277
|
-
|
|
342
|
+
// Debounce file changes
|
|
343
|
+
this.pendingFiles.add(file);
|
|
344
|
+
|
|
345
|
+
if (this.debounceTimer) {
|
|
346
|
+
clearTimeout(this.debounceTimer);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
this.debounceTimer = setTimeout(() => {
|
|
350
|
+
const files = Array.from(this.pendingFiles);
|
|
351
|
+
this.pendingFiles.clear();
|
|
352
|
+
|
|
353
|
+
// Only trigger if not already rebuilding
|
|
354
|
+
if (!this.rebuildInProgress) {
|
|
355
|
+
this.callbacks.forEach((cb) => {
|
|
356
|
+
files.forEach(file => cb(file));
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}, 100); // Wait 100ms for multiple file changes
|
|
360
|
+
});
|
|
278
361
|
|
|
279
|
-
|
|
362
|
+
this.watchers.set(dir, watcher);
|
|
363
|
+
} catch (error) {
|
|
364
|
+
logger.warn(`Failed to watch directory ${dir}:`, error);
|
|
365
|
+
}
|
|
280
366
|
}
|
|
281
367
|
|
|
282
368
|
onChange(cb: (file: string) => void) {
|
|
283
369
|
this.callbacks.push(cb);
|
|
284
370
|
}
|
|
371
|
+
|
|
372
|
+
setRebuildStatus(status: boolean) {
|
|
373
|
+
this.rebuildInProgress = status;
|
|
374
|
+
}
|
|
285
375
|
|
|
286
376
|
clear() {
|
|
377
|
+
if (this.debounceTimer) {
|
|
378
|
+
clearTimeout(this.debounceTimer);
|
|
379
|
+
}
|
|
287
380
|
this.watchers.forEach((w) => w.close());
|
|
288
381
|
this.watchers.clear();
|
|
289
382
|
}
|
|
@@ -430,67 +523,120 @@ function findAppFile(): string | null {
|
|
|
430
523
|
return null;
|
|
431
524
|
}
|
|
432
525
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
const
|
|
526
|
+
// Helper to deduplicate HTML injections
|
|
527
|
+
function getUniqueInjections(injections: string[]): string[] {
|
|
528
|
+
const seen = new Set<string>();
|
|
529
|
+
const unique: string[] = [];
|
|
436
530
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
if (appFile) {
|
|
441
|
-
logger.info(`Building App from: ${appFile}`);
|
|
531
|
+
for (const injection of injections) {
|
|
532
|
+
// Create a normalized key for comparison
|
|
533
|
+
let key = injection;
|
|
442
534
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
535
|
+
// For link tags, normalize by href
|
|
536
|
+
const hrefMatch = injection.match(/href=["']([^"']+)["']/);
|
|
537
|
+
if (hrefMatch) {
|
|
538
|
+
key = `link:${hrefMatch[1]}`;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// For script tags, normalize by src
|
|
542
|
+
const srcMatch = injection.match(/src=["']([^"']+)["']/);
|
|
543
|
+
if (srcMatch) {
|
|
544
|
+
key = `script:${srcMatch[1]}`;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// For meta tags, normalize by name/property
|
|
548
|
+
const nameMatch = injection.match(/(?:name|property)=["']([^"']+)["']/);
|
|
549
|
+
if (nameMatch) {
|
|
550
|
+
key = `meta:${nameMatch[1]}`;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (!seen.has(key)) {
|
|
554
|
+
seen.add(key);
|
|
555
|
+
unique.push(injection);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return unique;
|
|
560
|
+
}
|
|
467
561
|
|
|
468
|
-
|
|
469
|
-
|
|
562
|
+
async function buildAppEntrypoints() {
|
|
563
|
+
|
|
564
|
+
// Get unique HTML injections from plugins
|
|
565
|
+
const allInjections = htmlInjectionManager.getAll();
|
|
566
|
+
const uniqueInjections = getUniqueInjections(allInjections);
|
|
567
|
+
const htmlInjectionsString = uniqueInjections.join('\n ');
|
|
568
|
+
|
|
569
|
+
if (uniqueInjections.length > 0) {
|
|
570
|
+
logger.info(`Injecting ${uniqueInjections.length} unique HTML items into page`);
|
|
470
571
|
}
|
|
572
|
+
|
|
471
573
|
|
|
472
|
-
//
|
|
574
|
+
// Handle app directory structure with nested routes
|
|
473
575
|
if (!fsSync.existsSync(APP_DIR)) {
|
|
474
576
|
logger.warn("No App.tsx or app directory found");
|
|
475
577
|
return;
|
|
476
578
|
}
|
|
477
579
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
580
|
+
// Find all index.{tsx,jsx} files in app directory (including nested)
|
|
581
|
+
const entrypoints: { route: string; path: string }[] = [];
|
|
582
|
+
console.log(`Searching for entrypoints in ${APP_DIR}...`);
|
|
583
|
+
function findIndexFiles(dir: string, baseRoute: string = "") {
|
|
584
|
+
const entries = fsSync.readdirSync(dir, { withFileTypes: true });
|
|
585
|
+
console.log(entries)
|
|
586
|
+
|
|
587
|
+
for (const entry of entries) {
|
|
588
|
+
const fullPath = path.join(dir, entry.name);
|
|
589
|
+
const routePath = path.join(baseRoute, entry.name);
|
|
590
|
+
|
|
591
|
+
if (entry.isDirectory()) {
|
|
592
|
+
// Recursively search subdirectories
|
|
593
|
+
findIndexFiles(fullPath, routePath);
|
|
594
|
+
} else if (entry.name.match(/^index\.(tsx|jsx)$/)) {
|
|
595
|
+
// Found an index file
|
|
596
|
+
let route = baseRoute;
|
|
597
|
+
|
|
598
|
+
// Special handling for root index file
|
|
599
|
+
if (route === "") {
|
|
600
|
+
route = "index";
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
entrypoints.push({
|
|
604
|
+
route: route,
|
|
605
|
+
path: fullPath
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
logger.info(`Found route: ${route} -> ${fullPath}`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
findIndexFiles(APP_DIR);
|
|
614
|
+
|
|
615
|
+
if (entrypoints.length === 0) {
|
|
616
|
+
logger.warn("No index.tsx/jsx files found in app directory");
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Build each entrypoint
|
|
621
|
+
for (const entry of entrypoints) {
|
|
622
|
+
// Determine output directory based on route
|
|
623
|
+
let outDir: string;
|
|
624
|
+
let outputName = "index.js";
|
|
491
625
|
|
|
626
|
+
console.log(entry)
|
|
627
|
+
|
|
628
|
+
if (entry.route === "index") {
|
|
629
|
+
// Root route goes to dist root
|
|
630
|
+
outDir = DIST_DIR;
|
|
631
|
+
} else {
|
|
632
|
+
// Nested route goes to corresponding subdirectory
|
|
633
|
+
outDir = path.join(DIST_DIR, entry.route);
|
|
634
|
+
}
|
|
635
|
+
|
|
492
636
|
await fs.mkdir(outDir, { recursive: true });
|
|
493
|
-
|
|
637
|
+
|
|
638
|
+
logger.info(`Building route: ${entry.route} -> ${outDir}`);
|
|
639
|
+
|
|
494
640
|
await build({
|
|
495
641
|
entrypoints: [entry.path],
|
|
496
642
|
outdir: outDir,
|
|
@@ -498,32 +644,65 @@ ${htmlInjectionsString}
|
|
|
498
644
|
jsxFactory: "e",
|
|
499
645
|
jsxFragment: "Fragment",
|
|
500
646
|
jsxImportSource: "vaderjs",
|
|
647
|
+
naming: outputName,
|
|
501
648
|
external: [], // Bundle everything for website
|
|
502
649
|
});
|
|
503
|
-
|
|
650
|
+
|
|
651
|
+
// Generate HTML file for this route
|
|
504
652
|
const html = `<!DOCTYPE html>
|
|
505
653
|
<html>
|
|
506
654
|
<head>
|
|
507
655
|
<meta charset="UTF-8"/>
|
|
508
656
|
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
|
509
|
-
<title>${config.title || 'Vader App'}</title>
|
|
657
|
+
<title>${config.title || 'Vader App'} - ${entry.route === 'index' ? 'Home' : entry.route}</title>
|
|
510
658
|
${htmlInjectionsString}
|
|
511
659
|
</head>
|
|
512
660
|
<body>
|
|
513
661
|
<div id="app"></div>
|
|
514
|
-
<script type="module" src="
|
|
662
|
+
<script type="module" src="/${entry.route === 'index' ? '' : entry.route + '/'}${outputName}"></script>
|
|
515
663
|
</body>
|
|
516
664
|
</html>`;
|
|
665
|
+
|
|
666
|
+
const htmlPath = path.join(outDir, "index.html");
|
|
667
|
+
await fs.writeFile(htmlPath, html);
|
|
668
|
+
|
|
669
|
+
logger.success(`Generated ${htmlPath}`);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Log summary
|
|
673
|
+
logger.success(`Built ${entrypoints.length} routes: ${entrypoints.map(e => e.route).join(', ')}`);
|
|
674
|
+
}
|
|
517
675
|
|
|
518
|
-
|
|
676
|
+
// Windows-compatible directory removal
|
|
677
|
+
async function removeDirectory(dir: string) {
|
|
678
|
+
try {
|
|
679
|
+
if (fsSync.existsSync(dir)) {
|
|
680
|
+
// Use fs.rm with recursive option for Windows compatibility
|
|
681
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
682
|
+
}
|
|
683
|
+
} catch (error) {
|
|
684
|
+
// Fallback: manual deletion for stubborn directories
|
|
685
|
+
if (fsSync.existsSync(dir)) {
|
|
686
|
+
const files = await fs.readdir(dir);
|
|
687
|
+
for (const file of files) {
|
|
688
|
+
const filePath = path.join(dir, file);
|
|
689
|
+
const stat = await fs.stat(filePath);
|
|
690
|
+
if (stat.isDirectory()) {
|
|
691
|
+
await removeDirectory(filePath);
|
|
692
|
+
} else {
|
|
693
|
+
await fs.unlink(filePath).catch(() => {});
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
await fs.rmdir(dir).catch(() => {});
|
|
697
|
+
}
|
|
519
698
|
}
|
|
520
699
|
}
|
|
521
700
|
|
|
522
701
|
async function buildAll(dev = false) {
|
|
523
702
|
const start = performance.now();
|
|
524
703
|
|
|
525
|
-
//
|
|
526
|
-
|
|
704
|
+
// Clear HTML injections before build
|
|
705
|
+
htmlInjectionManager.clear();
|
|
527
706
|
|
|
528
707
|
// Load plugins from config first
|
|
529
708
|
await loadPluginsFromConfig();
|
|
@@ -540,9 +719,8 @@ async function buildAll(dev = false) {
|
|
|
540
719
|
// Ensure jsconfig.json exists before building
|
|
541
720
|
await ensureJSConfig();
|
|
542
721
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
}
|
|
722
|
+
// Remove dist directory with Windows compatibility
|
|
723
|
+
await removeDirectory(DIST_DIR);
|
|
546
724
|
|
|
547
725
|
await fs.mkdir(DIST_DIR, { recursive: true });
|
|
548
726
|
|
|
@@ -561,11 +739,32 @@ async function buildAll(dev = false) {
|
|
|
561
739
|
|
|
562
740
|
// --- DEV SERVER ---
|
|
563
741
|
|
|
564
|
-
async function runDevServer() {
|
|
742
|
+
async function runDevServer() {
|
|
743
|
+
let buildPromise: Promise<void> | null = null;
|
|
744
|
+
let reloadTimeout: NodeJS.Timeout | null = null;
|
|
745
|
+
|
|
746
|
+
// Function to trigger reload with debounce
|
|
747
|
+
const triggerReload = (clients: Set<any>) => {
|
|
748
|
+
if (reloadTimeout) {
|
|
749
|
+
clearTimeout(reloadTimeout);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
reloadTimeout = setTimeout(() => {
|
|
753
|
+
logger.info("🔄 Reloading clients...");
|
|
754
|
+
for (const c of clients) {
|
|
755
|
+
try {
|
|
756
|
+
c.send("reload");
|
|
757
|
+
} catch (e) {
|
|
758
|
+
// Client might be disconnected
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
reloadTimeout = null;
|
|
762
|
+
}, 50);
|
|
763
|
+
};
|
|
764
|
+
|
|
565
765
|
await buildAll(true);
|
|
566
766
|
|
|
567
767
|
const clients = new Set<any>();
|
|
568
|
-
|
|
569
768
|
const port = config.port || 3000;
|
|
570
769
|
|
|
571
770
|
const server = serve({
|
|
@@ -573,47 +772,92 @@ async function runDevServer() {
|
|
|
573
772
|
|
|
574
773
|
fetch(req, server) {
|
|
575
774
|
const url = new URL(req.url);
|
|
576
|
-
|
|
775
|
+
|
|
776
|
+
// Handle HMR upgrade
|
|
577
777
|
if (url.pathname === "/__hmr" && server.upgrade(req)) return;
|
|
578
778
|
|
|
579
|
-
|
|
580
|
-
|
|
779
|
+
// Remove leading slash and handle path
|
|
780
|
+
let requestPath = url.pathname;
|
|
781
|
+
|
|
782
|
+
// If path ends with slash, serve index.html from that directory
|
|
783
|
+
if (requestPath.endsWith('/')) {
|
|
784
|
+
requestPath = path.join(requestPath, 'index.html');
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// Build the file path
|
|
788
|
+
let filePath = path.join(DIST_DIR, requestPath);
|
|
789
|
+
|
|
790
|
+
// If no extension, try as directory with index.html
|
|
581
791
|
if (!path.extname(filePath)) {
|
|
582
|
-
|
|
792
|
+
const indexPath = path.join(filePath, 'index.html');
|
|
793
|
+
if (fsSync.existsSync(indexPath)) {
|
|
794
|
+
filePath = indexPath;
|
|
795
|
+
} else {
|
|
796
|
+
// Try as .html file
|
|
797
|
+
const htmlPath = filePath + '.html';
|
|
798
|
+
if (fsSync.existsSync(htmlPath)) {
|
|
799
|
+
filePath = htmlPath;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
583
802
|
}
|
|
584
803
|
|
|
585
|
-
//
|
|
804
|
+
// Special handling for root
|
|
586
805
|
if (url.pathname === "/" || url.pathname === "") {
|
|
587
806
|
filePath = path.join(DIST_DIR, "index.html");
|
|
588
807
|
}
|
|
589
808
|
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
:
|
|
598
|
-
|
|
809
|
+
// Check if file exists and serve it
|
|
810
|
+
if (fsSync.existsSync(filePath)) {
|
|
811
|
+
const file = Bun.file(filePath);
|
|
812
|
+
// Set correct content type
|
|
813
|
+
const ext = path.extname(filePath);
|
|
814
|
+
const contentType = {
|
|
815
|
+
'.html': 'text/html',
|
|
816
|
+
'.js': 'application/javascript',
|
|
817
|
+
'.css': 'text/css',
|
|
818
|
+
'.json': 'application/json',
|
|
819
|
+
'.png': 'image/png',
|
|
820
|
+
'.jpg': 'image/jpeg',
|
|
821
|
+
'.jpeg': 'image/jpeg',
|
|
822
|
+
'.gif': 'image/gif',
|
|
823
|
+
'.svg': 'image/svg+xml',
|
|
824
|
+
'.ico': 'image/x-icon',
|
|
825
|
+
}[ext] || 'application/octet-stream';
|
|
826
|
+
|
|
827
|
+
return new Response(file, {
|
|
828
|
+
headers: {
|
|
829
|
+
'Content-Type': contentType,
|
|
830
|
+
},
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// 404 - Not found
|
|
835
|
+
logger.warn(`404 Not Found: ${requestPath} (resolved to ${filePath})`);
|
|
836
|
+
return new Response("Not Found", { status: 404 });
|
|
599
837
|
},
|
|
600
838
|
|
|
601
839
|
websocket: {
|
|
602
840
|
open(ws) {
|
|
603
841
|
clients.add(ws);
|
|
842
|
+
logger.info(`Client connected (${clients.size} total)`);
|
|
604
843
|
},
|
|
605
844
|
close(ws) {
|
|
606
845
|
clients.delete(ws);
|
|
846
|
+
logger.info(`Client disconnected (${clients.size} total)`);
|
|
607
847
|
},
|
|
608
848
|
},
|
|
609
849
|
});
|
|
610
850
|
|
|
851
|
+
// Watch all relevant directories
|
|
611
852
|
watcher.watch(APP_DIR);
|
|
612
853
|
watcher.watch(SRC_DIR);
|
|
613
854
|
watcher.watch(PUBLIC_DIR);
|
|
614
855
|
|
|
615
856
|
// Watch config file
|
|
616
|
-
|
|
857
|
+
const configPath = path.join(PROJECT_ROOT, "vaderjs.config.ts");
|
|
858
|
+
if (fsSync.existsSync(configPath)) {
|
|
859
|
+
watcher.watch(configPath);
|
|
860
|
+
}
|
|
617
861
|
|
|
618
862
|
// Watch plugins directory
|
|
619
863
|
const pluginsDir = path.join(PROJECT_ROOT, "plugins");
|
|
@@ -622,38 +866,69 @@ async function runDevServer() {
|
|
|
622
866
|
}
|
|
623
867
|
|
|
624
868
|
// Also watch for root App file
|
|
625
|
-
const
|
|
626
|
-
if (
|
|
627
|
-
watcher.watch(
|
|
869
|
+
const rootAppFile = findAppFile();
|
|
870
|
+
if (rootAppFile) {
|
|
871
|
+
watcher.watch(path.dirname(rootAppFile));
|
|
628
872
|
}
|
|
629
873
|
|
|
630
874
|
watcher.onChange(async (file) => {
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
logger.info("Config or plugin changed, reloading...");
|
|
636
|
-
config = await loadConfig();
|
|
637
|
-
htmlInjections = [];
|
|
638
|
-
plugins = [];
|
|
639
|
-
await loadPluginsFromConfig();
|
|
640
|
-
await loadPluginsFromDirectory();
|
|
875
|
+
// Don't trigger rebuild if one is already in progress
|
|
876
|
+
if (buildPromise) {
|
|
877
|
+
logger.info("Build already in progress, skipping...");
|
|
878
|
+
return;
|
|
641
879
|
}
|
|
642
880
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
881
|
+
logger.info(`Changes detected in: ${path.basename(file)}`);
|
|
882
|
+
watcher.setRebuildStatus(true);
|
|
883
|
+
|
|
884
|
+
try {
|
|
885
|
+
// If config or plugin file changed, reload config and plugins
|
|
886
|
+
if (file.includes('vaderjs.config.ts') || file.includes('plugins')) {
|
|
887
|
+
logger.info("Config or plugin changed, reloading...");
|
|
888
|
+
config = await loadConfig();
|
|
889
|
+
plugins = [];
|
|
890
|
+
await loadPluginsFromConfig();
|
|
891
|
+
await loadPluginsFromDirectory();
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// Run onFileChange hooks for plugins
|
|
895
|
+
const pluginAPI = createPluginAPI();
|
|
896
|
+
for (const plugin of plugins) {
|
|
897
|
+
if (plugin.onFileChange) {
|
|
898
|
+
await plugin.onFileChange(file, pluginAPI);
|
|
899
|
+
}
|
|
648
900
|
}
|
|
901
|
+
|
|
902
|
+
// Rebuild
|
|
903
|
+
buildPromise = buildAll(true);
|
|
904
|
+
await buildPromise;
|
|
905
|
+
buildPromise = null;
|
|
906
|
+
|
|
907
|
+
// Trigger reload for all connected clients
|
|
908
|
+
triggerReload(clients);
|
|
909
|
+
|
|
910
|
+
} catch (error) {
|
|
911
|
+
logger.error("Build failed:", error);
|
|
912
|
+
buildPromise = null;
|
|
913
|
+
} finally {
|
|
914
|
+
watcher.setRebuildStatus(false);
|
|
649
915
|
}
|
|
650
|
-
|
|
651
|
-
await buildAll(true);
|
|
652
|
-
|
|
653
|
-
for (const c of clients) c.send("reload");
|
|
654
916
|
});
|
|
655
917
|
|
|
656
918
|
logger.success(`Dev server running http://localhost:${port}`);
|
|
919
|
+
logger.info("Waiting for changes... (Press Ctrl+C to stop)");
|
|
920
|
+
|
|
921
|
+
// Log available routes
|
|
922
|
+
const distFiles = fsSync.readdirSync(DIST_DIR, { recursive: true });
|
|
923
|
+
const htmlFiles = distFiles.filter(f => f.toString().endsWith('index.html'));
|
|
924
|
+
if (htmlFiles.length > 0) {
|
|
925
|
+
logger.info("Available routes:");
|
|
926
|
+
for (const htmlFile of htmlFiles) {
|
|
927
|
+
const route = path.dirname(htmlFile.toString());
|
|
928
|
+
const urlPath = route === '.' ? '/' : `/${route}/`;
|
|
929
|
+
logger.info(` http://localhost:${port}${urlPath}`);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
657
932
|
}
|
|
658
933
|
|
|
659
934
|
// --- PROD SERVER ---
|