vaderjs 2.3.17 → 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.
Files changed (2) hide show
  1. package/main.ts +388 -113
  2. package/package.json +1 -1
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
- htmlInjections.push(html);
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
- const watcher = fsSync.watch(dir, { recursive: true }, (_, filename) => {
265
- if (!filename) return;
328
+ try {
329
+ const watcher = fsSync.watch(dir, { recursive: true }, (event, filename) => {
330
+ if (!filename) return;
266
331
 
267
- const file = path.join(dir, filename);
332
+ const file = path.join(dir, filename);
268
333
 
269
- if (
270
- file.includes("node_modules") ||
271
- file.includes("dist") ||
272
- file.includes(".git")
273
- )
274
- return;
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
- this.callbacks.forEach((cb) => cb(file));
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
- this.watchers.set(dir, watcher);
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
- async function buildAppEntrypoints() {
434
- // First check for root App file (VaderJS standard)
435
- const appFile = findAppFile();
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
- // Build HTML with injections from plugins
438
- const htmlInjectionsString = htmlInjections.join('\n ');
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
- 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
- });
453
-
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>`;
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
- await fs.writeFile(path.join(DIST_DIR, "index.html"), html);
469
- return;
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
- // Fallback to app directory structure
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
- 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);
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
- console.log("Building entrypoint:", entry.path);
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="/index.js"></script>
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
- await fs.writeFile(path.join(outDir, "index.html"), html);
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
- // Reset HTML injections before build
526
- htmlInjections = [];
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
- if (fsSync.existsSync(DIST_DIR)) {
544
- await fs.rm(DIST_DIR, { recursive: true, force: true });
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
- let filePath = path.join(DIST_DIR, url.pathname);
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
- filePath = path.join(filePath, "index.html");
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
- // Ensure we're serving from dist directory
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
- 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
- );
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
- watcher.watch(path.join(PROJECT_ROOT, "vaderjs.config.js"));
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 rootAppDir = path.dirname(findAppFile() || "");
626
- if (rootAppDir && rootAppDir !== PROJECT_ROOT) {
627
- watcher.watch(rootAppDir);
869
+ const rootAppFile = findAppFile();
870
+ if (rootAppFile) {
871
+ watcher.watch(path.dirname(rootAppFile));
628
872
  }
629
873
 
630
874
  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();
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
- // 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);
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 ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vaderjs",
3
- "version": "2.3.17",
3
+ "version": "2.3.18",
4
4
  "description": "A simple and powerful JavaScript library for building modern web applications.",
5
5
  "bin": {
6
6
  "vaderjs": "./main.ts"