vaderjs 2.3.17 → 2.3.19

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.
Files changed (2) hide show
  1. package/main.ts +454 -141
  2. package/package.json +1 -1
package/main.ts CHANGED
@@ -36,7 +36,7 @@ async function timedStep(name, fn) {
36
36
  logger.success(`Finished '${name}' in ${duration}ms`);
37
37
  } catch (e) {
38
38
  logger.error(`Error during '${name}':`, e);
39
- if (!isDev) process.exit(1);
39
+ if (!globalThis.isDev) process.exit(1);
40
40
  }
41
41
  }
42
42
 
@@ -58,7 +58,6 @@ const VADER_SRC_PATH = path.join(
58
58
  const TEMP_SRC_DIR = path.join(PROJECT_ROOT, ".vader_temp_src");
59
59
 
60
60
  let config: any = {};
61
- let htmlInjections: string[] = [];
62
61
 
63
62
  // --- Plugin Support ---
64
63
 
@@ -72,7 +71,7 @@ interface Plugin {
72
71
  }
73
72
 
74
73
  interface PluginAPI {
75
- injectHTML(html: string): void;
74
+ injectHTML(html: string, id?: string): void;
76
75
  addWatchPath(path: string): void;
77
76
  config: any;
78
77
  isDev: boolean;
@@ -84,11 +83,61 @@ interface PluginAPI {
84
83
 
85
84
  let plugins: Plugin[] = [];
86
85
 
86
+ // Track HTML injections with IDs to prevent duplicates
87
+ class HTMLInjectionManager {
88
+ private injections: Map<string, string> = new Map();
89
+
90
+ add(html: string, id?: string): void {
91
+ const injectionId = id || this.generateId(html);
92
+
93
+ if (!this.injections.has(injectionId)) {
94
+ this.injections.set(injectionId, html);
95
+ logger.info(`Added HTML injection: ${injectionId}`);
96
+ } else {
97
+ logger.info(`Skipping duplicate HTML injection: ${injectionId}`);
98
+ }
99
+ }
100
+
101
+ private generateId(html: string): string {
102
+ const hrefMatch = html.match(/href=["']([^"']+)["']/);
103
+ if (hrefMatch) {
104
+ return `link:${hrefMatch[1]}`;
105
+ }
106
+
107
+ const srcMatch = html.match(/src=["']([^"']+)["']/);
108
+ if (srcMatch) {
109
+ return `script:${srcMatch[1]}`;
110
+ }
111
+
112
+ const nameMatch = html.match(/(?:name|property)=["']([^"']+)["']/);
113
+ if (nameMatch) {
114
+ return `meta:${nameMatch[1]}`;
115
+ }
116
+
117
+ let hash = 0;
118
+ for (let i = 0; i < html.length; i++) {
119
+ hash = ((hash << 5) - hash) + html.charCodeAt(i);
120
+ hash |= 0;
121
+ }
122
+ return `injection:${Math.abs(hash)}`;
123
+ }
124
+
125
+ getAll(): string[] {
126
+ return Array.from(this.injections.values());
127
+ }
128
+
129
+ clear(): void {
130
+ this.injections.clear();
131
+ }
132
+ }
133
+
134
+ const htmlInjectionManager = new HTMLInjectionManager();
135
+
87
136
  // Create plugin API helper
88
137
  function createPluginAPI(): PluginAPI {
89
138
  return {
90
- injectHTML: (html: string) => {
91
- htmlInjections.push(html);
139
+ injectHTML: (html: string, id?: string) => {
140
+ htmlInjectionManager.add(html, id);
92
141
  },
93
142
  addWatchPath: (watchPath: string) => {
94
143
  if (fsSync.existsSync(watchPath)) {
@@ -121,7 +170,6 @@ async function runPluginHook(hookName: 'onBuildStart' | 'onBuildFinish', api: Pl
121
170
 
122
171
  // Load plugins from config
123
172
  async function loadPluginsFromConfig() {
124
- console.log(config)
125
173
  if (!config.plugins || !Array.isArray(config.plugins)) {
126
174
  logger.info("No plugins defined in config");
127
175
  return;
@@ -133,32 +181,26 @@ async function loadPluginsFromConfig() {
133
181
  try {
134
182
  let plugin;
135
183
 
136
- // If plugin is a string, import it
137
184
  if (typeof pluginConfig === 'string') {
138
185
  const pluginPath = path.isAbsolute(pluginConfig)
139
186
  ? pluginConfig
140
187
  : path.join(PROJECT_ROOT, pluginConfig);
141
188
 
142
- // Check if file exists
143
189
  if (fsSync.existsSync(pluginPath)) {
144
190
  const pluginModule = await import(pluginPath);
145
191
  plugin = pluginModule.default || pluginModule;
146
192
  } else {
147
- // Try as npm package
148
193
  plugin = await import(pluginConfig);
149
194
  }
150
195
  }
151
- // If plugin is an object with resolve property
152
196
  else if (typeof pluginConfig === 'object' && pluginConfig.resolve) {
153
197
  const pluginModule = await import(pluginConfig.resolve);
154
198
  plugin = pluginModule.default || pluginModule;
155
199
 
156
- // Pass options to plugin if it's a factory function
157
200
  if (typeof plugin === 'function' && pluginConfig.options) {
158
201
  plugin = await plugin(pluginConfig.options);
159
202
  }
160
203
  }
161
- // If plugin is already a plugin object
162
204
  else if (typeof pluginConfig === 'object' && pluginConfig.name) {
163
205
  plugin = pluginConfig;
164
206
  }
@@ -177,7 +219,7 @@ async function loadPluginsFromConfig() {
177
219
  plugins = loadedPlugins;
178
220
  }
179
221
 
180
- // Also load from plugins directory (backward compatibility)
222
+ // Also load from plugins directory
181
223
  async function loadPluginsFromDirectory() {
182
224
  const pluginsDir = path.join(PROJECT_ROOT, "plugins");
183
225
 
@@ -196,7 +238,6 @@ async function loadPluginsFromDirectory() {
196
238
  const plugin = pluginModule.default || pluginModule;
197
239
 
198
240
  if (plugin && typeof plugin === 'object' && plugin.name) {
199
- // Check if plugin already loaded from config
200
241
  if (!plugins.some(p => p.name === plugin.name)) {
201
242
  loadedPlugins.push(plugin);
202
243
  logger.info(`Loaded plugin from directory: ${plugin.name} v${plugin.version}`);
@@ -218,7 +259,6 @@ async function loadPluginsFromDirectory() {
218
259
  async function ensureJSConfig() {
219
260
  const jsconfigPath = path.join(PROJECT_ROOT, "jsconfig.json");
220
261
 
221
- // Check if jsconfig.json already exists
222
262
  let existingConfig = {};
223
263
  if (fsSync.existsSync(jsconfigPath)) {
224
264
  try {
@@ -229,7 +269,6 @@ async function ensureJSConfig() {
229
269
  }
230
270
  }
231
271
 
232
- // Define the required VaderJS configuration
233
272
  const vaderConfig = {
234
273
  compilerOptions: {
235
274
  jsx: "react",
@@ -238,7 +277,6 @@ async function ensureJSConfig() {
238
277
  }
239
278
  };
240
279
 
241
- // Merge with existing config (preserve other settings)
242
280
  const mergedConfig = {
243
281
  ...existingConfig,
244
282
  compilerOptions: {
@@ -247,7 +285,6 @@ async function ensureJSConfig() {
247
285
  }
248
286
  };
249
287
 
250
- // Write the config
251
288
  await fs.writeFile(jsconfigPath, JSON.stringify(mergedConfig, null, 2));
252
289
  logger.success(`jsconfig.json created/updated at ${jsconfigPath}`);
253
290
  }
@@ -257,33 +294,63 @@ async function ensureJSConfig() {
257
294
  class FileWatcher {
258
295
  watchers = new Map<string, any>();
259
296
  callbacks: ((file: string) => void)[] = [];
297
+ private debounceTimer: NodeJS.Timeout | null = null;
298
+ private pendingFiles = new Set<string>();
299
+ private rebuildInProgress = false;
260
300
 
261
301
  watch(dir: string) {
262
302
  if (!fsSync.existsSync(dir)) return;
263
303
 
264
- const watcher = fsSync.watch(dir, { recursive: true }, (_, filename) => {
265
- if (!filename) return;
304
+ try {
305
+ const watcher = fsSync.watch(dir, { recursive: true }, (event, filename) => {
306
+ if (!filename) return;
266
307
 
267
- const file = path.join(dir, filename);
308
+ const file = path.join(dir, filename);
268
309
 
269
- if (
270
- file.includes("node_modules") ||
271
- file.includes("dist") ||
272
- file.includes(".git")
273
- )
274
- return;
310
+ if (
311
+ file.includes("node_modules") ||
312
+ file.includes("dist") ||
313
+ file.includes(".git") ||
314
+ file.includes(".vader_temp_src")
315
+ )
316
+ return;
275
317
 
276
- this.callbacks.forEach((cb) => cb(file));
277
- });
318
+ this.pendingFiles.add(file);
319
+
320
+ if (this.debounceTimer) {
321
+ clearTimeout(this.debounceTimer);
322
+ }
323
+
324
+ this.debounceTimer = setTimeout(() => {
325
+ const files = Array.from(this.pendingFiles);
326
+ this.pendingFiles.clear();
327
+
328
+ if (!this.rebuildInProgress) {
329
+ this.callbacks.forEach((cb) => {
330
+ files.forEach(file => cb(file));
331
+ });
332
+ }
333
+ }, 100);
334
+ });
278
335
 
279
- this.watchers.set(dir, watcher);
336
+ this.watchers.set(dir, watcher);
337
+ } catch (error) {
338
+ logger.warn(`Failed to watch directory ${dir}:`, error);
339
+ }
280
340
  }
281
341
 
282
342
  onChange(cb: (file: string) => void) {
283
343
  this.callbacks.push(cb);
284
344
  }
345
+
346
+ setRebuildStatus(status: boolean) {
347
+ this.rebuildInProgress = status;
348
+ }
285
349
 
286
350
  clear() {
351
+ if (this.debounceTimer) {
352
+ clearTimeout(this.debounceTimer);
353
+ }
287
354
  this.watchers.forEach((w) => w.close());
288
355
  this.watchers.clear();
289
356
  }
@@ -410,7 +477,6 @@ async function copyPublicAssets() {
410
477
  }
411
478
  }
412
479
 
413
- // Helper function to find App file
414
480
  function findAppFile(): string | null {
415
481
  const possiblePaths = [
416
482
  path.join(PROJECT_ROOT, "App.tsx"),
@@ -430,67 +496,204 @@ function findAppFile(): string | null {
430
496
  return null;
431
497
  }
432
498
 
433
- async function buildAppEntrypoints() {
434
- // First check for root App file (VaderJS standard)
435
- const appFile = findAppFile();
436
-
437
- // Build HTML with injections from plugins
438
- const htmlInjectionsString = htmlInjections.join('\n ');
499
+ function getUniqueInjections(injections: string[]): string[] {
500
+ const seen = new Set<string>();
501
+ const unique: string[] = [];
439
502
 
440
- if (appFile) {
441
- logger.info(`Building App from: ${appFile}`);
503
+ for (const injection of injections) {
504
+ let key = injection;
442
505
 
443
- await build({
444
- entrypoints: [appFile],
445
- outdir: DIST_DIR,
446
- target: "browser",
447
- jsxFactory: "e",
448
- jsxFragment: "Fragment",
449
- jsxImportSource: "vaderjs",
450
- naming: "index.js",
451
- external: [], // Bundle everything for website
452
- });
506
+ const hrefMatch = injection.match(/href=["']([^"']+)["']/);
507
+ if (hrefMatch) {
508
+ key = `link:${hrefMatch[1]}`;
509
+ }
510
+
511
+ const srcMatch = injection.match(/src=["']([^"']+)["']/);
512
+ if (srcMatch) {
513
+ key = `script:${srcMatch[1]}`;
514
+ }
515
+
516
+ const nameMatch = injection.match(/(?:name|property)=["']([^"']+)["']/);
517
+ if (nameMatch) {
518
+ key = `meta:${nameMatch[1]}`;
519
+ }
520
+
521
+ if (!seen.has(key)) {
522
+ seen.add(key);
523
+ unique.push(injection);
524
+ }
525
+ }
526
+
527
+ return unique;
528
+ }
453
529
 
454
- const html = `<!DOCTYPE html>
455
- <html>
456
- <head>
457
- <meta charset="UTF-8"/>
458
- <meta name="viewport" content="width=device-width,initial-scale=1"/>
459
- <title>${config.title || 'Vader App'}</title>
460
- ${htmlInjectionsString}
461
- </head>
462
- <body>
463
- <div id="app"></div>
464
- <script type="module" src="/index.js"></script>
465
- </body>
466
- </html>`;
530
+ // --- VERCEL CONFIG GENERATION ---
467
531
 
468
- await fs.writeFile(path.join(DIST_DIR, "index.html"), html);
532
+ async function generateVercelConfig(routes: { route: string; htmlPath: string }[]) {
533
+ if (config.host_provider !== "vercel") {
534
+ logger.info("Hosting provider is not 'vercel', skipping vercel.json generation");
469
535
  return;
470
536
  }
537
+
538
+ logger.info("🔧 Generating Vercel configuration for deployment...");
539
+
540
+ // Build the vercel.json configuration
541
+ const vercelConfig: any = {
542
+ version: 2,
543
+ buildCommand: "bun run build",
544
+ outputDirectory: "dist",
545
+ installCommand: "bun install",
546
+ framework: null,
547
+ rewrites: [],
548
+ };
549
+
550
+ // Add specific rewrites for each generated HTML route
551
+ for (const route of routes) {
552
+ if (route.route === "index") {
553
+ // Root route
554
+ vercelConfig.rewrites.push({
555
+ source: "/",
556
+ destination: "/dist/index.html",
557
+ });
558
+ } else {
559
+ // Nested route - exact match
560
+ vercelConfig.rewrites.push({
561
+ source: `/${route.route}`,
562
+ destination: `/dist/${route.route}/index.html`,
563
+ });
564
+ // Also handle subpaths (for client-side routing)
565
+ vercelConfig.rewrites.push({
566
+ source: `/${route.route}/:path*`,
567
+ destination: `/dist/${route.route}/index.html`,
568
+ });
569
+ }
570
+ }
571
+
572
+ // Fallback for static assets (CSS, JS, images)
573
+ vercelConfig.rewrites.push({
574
+ source: "/:path*",
575
+ destination: "/dist/:path*",
576
+ });
577
+
578
+ // Final fallback for any unmatched routes (SPA behavior)
579
+ vercelConfig.rewrites.push({
580
+ source: "/(.*)",
581
+ destination: "/dist/index.html",
582
+ });
583
+
584
+ // Remove duplicate rewrites (keep first occurrence for each source pattern)
585
+ const uniqueRewrites = [];
586
+ const seenSources = new Set();
587
+ for (const rewrite of vercelConfig.rewrites) {
588
+ if (!seenSources.has(rewrite.source)) {
589
+ seenSources.add(rewrite.source);
590
+ uniqueRewrites.push(rewrite);
591
+ }
592
+ }
593
+ vercelConfig.rewrites = uniqueRewrites;
594
+
595
+ // Write vercel.json to PROJECT ROOT (not inside dist)
596
+ const vercelPath = path.join(PROJECT_ROOT, "vercel.json");
597
+ await fs.writeFile(vercelPath, JSON.stringify(vercelConfig, null, 2));
598
+
599
+ logger.success(`✅ vercel.json generated at ${vercelPath}`);
600
+ logger.info(` Routes configured to serve from ./dist directory`);
601
+ logger.info(` ${routes.length} route(s) configured`);
602
+ }
603
+
604
+ async function generateVercelProjectConfig() {
605
+ if (config.hosting !== "vercel") return;
606
+
607
+ const vercelDir = path.join(PROJECT_ROOT, ".vercel");
608
+ await fs.mkdir(vercelDir, { recursive: true });
609
+
610
+ const projectConfig = {
611
+ projectId: config.vercelProjectId || "",
612
+ orgId: config.vercelOrgId || "",
613
+ settings: {
614
+ framework: null,
615
+ devCommand: "bun run dev",
616
+ installCommand: "bun install",
617
+ buildCommand: "bun run build",
618
+ outputDirectory: "dist",
619
+ },
620
+ };
621
+
622
+ const vercelProjectPath = path.join(vercelDir, "project.json");
623
+ await fs.writeFile(vercelProjectPath, JSON.stringify(projectConfig, null, 2));
624
+ logger.info(`📁 Generated .vercel/project.json`);
625
+ }
626
+
627
+ // --- BUILD APP ENTRYPOINTS ---
628
+
629
+ async function buildAppEntrypoints() {
471
630
 
472
- // Fallback to app directory structure
631
+ const allInjections = htmlInjectionManager.getAll();
632
+ const uniqueInjections = getUniqueInjections(allInjections);
633
+ const htmlInjectionsString = uniqueInjections.join('\n ');
634
+
635
+ if (uniqueInjections.length > 0) {
636
+ logger.info(`Injecting ${uniqueInjections.length} unique HTML items into page`);
637
+ }
638
+
473
639
  if (!fsSync.existsSync(APP_DIR)) {
474
- logger.warn("No App.tsx or app directory found");
640
+ logger.warn("No app directory found");
475
641
  return;
476
642
  }
477
643
 
478
- const entries = fsSync
479
- .readdirSync(APP_DIR, { recursive: true })
480
- .filter((f) => /index\.(tsx|jsx)$/.test(f as string))
481
- .map((f) => ({
482
- name:
483
- path.dirname(f as string) === "."
484
- ? "index"
485
- : path.dirname(f as string),
486
- path: path.join(APP_DIR, f as string),
487
- }));
488
-
489
- for (const entry of entries) {
490
- var outDir = path.join(DIST_DIR, entry.name === "index" ? "" : entry.name);
644
+ const entrypoints: { route: string; path: string }[] = [];
645
+ const generatedRoutes: { route: string; htmlPath: string }[] = [];
646
+
647
+ function findIndexFiles(dir: string, baseRoute: string = "") {
648
+ const entries = fsSync.readdirSync(dir, { withFileTypes: true });
649
+
650
+ for (const entry of entries) {
651
+ const fullPath = path.join(dir, entry.name);
652
+ const routePath = path.join(baseRoute, entry.name);
653
+
654
+ if (entry.isDirectory()) {
655
+ findIndexFiles(fullPath, routePath);
656
+ } else if (entry.name.match(/^index\.(tsx|jsx)$/)) {
657
+ let route = baseRoute;
658
+
659
+ if (route === "") {
660
+ route = "index";
661
+ } else {
662
+ // Convert Windows backslashes to forward slashes for URL
663
+ route = route.replace(/\\/g, '/');
664
+ }
665
+
666
+ entrypoints.push({
667
+ route: route,
668
+ path: fullPath
669
+ });
670
+
671
+ logger.info(`Found route: ${route} -> ${fullPath}`);
672
+ }
673
+ }
674
+ }
675
+
676
+ findIndexFiles(APP_DIR);
677
+
678
+ if (entrypoints.length === 0) {
679
+ logger.warn("No index.tsx/jsx files found in app directory");
680
+ return;
681
+ }
682
+
683
+ for (const entry of entrypoints) {
684
+ let outDir: string;
685
+ let outputName = "index.js";
491
686
 
687
+ if (entry.route === "index") {
688
+ outDir = DIST_DIR;
689
+ } else {
690
+ outDir = path.join(DIST_DIR, entry.route);
691
+ }
692
+
492
693
  await fs.mkdir(outDir, { recursive: true });
493
- console.log("Building entrypoint:", entry.path);
694
+
695
+ logger.info(`Building route: ${entry.route} -> ${outDir}`);
696
+
494
697
  await build({
495
698
  entrypoints: [entry.path],
496
699
  outdir: outDir,
@@ -498,60 +701,96 @@ ${htmlInjectionsString}
498
701
  jsxFactory: "e",
499
702
  jsxFragment: "Fragment",
500
703
  jsxImportSource: "vaderjs",
501
- external: [], // Bundle everything for website
704
+ naming: outputName,
705
+ external: [],
502
706
  });
503
-
707
+
708
+ const htmlPath = path.join(outDir, "index.html");
709
+ generatedRoutes.push({
710
+ route: entry.route,
711
+ htmlPath: htmlPath
712
+ });
713
+
714
+ const pageTitle = entry.route === "index"
715
+ ? (config.title || 'Vader App')
716
+ : `${config.title || 'Vader App'} - ${entry.route.charAt(0).toUpperCase() + entry.route.slice(1)}`;
717
+
504
718
  const html = `<!DOCTYPE html>
505
719
  <html>
506
720
  <head>
507
721
  <meta charset="UTF-8"/>
508
722
  <meta name="viewport" content="width=device-width,initial-scale=1"/>
509
- <title>${config.title || 'Vader App'}</title>
723
+ <title>${pageTitle}</title>
510
724
  ${htmlInjectionsString}
511
725
  </head>
512
726
  <body>
513
727
  <div id="app"></div>
514
- <script type="module" src="/index.js"></script>
728
+ <script type="module" src="/${entry.route === 'index' ? '' : entry.route + '/'}${outputName}"></script>
515
729
  </body>
516
730
  </html>`;
731
+
732
+ await fs.writeFile(htmlPath, html);
733
+ logger.success(`Generated ${htmlPath}`);
734
+ }
735
+
736
+ logger.success(`Built ${entrypoints.length} routes: ${entrypoints.map(e => e.route).join(', ')}`);
737
+
738
+ // Generate Vercel config if hosting is set to vercel
739
+ if (config.host_provider === "vercel") {
740
+ await generateVercelConfig(generatedRoutes);
741
+ }
742
+ }
517
743
 
518
- await fs.writeFile(path.join(outDir, "index.html"), html);
744
+ // Windows-compatible directory removal
745
+ async function removeDirectory(dir: string) {
746
+ try {
747
+ if (fsSync.existsSync(dir)) {
748
+ await fs.rm(dir, { recursive: true, force: true });
749
+ }
750
+ } catch (error) {
751
+ if (fsSync.existsSync(dir)) {
752
+ const files = await fs.readdir(dir);
753
+ for (const file of files) {
754
+ const filePath = path.join(dir, file);
755
+ const stat = await fs.stat(filePath);
756
+ if (stat.isDirectory()) {
757
+ await removeDirectory(filePath);
758
+ } else {
759
+ await fs.unlink(filePath).catch(() => {});
760
+ }
761
+ }
762
+ await fs.rmdir(dir).catch(() => {});
763
+ }
519
764
  }
520
765
  }
521
766
 
767
+ // --- MAIN BUILD ---
768
+
522
769
  async function buildAll(dev = false) {
523
770
  const start = performance.now();
524
771
 
525
- // Reset HTML injections before build
526
- htmlInjections = [];
772
+ htmlInjectionManager.clear();
527
773
 
528
- // Load plugins from config first
529
774
  await loadPluginsFromConfig();
530
-
531
- // Also load from plugins directory (backward compatibility)
532
775
  await loadPluginsFromDirectory();
533
776
 
534
- // Create plugin API
535
777
  const pluginAPI = createPluginAPI();
536
-
537
- // Run onBuildStart hooks
538
778
  await runPluginHook('onBuildStart', pluginAPI);
539
779
 
540
- // Ensure jsconfig.json exists before building
541
780
  await ensureJSConfig();
542
-
543
- if (fsSync.existsSync(DIST_DIR)) {
544
- await fs.rm(DIST_DIR, { recursive: true, force: true });
545
- }
546
-
781
+ await removeDirectory(DIST_DIR);
547
782
  await fs.mkdir(DIST_DIR, { recursive: true });
548
783
 
549
784
  await timedStep("Build Vader Core", buildVaderCore);
550
785
  await timedStep("Build Src", buildSrc);
551
786
  await timedStep("Copy Public", copyPublicAssets);
552
787
  await timedStep("Build App", buildAppEntrypoints);
788
+
789
+ // Generate Vercel project config if needed (optional)
790
+ if (config.hosting === "vercel") {
791
+ await generateVercelProjectConfig();
792
+ }
553
793
 
554
- // Run onBuildFinish hooks
555
794
  await runPluginHook('onBuildFinish', pluginAPI);
556
795
 
557
796
  logger.success(
@@ -562,10 +801,28 @@ async function buildAll(dev = false) {
562
801
  // --- DEV SERVER ---
563
802
 
564
803
  async function runDevServer() {
804
+ let buildPromise: Promise<void> | null = null;
805
+ let reloadTimeout: NodeJS.Timeout | null = null;
806
+
807
+ const triggerReload = (clients: Set<any>) => {
808
+ if (reloadTimeout) {
809
+ clearTimeout(reloadTimeout);
810
+ }
811
+
812
+ reloadTimeout = setTimeout(() => {
813
+ logger.info("🔄 Reloading clients...");
814
+ for (const c of clients) {
815
+ try {
816
+ c.send("reload");
817
+ } catch (e) {}
818
+ }
819
+ reloadTimeout = null;
820
+ }, 50);
821
+ };
822
+
565
823
  await buildAll(true);
566
824
 
567
825
  const clients = new Set<any>();
568
-
569
826
  const port = config.port || 3000;
570
827
 
571
828
  const server = serve({
@@ -573,37 +830,68 @@ async function runDevServer() {
573
830
 
574
831
  fetch(req, server) {
575
832
  const url = new URL(req.url);
576
-
833
+
577
834
  if (url.pathname === "/__hmr" && server.upgrade(req)) return;
578
835
 
579
- let filePath = path.join(DIST_DIR, url.pathname);
580
-
836
+ let requestPath = url.pathname;
837
+
838
+ if (requestPath.endsWith('/')) {
839
+ requestPath = path.join(requestPath, 'index.html');
840
+ }
841
+
842
+ let filePath = path.join(DIST_DIR, requestPath);
843
+
581
844
  if (!path.extname(filePath)) {
582
- filePath = path.join(filePath, "index.html");
845
+ const indexPath = path.join(filePath, 'index.html');
846
+ if (fsSync.existsSync(indexPath)) {
847
+ filePath = indexPath;
848
+ } else {
849
+ const htmlPath = filePath + '.html';
850
+ if (fsSync.existsSync(htmlPath)) {
851
+ filePath = htmlPath;
852
+ }
853
+ }
583
854
  }
584
855
 
585
- // Ensure we're serving from dist directory
586
856
  if (url.pathname === "/" || url.pathname === "") {
587
857
  filePath = path.join(DIST_DIR, "index.html");
588
858
  }
589
859
 
590
- console.log("Serving:", filePath);
591
-
592
- const file = Bun.file(filePath);
593
-
594
- return file.exists().then((exists) =>
595
- exists
596
- ? new Response(file)
597
- : new Response("Not Found", { status: 404 })
598
- );
860
+ if (fsSync.existsSync(filePath)) {
861
+ const file = Bun.file(filePath);
862
+ const ext = path.extname(filePath);
863
+ const contentType = {
864
+ '.html': 'text/html',
865
+ '.js': 'application/javascript',
866
+ '.css': 'text/css',
867
+ '.json': 'application/json',
868
+ '.png': 'image/png',
869
+ '.jpg': 'image/jpeg',
870
+ '.jpeg': 'image/jpeg',
871
+ '.gif': 'image/gif',
872
+ '.svg': 'image/svg+xml',
873
+ '.ico': 'image/x-icon',
874
+ }[ext] || 'application/octet-stream';
875
+
876
+ return new Response(file, {
877
+ headers: {
878
+ 'Content-Type': contentType,
879
+ },
880
+ });
881
+ }
882
+
883
+ logger.warn(`404 Not Found: ${requestPath}`);
884
+ return new Response("Not Found", { status: 404 });
599
885
  },
600
886
 
601
887
  websocket: {
602
888
  open(ws) {
603
889
  clients.add(ws);
890
+ logger.info(`Client connected (${clients.size} total)`);
604
891
  },
605
892
  close(ws) {
606
893
  clients.delete(ws);
894
+ logger.info(`Client disconnected (${clients.size} total)`);
607
895
  },
608
896
  },
609
897
  });
@@ -612,48 +900,73 @@ async function runDevServer() {
612
900
  watcher.watch(SRC_DIR);
613
901
  watcher.watch(PUBLIC_DIR);
614
902
 
615
- // Watch config file
616
- watcher.watch(path.join(PROJECT_ROOT, "vaderjs.config.js"));
903
+ const configPath = path.join(PROJECT_ROOT, "vaderjs.config.ts");
904
+ if (fsSync.existsSync(configPath)) {
905
+ watcher.watch(configPath);
906
+ }
617
907
 
618
- // Watch plugins directory
619
908
  const pluginsDir = path.join(PROJECT_ROOT, "plugins");
620
909
  if (fsSync.existsSync(pluginsDir)) {
621
910
  watcher.watch(pluginsDir);
622
911
  }
623
912
 
624
- // Also watch for root App file
625
- const rootAppDir = path.dirname(findAppFile() || "");
626
- if (rootAppDir && rootAppDir !== PROJECT_ROOT) {
627
- watcher.watch(rootAppDir);
913
+ const rootAppFile = findAppFile();
914
+ if (rootAppFile) {
915
+ watcher.watch(path.dirname(rootAppFile));
628
916
  }
629
917
 
630
918
  watcher.onChange(async (file) => {
631
- logger.info("Changes detected, rebuilding...");
632
-
633
- // If config or plugin file changed, reload config and plugins
634
- if (file.includes('vaderjs.config.js') || file.includes('plugins')) {
635
- logger.info("Config or plugin changed, reloading...");
636
- config = await loadConfig();
637
- htmlInjections = [];
638
- plugins = [];
639
- await loadPluginsFromConfig();
640
- await loadPluginsFromDirectory();
919
+ if (buildPromise) {
920
+ logger.info("Build already in progress, skipping...");
921
+ return;
641
922
  }
642
923
 
643
- // Run onFileChange hooks for plugins
644
- const pluginAPI = createPluginAPI();
645
- for (const plugin of plugins) {
646
- if (plugin.onFileChange) {
647
- await plugin.onFileChange(file, pluginAPI);
924
+ logger.info(`Changes detected in: ${path.basename(file)}`);
925
+ watcher.setRebuildStatus(true);
926
+
927
+ try {
928
+ if (file.includes('vaderjs.config.ts') || file.includes('plugins')) {
929
+ logger.info("Config or plugin changed, reloading...");
930
+ config = await loadConfig();
931
+ plugins = [];
932
+ await loadPluginsFromConfig();
933
+ await loadPluginsFromDirectory();
648
934
  }
935
+
936
+ const pluginAPI = createPluginAPI();
937
+ for (const plugin of plugins) {
938
+ if (plugin.onFileChange) {
939
+ await plugin.onFileChange(file, pluginAPI);
940
+ }
941
+ }
942
+
943
+ buildPromise = buildAll(true);
944
+ await buildPromise;
945
+ buildPromise = null;
946
+
947
+ triggerReload(clients);
948
+
949
+ } catch (error) {
950
+ logger.error("Build failed:", error);
951
+ buildPromise = null;
952
+ } finally {
953
+ watcher.setRebuildStatus(false);
649
954
  }
650
-
651
- await buildAll(true);
652
-
653
- for (const c of clients) c.send("reload");
654
955
  });
655
956
 
656
957
  logger.success(`Dev server running http://localhost:${port}`);
958
+ logger.info("Waiting for changes... (Press Ctrl+C to stop)");
959
+
960
+ const distFiles = fsSync.readdirSync(DIST_DIR, { recursive: true });
961
+ const htmlFiles = distFiles.filter(f => f.toString().endsWith('index.html'));
962
+ if (htmlFiles.length > 0) {
963
+ logger.info("Available routes:");
964
+ for (const htmlFile of htmlFiles) {
965
+ const route = path.dirname(htmlFile.toString());
966
+ const urlPath = route === '.' ? '/' : `/${route}/`;
967
+ logger.info(` http://localhost:${port}${urlPath}`);
968
+ }
969
+ }
657
970
  }
658
971
 
659
972
  // --- PROD SERVER ---
@@ -703,7 +1016,6 @@ ${colors.reset}`);
703
1016
 
704
1017
  if (cmd === "init") {
705
1018
  await initProject(process.argv[3]);
706
- // Also create jsconfig.json when initializing a new project
707
1019
  await ensureJSConfig();
708
1020
  return;
709
1021
  }
@@ -737,6 +1049,7 @@ Make sure you have:
737
1049
  - App.tsx or App.jsx in your project root
738
1050
  - OR an app/ directory with index.tsx/jsx files
739
1051
  - vaderjs installed as a dependency
1052
+ - For Vercel deployment: set hosting: "vercel" in vaderjs.config.ts
740
1053
  `);
741
1054
  }
742
1055
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vaderjs",
3
- "version": "2.3.17",
3
+ "version": "2.3.19",
4
4
  "description": "A simple and powerful JavaScript library for building modern web applications.",
5
5
  "bin": {
6
6
  "vaderjs": "./main.ts"