vaderjs-native 1.0.37 → 1.0.38

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/main.ts CHANGED
@@ -1,1027 +1,1003 @@
1
- #!/usr/bin/env bun
2
-
3
- import { build, serve } from "bun";
4
- import fs from "fs/promises";
5
- import fsSync from "fs";
6
- import path from "path";
7
- import { initProject } from "./cli";
8
- import { logger, timedStep } from "./cli/logger";
9
- import { colors } from "./cli/logger";
10
- import runDevServer from "vaderjs-native/cli/web/server";
11
- import { runProdServer } from "vaderjs-native/cli/web/server";
12
- import { androidDev } from "./cli/android/dev.js";
13
- import { buildAndroid } from "./cli/android/build";
14
- import { buildWindows } from "./cli/windows/build";
15
- import openWinApp from "./cli/windows/dev";
16
- import { Config } from "./config";
17
-
18
- // --- CONSTANTS ---
19
- const PROJECT_ROOT = process.cwd();
20
- const PUBLIC_DIR = path.join(PROJECT_ROOT, "public");
21
- const DIST_DIR = path.join(PROJECT_ROOT, "dist");
22
- const SRC_DIR = path.join(PROJECT_ROOT, "src");
23
- const VADER_SRC_PATH = path.join(PROJECT_ROOT, "node_modules", "vaderjs-native", "index.ts");
24
- const TEMP_SRC_DIR = path.join(PROJECT_ROOT, ".vader_temp_src");
25
-
26
- // --- CACHE SYSTEM ---
27
- const buildCache = new Map<string, { mtime: number; hash: string }>();
28
- const configCache = new Map<string, { config: Config; mtime: number }>();
29
- let config: Config = {};
30
- let htmlInjections: string[] = [];
31
-
32
- // --- SIMPLIFIED WATCHER ---
33
- class FileWatcher {
34
- private watchers: Map<string, any>;
35
- private onChangeCallbacks: Array<(filePath: string) => void>;
36
- private isRebuilding: boolean;
37
- private lastRebuildTime: number;
38
- private readonly REBUILD_COOLDOWN = 1000;
39
-
40
- constructor() {
41
- this.watchers = new Map();
42
- this.onChangeCallbacks = [];
43
- this.isRebuilding = false;
44
- this.lastRebuildTime = 0;
45
- }
46
-
47
- shouldIgnorePath(filePath: string): boolean {
48
- const normalized = path.normalize(filePath);
49
- // Ignore dist folder and its contents
50
- if (normalized.includes(path.normalize(DIST_DIR))) {
51
- return true;
52
- }
53
- // Ignore node_modules
54
- if (normalized.includes(path.normalize('node_modules'))) {
55
- return true;
56
- }
57
- // Ignore .git folder
58
- if (normalized.includes(path.normalize('.git'))) {
59
- return true;
60
- }
61
- // Ignore the temporary source directory
62
- if (normalized.includes(path.normalize(TEMP_SRC_DIR))) {
63
- return true;
64
- }
65
- return false;
66
- }
67
-
68
- async watchDirectory(dirPath: string, recursive = true): Promise<void> {
69
- // Skip if directory should be ignored
70
- if (this.shouldIgnorePath(dirPath) || !fsSync.existsSync(dirPath)) {
71
- return;
72
- }
73
-
74
- try {
75
- // Close existing watcher if any
76
- if (this.watchers.has(dirPath)) {
77
- try {
78
- this.watchers.get(dirPath).close();
79
- } catch (err) {
80
- // Ignore close errors
81
- }
82
- }
83
-
84
- // Create new watcher
85
- const watcher = fsSync.watch(dirPath, { recursive }, (eventType: string, filename: string | null) => {
86
- if (!filename) return;
87
-
88
- const changedFile = path.join(dirPath, filename);
89
- const normalizedChanged = path.normalize(changedFile);
90
-
91
- // Skip if file should be ignored
92
- if (this.shouldIgnorePath(normalizedChanged)) {
93
- return;
94
- }
95
-
96
- // Check if this is a file we care about
97
- if (this.shouldTriggerRebuild(normalizedChanged)) {
98
- logger.info(`File changed: ${path.relative(PROJECT_ROOT, normalizedChanged)}`);
99
-
100
- // Only trigger if not already rebuilding and cooldown has passed
101
- const now = Date.now();
102
- if (!this.isRebuilding && (now - this.lastRebuildTime) > this.REBUILD_COOLDOWN) {
103
- this.triggerChange(normalizedChanged);
104
- } else if (this.isRebuilding) {
105
- logger.info(`Skipping rebuild - already rebuilding`);
106
- } else {
107
- logger.info(`Skipping rebuild - cooldown period`);
108
- }
109
- }
110
- });
111
-
112
- watcher.on('error', (err: Error) => {
113
- logger.warn(`Watcher error on ${dirPath}:`, err.message);
114
- });
115
-
116
- this.watchers.set(dirPath, watcher);
117
-
118
- logger.info(`Watching directory: ${path.relative(PROJECT_ROOT, dirPath)}`);
119
- } catch (err: any) {
120
- logger.warn(`Could not watch directory ${dirPath}:`, err.message);
121
- }
122
- }
123
-
124
- shouldTriggerRebuild(filePath: string): boolean {
125
- // Only trigger rebuild for specific file types
126
- const ext = path.extname(filePath).toLowerCase();
127
- const triggerExtensions = ['.js', '.jsx', '.ts', '.tsx', '.css', '.html', '.json', '.config.js', '.config.ts'];
128
- return triggerExtensions.includes(ext) || ext === '';
129
- }
130
-
131
- triggerChange(filePath: string): void {
132
- for (const callback of this.onChangeCallbacks) {
133
- try {
134
- callback(filePath);
135
- } catch (err) {
136
- logger.error("Change callback error:", err);
137
- }
138
- }
139
- }
140
-
141
- onChange(callback: (filePath: string) => void): () => void {
142
- this.onChangeCallbacks.push(callback);
143
- return () => {
144
- const index = this.onChangeCallbacks.indexOf(callback);
145
- if (index > -1) this.onChangeCallbacks.splice(index, 1);
146
- };
147
- }
148
-
149
- setRebuilding(state: boolean): void {
150
- this.isRebuilding = state;
151
- if (state) {
152
- this.lastRebuildTime = Date.now();
153
- }
154
- }
155
-
156
- clear(): void {
157
- for (const [dir, watcher] of this.watchers) {
158
- try {
159
- watcher.close();
160
- } catch (err) {
161
- // Ignore close errors
162
- }
163
- }
164
- this.watchers.clear();
165
- this.onChangeCallbacks = [];
166
- this.isRebuilding = false;
167
- }
168
- }
169
-
170
- const watcher = new FileWatcher();
171
-
172
- // --- CONFIG & PLUGIN SYSTEM ---
173
- interface VaderAPI {
174
- runCommand: (cmd: string | string[]) => Promise<void>;
175
- injectHTML: (content: string) => void;
176
- log: {
177
- warn: (msg: string) => void;
178
- info: (msg: string) => void;
179
- success: (msg: string) => void;
180
- step: (msg: string) => void;
181
- };
182
- getProjectRoot: () => string;
183
- getDistDir: () => string;
184
- getPublicDir: () => string;
185
- }
186
-
187
- const vaderAPI: VaderAPI = {
188
- runCommand: async (cmd) => {
189
- if (typeof cmd === "string") cmd = cmd.split(" ");
190
- const p = Bun.spawn(cmd);
191
- await p.exited;
192
- },
193
- injectHTML: (content) => htmlInjections.push(content),
194
- log: {
195
- warn: (msg) => logger.warn(msg),
196
- info: (msg) => logger.info(msg),
197
- success: (msg) => logger.success(msg),
198
- step: (msg) => logger.step(msg)
199
- },
200
- getProjectRoot: () => PROJECT_ROOT,
201
- getDistDir: () => DIST_DIR,
202
- getPublicDir: () => PUBLIC_DIR,
203
- };
204
-
205
- // Optimized config loading with cache
206
- export async function loadConfig(projectDir?: string): Promise<Config> {
207
- projectDir = projectDir || process.cwd();
208
- const configKey = `config-${projectDir}`;
209
-
210
- const configPathJs = path.join(projectDir, "vaderjs.config.js");
211
- const configPathTs = path.join(projectDir, "vaderjs.config.ts");
212
-
213
- let configPath: string | null = null;
214
- let stat: any = null;
215
-
216
- // Find which config file exists
217
- try {
218
- stat = await fs.stat(configPathTs);
219
- configPath = configPathTs;
220
- } catch {
221
- try {
222
- stat = await fs.stat(configPathJs);
223
- configPath = configPathJs;
224
- } catch {
225
- return {}; // No config file
226
- }
227
- }
228
-
229
- // Check cache
230
- if (stat && configPath) {
231
- const cached = configCache.get(configKey);
232
- if (cached && cached.mtime === stat.mtimeMs) {
233
- return cached.config;
234
- }
235
-
236
- // Load config
237
- const userConfig = (await import(`file://${configPath}`)).default;
238
- configCache.set(configKey, { config: userConfig, mtime: stat.mtimeMs });
239
- return userConfig;
240
- }
241
-
242
- return {};
243
- }
244
-
245
- export function defineConfig(config: Config): Config {
246
- return config;
247
- }
248
-
249
- async function runPluginHook(hookName: string): Promise<void> {
250
- if (!config.plugins) return;
251
-
252
- const pluginPromises = config.plugins.map(async (plugin) => {
253
- if (typeof plugin[hookName] === "function") {
254
- try {
255
- await plugin[hookName](vaderAPI);
256
- } catch (e) {
257
- logger.error(`Plugin hook error (${hookName} in ${plugin.name || 'anonymous'}):`, e);
258
- }
259
- }
260
- });
261
-
262
- await Promise.all(pluginPromises);
263
- }
264
-
265
- // --- OPTIMIZED BUILD HELPERS ---
266
-
267
- // Helper to find App.tsx in project root
268
- function findAppFile(): string | null {
269
- const possiblePaths = [
270
- path.join(PROJECT_ROOT, "App.tsx"),
271
- path.join(PROJECT_ROOT, "App.jsx"),
272
- path.join(PROJECT_ROOT, "App.ts"),
273
- path.join(PROJECT_ROOT, "App.js")
274
- ];
275
-
276
- for (const appPath of possiblePaths) {
277
- if (fsSync.existsSync(appPath)) {
278
- return appPath;
279
- }
280
- }
281
-
282
- return null;
283
- }
284
-
285
- // Helper to find route files in src/pages or src/routes
286
- async function findRouteFiles(): Promise<string[]> {
287
- const routes: string[] = [];
288
-
289
- // Look for pages in src/pages or src/routes
290
- const possibleDirs = [
291
- path.join(PROJECT_ROOT, "src", "pages"),
292
- path.join(PROJECT_ROOT, "src", "routes")
293
- ];
294
-
295
- for (const dir of possibleDirs) {
296
- if (fsSync.existsSync(dir)) {
297
- await collectRouteFiles(dir, routes);
298
- }
299
- }
300
-
301
- return routes;
302
- }
303
-
304
- async function collectRouteFiles(dir: string, routes: string[]): Promise<void> {
305
- const entries = await fs.readdir(dir, { withFileTypes: true });
306
-
307
- for (const entry of entries) {
308
- const fullPath = path.join(dir, entry.name);
309
-
310
- if (entry.isDirectory()) {
311
- await collectRouteFiles(fullPath, routes);
312
- } else if (entry.name.endsWith('.tsx') || entry.name.endsWith('.jsx')) {
313
- // Check if it's a route component (not layout or other)
314
- if (!entry.name.includes('.layout.') && !entry.name.includes('.component.')) {
315
- routes.push(fullPath);
316
- }
317
- }
318
- }
319
- }
320
-
321
- // File hashing for cache invalidation
322
- async function getFileHash(filepath: string): Promise<string> {
323
- const content = await fs.readFile(filepath);
324
- return Bun.hash(content).toString(16);
325
- }
326
-
327
- // Check if file needs rebuild
328
- async function needsRebuild(sourcePath: string, destPath: string): Promise<boolean> {
329
- try {
330
- const [sourceStat, destStat] = await Promise.all([
331
- fs.stat(sourcePath),
332
- fs.stat(destPath).catch(() => null)
333
- ]);
334
-
335
- if (!destStat) return true;
336
-
337
- const cacheKey = `${sourcePath}:${destPath}`;
338
- const cached = buildCache.get(cacheKey);
339
-
340
- if (cached && cached.mtime === sourceStat.mtimeMs) {
341
- return false;
342
- }
343
-
344
- // Check if content changed
345
- const hash = await getFileHash(sourcePath);
346
- if (cached && cached.hash === hash) {
347
- buildCache.set(cacheKey, { mtime: sourceStat.mtimeMs, hash });
348
- return false;
349
- }
350
-
351
- return true;
352
- } catch {
353
- return true;
354
- }
355
- }
356
-
357
- // Parallel file operations
358
- async function parallelForEach<T>(
359
- items: T[],
360
- callback: (item: T, index: number) => Promise<void>,
361
- concurrency = 4
362
- ): Promise<void> {
363
- const chunks = [];
364
- for (let i = 0; i < items.length; i += concurrency) {
365
- chunks.push(items.slice(i, i + concurrency));
366
- }
367
-
368
- for (const chunk of chunks) {
369
- await Promise.all(chunk.map((item, index) => callback(item, index)));
370
- }
371
- }
372
-
373
- async function copyIfNeeded(src: string, dest: string): Promise<void> {
374
- const cacheKey = `copy-${src}`;
375
- const stat = await fs.stat(src).catch(() => null);
376
- if (!stat) return;
377
-
378
- const cached = buildCache.get(cacheKey);
379
- if (cached && cached.mtime === stat.mtimeMs) {
380
- return;
381
- }
382
-
383
- await fs.copyFile(src, dest);
384
- buildCache.set(cacheKey, { mtime: stat.mtimeMs, hash: await getFileHash(src) });
385
- }
386
-
387
- // --- BUILD LOGIC ---
388
-
389
- /**
390
- * Step 1: Transpile and bundle the core vaderjs library (cached)
391
- */
392
- async function buildVaderCore(): Promise<void> {
393
- if (!fsSync.existsSync(VADER_SRC_PATH)) {
394
- logger.error("VaderJS source not found:", VADER_SRC_PATH);
395
- throw new Error("Missing vaderjs dependency.");
396
- }
397
-
398
- const outDir = path.join(DIST_DIR, "src", "vader");
399
- const mainOutput = path.join(outDir, "index.js");
400
-
401
- // Check if rebuild is needed
402
- if (!(await needsRebuild(VADER_SRC_PATH, mainOutput))) {
403
- logger.info("VaderJS Core is up to date");
404
- return;
405
- }
406
-
407
- await fs.mkdir(outDir, { recursive: true });
408
-
409
- await build({
410
- entrypoints: [VADER_SRC_PATH],
411
- outdir: outDir,
412
- target: "browser",
413
- minify: false,
414
- sourcemap: "external",
415
- jsxFactory: "e",
416
- jsxFragment: "Fragment",
417
- jsxImportSource: "vaderjs",
418
- external: [], // Bundle everything
419
- });
420
-
421
- // Update cache
422
- const stat = await fs.stat(VADER_SRC_PATH);
423
- const hash = await getFileHash(VADER_SRC_PATH);
424
- buildCache.set(`${VADER_SRC_PATH}:${mainOutput}`, { mtime: stat.mtimeMs, hash });
425
- }
426
-
427
- /**
428
- * Step 2: Patches source code to remove server-side hook imports
429
- */
430
- function patchHooksUsage(code: string): string {
431
- return code.replace(/import\s+{[^}]*use(State|Effect|Memo|Navigation)[^}]*}\s+from\s+['"]vaderjs['"];?\n?/g, "");
432
- }
433
-
434
- function publicAssetPlugin() {
435
- const assetCache = new Map<string, string>();
436
-
437
- return {
438
- name: "public-asset-replacer",
439
- setup(build: any) {
440
- build.onLoad({ filter: /\.(js|ts|jsx|tsx|html)$/ }, async (args: any) => {
441
- const stat = await fs.stat(args.path).catch(() => null);
442
- if (!stat) return null;
443
-
444
- const cacheKey = `asset-${args.path}`;
445
- const cached = assetCache.get(cacheKey);
446
- if (cached && stat.mtimeMs <= (await fs.stat(args.path).catch(() => ({ mtimeMs: 0 }))).mtimeMs) {
447
- return { contents: cached, loader: getLoader(args.path) };
448
- }
449
-
450
- let code = await fs.readFile(args.path, "utf8");
451
-
452
- // Process asset paths in parallel
453
- const assetMatches = [...code.matchAll(/\{\{public:(.+?)\}\}/g)];
454
- const processedAssets = await Promise.all(
455
- assetMatches.map(async (match) => {
456
- const relPath = match[1].trim();
457
- const absPath = path.join(PUBLIC_DIR, relPath);
458
- try {
459
- await fs.access(absPath);
460
- return { match: match[0], replacement: "/" + relPath.replace(/\\/g, "/") };
461
- } catch {
462
- logger.warn(`Public asset not found: ${relPath}`);
463
- return { match: match[0], replacement: relPath };
464
- }
465
- })
466
- );
467
-
468
- for (const { match, replacement } of processedAssets) {
469
- code = code.replace(match, replacement);
470
- }
471
-
472
- assetCache.set(cacheKey, code);
473
- return { contents: code, loader: getLoader(args.path) };
474
- });
475
- },
476
- };
477
- }
478
-
479
- function getLoader(filepath: string): string {
480
- if (filepath.endsWith(".html")) return "text";
481
- if (filepath.endsWith(".tsx")) return "tsx";
482
- if (filepath.endsWith(".jsx")) return "jsx";
483
- if (filepath.endsWith(".ts")) return "ts";
484
- return "js";
485
- }
486
-
487
- /**
488
- * Step 3: Pre-processes all files in `/src` into a temporary directory (parallel)
489
- */
490
- async function preprocessSources(srcDir: string, tempDir: string): Promise<void> {
491
- await fs.mkdir(tempDir, { recursive: true });
492
-
493
- const entries = await fs.readdir(srcDir, { withFileTypes: true });
494
-
495
- await parallelForEach(entries, async (entry) => {
496
- const srcPath = path.join(srcDir, entry.name);
497
- const destPath = path.join(tempDir, entry.name);
498
-
499
- if (entry.isDirectory()) {
500
- await preprocessSources(srcPath, destPath);
501
- } else if (/\.(tsx|jsx|ts|js)$/.test(entry.name)) {
502
- const content = await fs.readFile(srcPath, "utf8");
503
- const processed = patchHooksUsage(content);
504
- await fs.writeFile(destPath, processed);
505
- } else {
506
- await fs.copyFile(srcPath, destPath);
507
- }
508
- });
509
- }
510
- function absoluteAssetPlugin() {
511
- return {
512
- name: "absolute-asset-plugin",
513
- setup(build) {
514
- build.onLoad(
515
- { filter: /\.(js|jsx|ts|tsx)$/ },
516
- async (args) => {
517
- let code = await Bun.file(args.path).text();
518
-
519
- // Replace "./asset.ext" "/asset.ext"
520
- code = code.replace(
521
- /(["'`])\.\/([^"'`]+\.(png|jpe?g|svg|webp|gif|avif))\1/g,
522
- (_, quote, asset) => `${quote}/${asset}${quote}`
523
- );
524
-
525
- return {
526
- contents: code,
527
- loader: args.path.endsWith("x") ? "tsx" : "js",
528
- };
529
- }
530
- );
531
- },
532
- };
533
- }
534
-
535
- /**
536
- * Step 4: Build the application's source code from the preprocessed temp directory
537
- */
538
- async function buildSrc(): Promise<void> {
539
- if (!fsSync.existsSync(SRC_DIR)) return;
540
-
541
- // Clean temp dir if exists
542
- if (fsSync.existsSync(TEMP_SRC_DIR)) {
543
- await fs.rm(TEMP_SRC_DIR, { recursive: true, force: true });
544
- }
545
-
546
- await preprocessSources(SRC_DIR, TEMP_SRC_DIR);
547
-
548
- const entrypoints: string[] = [];
549
- function collectEntries(dir: string): void {
550
- const items = fsSync.readdirSync(dir, { withFileTypes: true });
551
- for (const item of items) {
552
- const fullPath = path.join(dir, item.name);
553
- if (item.isDirectory()) {
554
- collectEntries(fullPath);
555
- } else if (/\.(ts|tsx|js|jsx)$/.test(item.name)) {
556
- entrypoints.push(fullPath);
557
- }
558
- }
559
- }
560
-
561
- collectEntries(TEMP_SRC_DIR);
562
-
563
- if (entrypoints.length === 0) {
564
- logger.info("No source files found in /src to build.");
565
- return;
566
- }
567
-
568
- const outDir = path.join(DIST_DIR, "src");
569
- await fs.mkdir(outDir, { recursive: true });
570
-
571
- // Build in chunks to avoid memory issues
572
- const CHUNK_SIZE = 10;
573
- for (let i = 0; i < entrypoints.length; i += CHUNK_SIZE) {
574
- const chunk = entrypoints.slice(i, i + CHUNK_SIZE);
575
-
576
- await build({
577
- entrypoints: chunk,
578
- outdir: outDir,
579
- root: TEMP_SRC_DIR,
580
- naming: { entry: "[dir]/[name].js" },
581
- jsxFactory: "e",
582
- jsxFragment: "Fragment",
583
- jsxImportSource: "vaderjs-native",
584
- target: "browser",
585
- publicPath: "/",
586
- env: 'inline',
587
- minify: false,
588
- assetNaming: "assets/[name]-[hash].[ext]",
589
- loader: {
590
- '.png': 'file',
591
- '.svg': 'file',
592
- '.txt': 'text',
593
- '.json': 'json'
594
- },
595
-
596
- external: ["vaderjs-native"],
597
- splitting: false,
598
- });
599
- }
600
- }
601
-
602
- /**
603
- * Step 5: Copy all assets from the `/public` directory to `/dist` (incremental)
604
- */
605
- async function copyPublicAssets(): Promise<void> {
606
- if (!fsSync.existsSync(PUBLIC_DIR)) return;
607
-
608
- await fs.mkdir(DIST_DIR, { recursive: true });
609
-
610
- const items = await fs.readdir(PUBLIC_DIR, { withFileTypes: true });
611
-
612
- await parallelForEach(items, async (item) => {
613
- const srcPath = path.join(PUBLIC_DIR, item.name);
614
- const destPath = path.join(DIST_DIR, item.name);
615
-
616
- if (item.isDirectory()) {
617
- await copyPublicAssetsRecursive(srcPath, destPath);
618
- } else {
619
- await copyIfNeeded(srcPath, destPath);
620
- }
621
- });
622
- }
623
-
624
- async function copyPublicAssetsRecursive(srcDir: string, destDir: string): Promise<void> {
625
- await fs.mkdir(destDir, { recursive: true });
626
- const items = await fs.readdir(srcDir, { withFileTypes: true });
627
-
628
- await parallelForEach(items, async (item) => {
629
- const srcPath = path.join(srcDir, item.name);
630
- const destPath = path.join(destDir, item.name);
631
-
632
- if (item.isDirectory()) {
633
- await copyPublicAssetsRecursive(srcPath, destPath);
634
- } else {
635
- await copyIfNeeded(srcPath, destPath);
636
- }
637
- });
638
- }
639
-
640
- // --- SINGLE PAGE APPLICATION BUILD ---
641
-
642
- const devClientScript = `
643
- <script type="module">
644
- // connect to ws server
645
- // reload on changes
646
- //
647
- const ws = new WebSocket('ws://' + location.host + '/__hmr');
648
- ws.onmessage = (event) => {
649
- const msg = event.data;
650
- if (msg === 'reload') {
651
- console.log('[VaderJS] Reloading due to changes...');
652
- location.reload();
653
- }
654
- };
655
- </script>
656
- `;
657
-
658
- async function buildSPAHtml(): Promise<void> {
659
- const html = `<!DOCTYPE html>
660
- <html lang="en">
661
- <head>
662
- <meta charset="UTF-8" />
663
- <meta name="viewport" content="width=device-width, initial-scale=1" />
664
- <title>${config.app?.name ?? "Vader App"}</title>
665
- ${htmlInjections.join("\n")}
666
- </head>
667
- <body>
668
- <div id="app"></div>
669
- <script src="/App.js " type="module"></script>
670
- ${isDev ? devClientScript : ""}
671
- </body>
672
- </html>`;
673
-
674
- await fs.writeFile(path.join(DIST_DIR, "index.html"), html);
675
- }
676
-
677
- async function buildRouteComponents(isDev: boolean): Promise<Record<string, string>> {
678
- const routeFiles = await findRouteFiles();
679
- const routeMap: Record<string, string> = {};
680
-
681
- if (routeFiles.length === 0) {
682
- logger.info("No route files found in src/pages or src/routes");
683
- return routeMap;
684
- }
685
-
686
- // Create routes directory
687
- const routesDir = path.join(DIST_DIR, "routes");
688
- await fs.mkdir(routesDir, { recursive: true });
689
-
690
- // Build each route component separately
691
- for (const routeFile of routeFiles) {
692
- // Determine route path from file structure
693
- const relativePath = path.relative(PROJECT_ROOT, routeFile);
694
- let routePath = "/";
695
-
696
- // Convert file path to route path
697
- // Example: src/pages/home/index.tsx -> /home
698
- // Example: src/pages/about.tsx -> /about
699
- if (relativePath.includes("src/pages/")) {
700
- routePath = "/" + relativePath
701
- .replace("src/pages/", "")
702
- .replace(/\/index\.(tsx|jsx)$/, "")
703
- .replace(/\.(tsx|jsx)$/, "")
704
- .replace(/\/$/, "");
705
- } else if (relativePath.includes("src/routes/")) {
706
- routePath = "/" + relativePath
707
- .replace("src/routes/", "")
708
- .replace(/\/index\.(tsx|jsx)$/, "")
709
- .replace(/\.(tsx|jsx)$/, "")
710
- .replace(/\/$/, "");
711
- }
712
-
713
- // Handle root route
714
- if (routePath === "/index") routePath = "/";
715
-
716
- // Generate component name from route
717
- const componentName = routePath === "/" ? "Home" :
718
- routePath.slice(1).split('/').map(part =>
719
- part.charAt(0).toUpperCase() + part.slice(1)
720
- ).join('');
721
-
722
- // Build the component
723
- const outputFile = `${componentName}.js`;
724
- const outputPath = path.join(routesDir, outputFile);
725
-
726
- await build({
727
- entrypoints: [routeFile],
728
- outdir: routesDir,
729
- naming: { entry: componentName + ".js" },
730
- target: "browser",
731
- minify: !isDev,
732
- sourcemap: isDev ? "inline" : "external",
733
- jsxFactory: "Vader.createElement",
734
- env: 'inline',
735
- jsxFragment: "Fragment",
736
- plugins: [publicAssetPlugin()],
737
- external: ["vaderjs-native"],
738
- });
739
-
740
- routeMap[routePath] = `./routes/${outputFile}`;
741
- }
742
-
743
- return routeMap;
744
- }
745
-
746
-
747
-
748
- async function buildAppEntry(isDev: boolean): Promise<void> {
749
- const appFile = findAppFile();
750
-
751
- if (!appFile) {
752
- logger.error("No App.tsx or App.jsx found in project root!");
753
- throw new Error("Missing App.tsx/App.jsx in project root");
754
- }
755
-
756
- // Build the App component
757
- await build({
758
- entrypoints: [appFile],
759
- outdir: DIST_DIR,
760
- target: "browser",
761
- minify: !isDev,
762
- sourcemap: isDev ? "inline" : "external",
763
- jsxFactory: "Vader.createElement",
764
- jsxFragment: "Fragment",
765
- env: "inline",
766
- naming: "App.js",
767
- plugins: [publicAssetPlugin()],
768
- external: ["./routes.manifest.js"],
769
- });
770
- }
771
-
772
- async function buildMainRuntime(isDev: boolean): Promise<void> {
773
-
774
- }
775
-
776
- async function buildSPA(isDev: boolean): Promise<void> {
777
- logger.step("Building SPA");
778
-
779
- // 1. Generate single HTML file
780
- await buildSPAHtml();
781
-
782
- // 2. Build route components separately
783
- const routeMap = await buildRouteComponents(isDev);
784
-
785
-
786
- // 4. Build App component
787
- await buildAppEntry(isDev);
788
-
789
- // 5. Build main runtime that ties everything together
790
- await buildMainRuntime(isDev);
791
- }
792
-
793
- async function buildMPA(isDev: boolean): Promise<void> {
794
- logger.step("Building MPA (Single page with all routes bundled)");
795
-
796
- const appFile = findAppFile();
797
-
798
- if (!appFile) {
799
- logger.warn("No App.tsx or App.jsx found for MPA mode.");
800
- return;
801
- }
802
-
803
- // For MPA, just bundle everything together
804
- await buildSPAHtml();
805
-
806
- // Build single bundle with all routes
807
- await build({
808
- entrypoints: [appFile],
809
- outdir: DIST_DIR,
810
- target: "browser",
811
- minify: !isDev,
812
- sourcemap: isDev ? "inline" : "external",
813
- jsxFactory: "Vader.createElement",
814
- jsxFragment: "Fragment",
815
- naming: "index.js",
816
- plugins: [publicAssetPlugin()],
817
- external: [],
818
- });
819
- }
820
-
821
- async function buildAppEntrypoints(isDev = false): Promise<void> {
822
- const appFile = findAppFile();
823
-
824
- if (!appFile) {
825
- logger.warn("No App.tsx or App.jsx found in project root.");
826
- return;
827
- }
828
-
829
- if (config.build_type === "spa") {
830
- await buildSPA(isDev);
831
- } else {
832
- await buildMPA(isDev);
833
- }
834
- }
835
-
836
- // --- MAIN BUILD FUNCTION ---
837
- export async function buildAll(isDev = false): Promise<void> {
838
- config = await loadConfig();
839
- logger.info(`Starting VaderJS ${isDev ? 'development' : 'production'} build...`);
840
- const totalTime = performance.now();
841
-
842
- await runPluginHook("onBuildStart");
843
-
844
- // Clean dist directory only if not in dev mode or if it doesn't exist
845
- if (!isDev || !fsSync.existsSync(DIST_DIR)) {
846
- await fs.rm(DIST_DIR, { recursive: true, force: true });
847
- await fs.mkdir(DIST_DIR, { recursive: true });
848
- } else if (isDev) {
849
- // In dev mode, only clean if explicitly requested
850
- const needsClean = process.env.VADER_CLEAN === 'true';
851
- if (needsClean) {
852
- await fs.rm(DIST_DIR, { recursive: true, force: true });
853
- await fs.mkdir(DIST_DIR, { recursive: true });
854
- }
855
- }
856
-
857
- // Run build steps in optimal order with parallelization where possible
858
- const buildSteps = [
859
- { name: "Building VaderJS Core", fn: buildVaderCore },
860
- { name: "Building App Source (/src)", fn: buildSrc },
861
- { name: "Copying Public Assets", fn: copyPublicAssets },
862
- { name: "Building App Entrypoints", fn: () => buildAppEntrypoints(isDev) },
863
- ];
864
-
865
- for (const step of buildSteps) {
866
- await timedStep(step.name, step.fn);
867
- }
868
-
869
- await runPluginHook("onBuildFinish");
870
-
871
- // Cache cleanup for old entries
872
- if (buildCache.size > 1000) {
873
- const keys = Array.from(buildCache.keys()).slice(0, 500);
874
- for (const key of keys) {
875
- buildCache.delete(key);
876
- }
877
- }
878
-
879
- const duration = (performance.now() - totalTime).toFixed(2);
880
- logger.success(`Build completed in ${duration}ms. Output in ${DIST_DIR}`);
881
- }
882
-
883
- // --- SCRIPT ENTRYPOINT ---
884
- async function main(): Promise<void> {
885
- // Banner
886
- console.log(`${colors.magenta}
887
- __ __ ____ ____ _______ __
888
- | | / |/ __ \\ / __ \\ / ____/ |/ /
889
- | | / / / / // /_/ // /___ | /
890
- | | / / /_/ / \\____// /___ / |
891
- |____/____/_____/ /_____/ |_| |_|
892
- ${colors.reset}`);
893
-
894
- const command = process.argv[2];
895
- const arg = process.argv[3];
896
-
897
- // Set global flags
898
- globalThis.isDev = command?.includes('dev') || false;
899
- globalThis.isBuildingForWindows = command?.includes('windows') || false;
900
-
901
- // Commands that don't require config
902
- if (command === "init") {
903
- await initProject(arg);
904
- return;
905
- }
906
-
907
- // Load config for runtime commands
908
- config = await loadConfig();
909
- config.port = config.port || 3000;
910
-
911
- // Command router
912
- const commandHandlers: Record<string, () => Promise<void>> = {
913
- 'add': async () => {
914
- if (!arg) {
915
- logger.error("Please specify a plugin to add.");
916
- process.exit(1);
917
- }
918
- await addPlugin(arg);
919
- },
920
- 'list_plugins': async () => {
921
- await listPlugins();
922
- },
923
- 'remove': async () => {
924
- if (!arg) {
925
- logger.error("Please specify a plugin to remove.");
926
- process.exit(1);
927
- }
928
- await removePlugin(arg);
929
- },
930
- 'dev': async () => {
931
- globalThis.isDev = true;
932
- await runDevServer("web");
933
- },
934
- 'android:dev': async () => {
935
- await buildAll(true);
936
- await androidDev();
937
- },
938
- 'android:build': async () => {
939
- await buildAll(false);
940
- await buildAndroid(false);
941
- logger.success("Android build completed 🚀");
942
- },
943
- 'windows:dev': async () => {
944
- await buildAll(true);
945
- await buildWindows(true);
946
- await runDevServer("web");
947
- await openWinApp();
948
- },
949
- 'windows:build': async () => {
950
- await buildAll(false);
951
- await buildWindows(false);
952
- logger.success("Windows build completed 🚀");
953
- },
954
- 'build': async () => {
955
- await buildAll(false);
956
- },
957
- 'serve': async () => {
958
- await buildAll(false);
959
- await runProdServer();
960
- }
961
- };
962
-
963
- if (command && command in commandHandlers) {
964
- await commandHandlers[command]();
965
- } else {
966
- logger.error(`Unknown command: ${command ?? ""}`);
967
- logger.info(`
968
- Available commands:
969
- dev Start dev server
970
- build Build for production
971
- serve Build + serve production
972
- init [dir] Create a new Vader project
973
- add <plugin> Add a Vader plugin
974
- remove <plugin> Remove a Vader plugin
975
- list_plugins List currently installed Vaderjs plugins
976
- android:dev Start Android development
977
- android:build Build Android app
978
- windows:dev Start Windows development
979
- windows:build Build Windows app
980
- `.trim());
981
- process.exit(1);
982
- }
983
- }
984
-
985
- // Stub functions for plugin management
986
- async function addPlugin(pluginName: string): Promise<void> {
987
- logger.info(`Adding plugin: ${pluginName}`);
988
- // TODO: Implement plugin addition
989
- logger.warn("Plugin addition not yet implemented");
990
- }
991
-
992
- async function removePlugin(pluginName: string): Promise<void> {
993
- logger.info(`Removing plugin: ${pluginName}`);
994
- // TODO: Implement plugin removal
995
- logger.warn("Plugin removal not yet implemented");
996
- }
997
-
998
- async function listPlugins(): Promise<void> {
999
- logger.info("Currently installed plugins:");
1000
- if (config.plugins && config.plugins.length > 0) {
1001
- config.plugins.forEach((plugin, index) => {
1002
- logger.info(` ${index + 1}. ${plugin.name || 'Unnamed plugin'}`);
1003
- });
1004
- } else {
1005
- logger.info(" No plugins installed.");
1006
- }
1007
- }
1008
-
1009
- // Error handling
1010
- process.on("unhandledRejection", (err) => {
1011
- logger.error("Unhandled Promise rejection:", err);
1012
- });
1013
-
1014
- process.on("uncaughtException", (err) => {
1015
- logger.error("Uncaught Exception:", err);
1016
- process.exit(1);
1017
- });
1018
-
1019
- // Start the application
1020
- if (require.main === module) {
1021
- main().catch(err => {
1022
- logger.error("An unexpected error occurred:", err);
1023
- process.exit(1);
1024
- });
1025
- }
1026
-
1
+ #!/usr/bin/env bun
2
+
3
+ import { build, serve } from "bun";
4
+ import fs from "fs/promises";
5
+ import fsSync from "fs";
6
+ import path from "path";
7
+ import { initProject } from "./cli";
8
+ import { logger, timedStep } from "./cli/logger";
9
+ import { colors } from "./cli/logger";
10
+ import runDevServer from "vaderjs-native/cli/web/server";
11
+ import { runProdServer } from "vaderjs-native/cli/web/server";
12
+ import { androidDev } from "./cli/android/dev.js";
13
+ import { buildAndroid } from "./cli/android/build";
14
+ import { buildWindows } from "./cli/windows/build";
15
+ import openWinApp from "./cli/windows/dev";
16
+ import { Config } from "./config";
17
+
18
+ // --- CONSTANTS ---
19
+ const PROJECT_ROOT = process.cwd();
20
+ const PUBLIC_DIR = path.join(PROJECT_ROOT, "public");
21
+ const DIST_DIR = path.join(PROJECT_ROOT, "dist");
22
+ const SRC_DIR = path.join(PROJECT_ROOT, "src");
23
+ const VADER_SRC_PATH = path.join(PROJECT_ROOT, "node_modules", "vaderjs-native", "index.ts");
24
+ const TEMP_SRC_DIR = path.join(PROJECT_ROOT, ".vader_temp_src");
25
+
26
+ // --- CACHE SYSTEM ---
27
+ const buildCache = new Map<string, { mtime: number; hash: string }>();
28
+ const configCache = new Map<string, { config: Config; mtime: number }>();
29
+ let config: Config = {};
30
+ let htmlInjections: string[] = [];
31
+
32
+ // --- SIMPLIFIED WATCHER ---
33
+ class FileWatcher {
34
+ private watchers: Map<string, any>;
35
+ private onChangeCallbacks: Array<(filePath: string) => void>;
36
+ private isRebuilding: boolean;
37
+ private lastRebuildTime: number;
38
+ private readonly REBUILD_COOLDOWN = 1000;
39
+
40
+ constructor() {
41
+ this.watchers = new Map();
42
+ this.onChangeCallbacks = [];
43
+ this.isRebuilding = false;
44
+ this.lastRebuildTime = 0;
45
+ }
46
+
47
+ shouldIgnorePath(filePath: string): boolean {
48
+ const normalized = path.normalize(filePath);
49
+ // Ignore dist folder and its contents
50
+ if (normalized.includes(path.normalize(DIST_DIR))) {
51
+ return true;
52
+ }
53
+ // Ignore node_modules
54
+ if (normalized.includes(path.normalize('node_modules'))) {
55
+ return true;
56
+ }
57
+ // Ignore .git folder
58
+ if (normalized.includes(path.normalize('.git'))) {
59
+ return true;
60
+ }
61
+ // Ignore the temporary source directory
62
+ if (normalized.includes(path.normalize(TEMP_SRC_DIR))) {
63
+ return true;
64
+ }
65
+ return false;
66
+ }
67
+
68
+ async watchDirectory(dirPath: string, recursive = true): Promise<void> {
69
+ // Skip if directory should be ignored
70
+ if (this.shouldIgnorePath(dirPath) || !fsSync.existsSync(dirPath)) {
71
+ return;
72
+ }
73
+
74
+ try {
75
+ // Close existing watcher if any
76
+ if (this.watchers.has(dirPath)) {
77
+ try {
78
+ this.watchers.get(dirPath).close();
79
+ } catch (err) {
80
+ // Ignore close errors
81
+ }
82
+ }
83
+
84
+ // Create new watcher
85
+ const watcher = fsSync.watch(dirPath, { recursive }, (eventType: string, filename: string | null) => {
86
+ if (!filename) return;
87
+
88
+ const changedFile = path.join(dirPath, filename);
89
+ const normalizedChanged = path.normalize(changedFile);
90
+
91
+ // Skip if file should be ignored
92
+ if (this.shouldIgnorePath(normalizedChanged)) {
93
+ return;
94
+ }
95
+
96
+ // Check if this is a file we care about
97
+ if (this.shouldTriggerRebuild(normalizedChanged)) {
98
+ logger.info(`File changed: ${path.relative(PROJECT_ROOT, normalizedChanged)}`);
99
+
100
+ // Only trigger if not already rebuilding and cooldown has passed
101
+ const now = Date.now();
102
+ if (!this.isRebuilding && (now - this.lastRebuildTime) > this.REBUILD_COOLDOWN) {
103
+ this.triggerChange(normalizedChanged);
104
+ } else if (this.isRebuilding) {
105
+ logger.info(`Skipping rebuild - already rebuilding`);
106
+ } else {
107
+ logger.info(`Skipping rebuild - cooldown period`);
108
+ }
109
+ }
110
+ });
111
+
112
+ watcher.on('error', (err: Error) => {
113
+ logger.warn(`Watcher error on ${dirPath}:`, err.message);
114
+ });
115
+
116
+ this.watchers.set(dirPath, watcher);
117
+
118
+ logger.info(`Watching directory: ${path.relative(PROJECT_ROOT, dirPath)}`);
119
+ } catch (err: any) {
120
+ logger.warn(`Could not watch directory ${dirPath}:`, err.message);
121
+ }
122
+ }
123
+
124
+ shouldTriggerRebuild(filePath: string): boolean {
125
+ // Only trigger rebuild for specific file types
126
+ const ext = path.extname(filePath).toLowerCase();
127
+ const triggerExtensions = ['.js', '.jsx', '.ts', '.tsx', '.css', '.html', '.json', '.config.js', '.config.ts'];
128
+ return triggerExtensions.includes(ext) || ext === '';
129
+ }
130
+
131
+ triggerChange(filePath: string): void {
132
+ for (const callback of this.onChangeCallbacks) {
133
+ try {
134
+ callback(filePath);
135
+ } catch (err) {
136
+ logger.error("Change callback error:", err);
137
+ }
138
+ }
139
+ }
140
+
141
+ onChange(callback: (filePath: string) => void): () => void {
142
+ this.onChangeCallbacks.push(callback);
143
+ return () => {
144
+ const index = this.onChangeCallbacks.indexOf(callback);
145
+ if (index > -1) this.onChangeCallbacks.splice(index, 1);
146
+ };
147
+ }
148
+
149
+ setRebuilding(state: boolean): void {
150
+ this.isRebuilding = state;
151
+ if (state) {
152
+ this.lastRebuildTime = Date.now();
153
+ }
154
+ }
155
+
156
+ clear(): void {
157
+ for (const [dir, watcher] of this.watchers) {
158
+ try {
159
+ watcher.close();
160
+ } catch (err) {
161
+ // Ignore close errors
162
+ }
163
+ }
164
+ this.watchers.clear();
165
+ this.onChangeCallbacks = [];
166
+ this.isRebuilding = false;
167
+ }
168
+ }
169
+
170
+ const watcher = new FileWatcher();
171
+
172
+ // --- CONFIG & PLUGIN SYSTEM ---
173
+ interface VaderAPI {
174
+ runCommand: (cmd: string | string[]) => Promise<void>;
175
+ injectHTML: (content: string) => void;
176
+ log: {
177
+ warn: (msg: string) => void;
178
+ info: (msg: string) => void;
179
+ success: (msg: string) => void;
180
+ step: (msg: string) => void;
181
+ };
182
+ getProjectRoot: () => string;
183
+ getDistDir: () => string;
184
+ getPublicDir: () => string;
185
+ }
186
+
187
+ const vaderAPI: VaderAPI = {
188
+ runCommand: async (cmd) => {
189
+ if (typeof cmd === "string") cmd = cmd.split(" ");
190
+ const p = Bun.spawn(cmd);
191
+ await p.exited;
192
+ },
193
+ injectHTML: (content) => htmlInjections.push(content),
194
+ log: {
195
+ warn: (msg) => logger.warn(msg),
196
+ info: (msg) => logger.info(msg),
197
+ success: (msg) => logger.success(msg),
198
+ step: (msg) => logger.step(msg)
199
+ },
200
+ getProjectRoot: () => PROJECT_ROOT,
201
+ getDistDir: () => DIST_DIR,
202
+ getPublicDir: () => PUBLIC_DIR,
203
+ };
204
+
205
+ // Optimized config loading with cache
206
+ export async function loadConfig(projectDir?: string): Promise<Config> {
207
+ projectDir = projectDir || process.cwd();
208
+ const configKey = `config-${projectDir}`;
209
+
210
+ const configPathJs = path.join(projectDir, "vaderjs.config.js");
211
+ const configPathTs = path.join(projectDir, "vaderjs.config.ts");
212
+
213
+ let configPath: string | null = null;
214
+ let stat: any = null;
215
+
216
+ // Find which config file exists
217
+ try {
218
+ stat = await fs.stat(configPathTs);
219
+ configPath = configPathTs;
220
+ } catch {
221
+ try {
222
+ stat = await fs.stat(configPathJs);
223
+ configPath = configPathJs;
224
+ } catch {
225
+ return {}; // No config file
226
+ }
227
+ }
228
+
229
+ // Check cache
230
+ if (stat && configPath) {
231
+ const cached = configCache.get(configKey);
232
+ if (cached && cached.mtime === stat.mtimeMs) {
233
+ return cached.config;
234
+ }
235
+
236
+ // Load config
237
+ const userConfig = (await import(`file://${configPath}`)).default;
238
+ configCache.set(configKey, { config: userConfig, mtime: stat.mtimeMs });
239
+ return userConfig;
240
+ }
241
+
242
+ return {};
243
+ }
244
+
245
+ export function defineConfig(config: Config): Config {
246
+ return config;
247
+ }
248
+
249
+ async function runPluginHook(hookName: string): Promise<void> {
250
+ if (!config.plugins) return;
251
+
252
+ const pluginPromises = config.plugins.map(async (plugin) => {
253
+ if (typeof plugin[hookName] === "function") {
254
+ try {
255
+ await plugin[hookName](vaderAPI);
256
+ } catch (e) {
257
+ logger.error(`Plugin hook error (${hookName} in ${plugin.name || 'anonymous'}):`, e);
258
+ }
259
+ }
260
+ });
261
+
262
+ await Promise.all(pluginPromises);
263
+ }
264
+
265
+ // --- OPTIMIZED BUILD HELPERS ---
266
+
267
+ // Helper to find App.tsx in project root
268
+ function findAppFile(): string | null {
269
+ const possiblePaths = [
270
+ path.join(PROJECT_ROOT, "App.tsx"),
271
+ path.join(PROJECT_ROOT, "App.jsx"),
272
+ path.join(PROJECT_ROOT, "App.ts"),
273
+ path.join(PROJECT_ROOT, "App.js")
274
+ ];
275
+
276
+ for (const appPath of possiblePaths) {
277
+ if (fsSync.existsSync(appPath)) {
278
+ return appPath;
279
+ }
280
+ }
281
+
282
+ return null;
283
+ }
284
+
285
+ // Helper to find route files in src/pages or src/routes
286
+ async function findRouteFiles(): Promise<string[]> {
287
+ const routes: string[] = [];
288
+
289
+ // Look for pages in src/pages or src/routes
290
+ const possibleDirs = [
291
+ path.join(PROJECT_ROOT, "src", "pages"),
292
+ path.join(PROJECT_ROOT, "src", "routes")
293
+ ];
294
+
295
+ for (const dir of possibleDirs) {
296
+ if (fsSync.existsSync(dir)) {
297
+ await collectRouteFiles(dir, routes);
298
+ }
299
+ }
300
+
301
+ return routes;
302
+ }
303
+
304
+ async function collectRouteFiles(dir: string, routes: string[]): Promise<void> {
305
+ const entries = await fs.readdir(dir, { withFileTypes: true });
306
+
307
+ for (const entry of entries) {
308
+ const fullPath = path.join(dir, entry.name);
309
+
310
+ if (entry.isDirectory()) {
311
+ await collectRouteFiles(fullPath, routes);
312
+ } else if (entry.name.endsWith('.tsx') || entry.name.endsWith('.jsx')) {
313
+ // Check if it's a route component (not layout or other)
314
+ if (!entry.name.includes('.layout.') && !entry.name.includes('.component.')) {
315
+ routes.push(fullPath);
316
+ }
317
+ }
318
+ }
319
+ }
320
+
321
+ // File hashing for cache invalidation
322
+ async function getFileHash(filepath: string): Promise<string> {
323
+ const content = await fs.readFile(filepath);
324
+ return Bun.hash(content).toString(16);
325
+ }
326
+
327
+ // Check if file needs rebuild
328
+ async function needsRebuild(sourcePath: string, destPath: string): Promise<boolean> {
329
+ try {
330
+ const [sourceStat, destStat] = await Promise.all([
331
+ fs.stat(sourcePath),
332
+ fs.stat(destPath).catch(() => null)
333
+ ]);
334
+
335
+ if (!destStat) return true;
336
+
337
+ const cacheKey = `${sourcePath}:${destPath}`;
338
+ const cached = buildCache.get(cacheKey);
339
+
340
+ if (cached && cached.mtime === sourceStat.mtimeMs) {
341
+ return false;
342
+ }
343
+
344
+ // Check if content changed
345
+ const hash = await getFileHash(sourcePath);
346
+ if (cached && cached.hash === hash) {
347
+ buildCache.set(cacheKey, { mtime: sourceStat.mtimeMs, hash });
348
+ return false;
349
+ }
350
+
351
+ return true;
352
+ } catch {
353
+ return true;
354
+ }
355
+ }
356
+
357
+ // Parallel file operations
358
+ async function parallelForEach<T>(
359
+ items: T[],
360
+ callback: (item: T, index: number) => Promise<void>,
361
+ concurrency = 4
362
+ ): Promise<void> {
363
+ const chunks = [];
364
+ for (let i = 0; i < items.length; i += concurrency) {
365
+ chunks.push(items.slice(i, i + concurrency));
366
+ }
367
+
368
+ for (const chunk of chunks) {
369
+ await Promise.all(chunk.map((item, index) => callback(item, index)));
370
+ }
371
+ }
372
+
373
+ async function copyIfNeeded(src: string, dest: string): Promise<void> {
374
+ const cacheKey = `copy-${src}`;
375
+ const stat = await fs.stat(src).catch(() => null);
376
+ if (!stat) return;
377
+
378
+ const cached = buildCache.get(cacheKey);
379
+ if (cached && cached.mtime === stat.mtimeMs) {
380
+ return;
381
+ }
382
+
383
+ await fs.copyFile(src, dest);
384
+ buildCache.set(cacheKey, { mtime: stat.mtimeMs, hash: await getFileHash(src) });
385
+ }
386
+
387
+ // --- BUILD LOGIC ---
388
+
389
+ /**
390
+ * Step 1: Transpile and bundle the core vaderjs library (cached)
391
+ */
392
+ async function buildVaderCore(): Promise<void> {
393
+ if (!fsSync.existsSync(VADER_SRC_PATH)) {
394
+ logger.error("VaderJS source not found:", VADER_SRC_PATH);
395
+ throw new Error("Missing vaderjs dependency.");
396
+ }
397
+
398
+ const outDir = path.join(DIST_DIR, "src", "vader");
399
+ const mainOutput = path.join(outDir, "index.js");
400
+
401
+ // Check if rebuild is needed
402
+ if (!(await needsRebuild(VADER_SRC_PATH, mainOutput))) {
403
+ logger.info("VaderJS Core is up to date");
404
+ return;
405
+ }
406
+
407
+ await fs.mkdir(outDir, { recursive: true });
408
+
409
+ await build({
410
+ entrypoints: [VADER_SRC_PATH],
411
+ outdir: outDir,
412
+ target: "browser",
413
+ minify: false,
414
+ sourcemap: "external",
415
+ jsxFactory: "e",
416
+ jsxFragment: "Fragment",
417
+ jsxImportSource: "vaderjs",
418
+ external: [], // Bundle everything
419
+ });
420
+
421
+ // Update cache
422
+ const stat = await fs.stat(VADER_SRC_PATH);
423
+ const hash = await getFileHash(VADER_SRC_PATH);
424
+ buildCache.set(`${VADER_SRC_PATH}:${mainOutput}`, { mtime: stat.mtimeMs, hash });
425
+ }
426
+
427
+ /**
428
+ * Step 2: Patches source code to remove server-side hook imports
429
+ */
430
+ function patchHooksUsage(code: string): string {
431
+ return code.replace(/import\s+{[^}]*use(State|Effect|Memo|Navigation)[^}]*}\s+from\s+['"]vaderjs['"];?\n?/g, "");
432
+ }
433
+
434
+ function publicAssetPlugin() {
435
+ const assetCache = new Map<string, string>();
436
+
437
+ return {
438
+ name: "public-asset-replacer",
439
+ setup(build: any) {
440
+ build.onLoad({ filter: /\.(js|ts|jsx|tsx|html)$/ }, async (args: any) => {
441
+ const stat = await fs.stat(args.path).catch(() => null);
442
+ if (!stat) return null;
443
+
444
+ const cacheKey = `asset-${args.path}`;
445
+ const cached = assetCache.get(cacheKey);
446
+ if (cached && stat.mtimeMs <= (await fs.stat(args.path).catch(() => ({ mtimeMs: 0 }))).mtimeMs) {
447
+ return { contents: cached, loader: getLoader(args.path) };
448
+ }
449
+
450
+ let code = await fs.readFile(args.path, "utf8");
451
+
452
+ // Process asset paths in parallel
453
+ const assetMatches = [...code.matchAll(/\{\{public:(.+?)\}\}/g)];
454
+ const processedAssets = await Promise.all(
455
+ assetMatches.map(async (match) => {
456
+ const relPath = match[1].trim();
457
+ const absPath = path.join(PUBLIC_DIR, relPath);
458
+ try {
459
+ await fs.access(absPath);
460
+ return { match: match[0], replacement: "/" + relPath.replace(/\\/g, "/") };
461
+ } catch {
462
+ logger.warn(`Public asset not found: ${relPath}`);
463
+ return { match: match[0], replacement: relPath };
464
+ }
465
+ })
466
+ );
467
+
468
+ for (const { match, replacement } of processedAssets) {
469
+ code = code.replace(match, replacement);
470
+ }
471
+
472
+ assetCache.set(cacheKey, code);
473
+ return { contents: code, loader: getLoader(args.path) };
474
+ });
475
+ },
476
+ };
477
+ }
478
+
479
+ function getLoader(filepath: string): string {
480
+ if (filepath.endsWith(".html")) return "text";
481
+ if (filepath.endsWith(".tsx")) return "tsx";
482
+ if (filepath.endsWith(".jsx")) return "jsx";
483
+ if (filepath.endsWith(".ts")) return "ts";
484
+ return "js";
485
+ }
486
+
487
+ /**
488
+ * Step 3: Pre-processes all files in `/src` into a temporary directory (parallel)
489
+ */
490
+ async function preprocessSources(srcDir: string, tempDir: string): Promise<void> {
491
+ await fs.mkdir(tempDir, { recursive: true });
492
+
493
+ const entries = await fs.readdir(srcDir, { withFileTypes: true });
494
+
495
+ await parallelForEach(entries, async (entry) => {
496
+ const srcPath = path.join(srcDir, entry.name);
497
+ const destPath = path.join(tempDir, entry.name);
498
+
499
+ if (entry.isDirectory()) {
500
+ await preprocessSources(srcPath, destPath);
501
+ } else if (/\.(tsx|jsx|ts|js)$/.test(entry.name)) {
502
+ const content = await fs.readFile(srcPath, "utf8");
503
+ const processed = patchHooksUsage(content);
504
+ await fs.writeFile(destPath, processed);
505
+ } else {
506
+ await fs.copyFile(srcPath, destPath);
507
+ }
508
+ });
509
+ }
510
+
511
+ /**
512
+ * Step 4: Build the application's source code from the preprocessed temp directory
513
+ */
514
+ async function buildSrc(): Promise<void> {
515
+ if (!fsSync.existsSync(SRC_DIR)) return;
516
+
517
+ // Clean temp dir if exists
518
+ if (fsSync.existsSync(TEMP_SRC_DIR)) {
519
+ await fs.rm(TEMP_SRC_DIR, { recursive: true, force: true });
520
+ }
521
+
522
+ await preprocessSources(SRC_DIR, TEMP_SRC_DIR);
523
+
524
+ const entrypoints: string[] = [];
525
+ function collectEntries(dir: string): void {
526
+ const items = fsSync.readdirSync(dir, { withFileTypes: true });
527
+ for (const item of items) {
528
+ const fullPath = path.join(dir, item.name);
529
+ if (item.isDirectory()) {
530
+ collectEntries(fullPath);
531
+ } else if (/\.(ts|tsx|js|jsx)$/.test(item.name)) {
532
+ entrypoints.push(fullPath);
533
+ }
534
+ }
535
+ }
536
+
537
+ collectEntries(TEMP_SRC_DIR);
538
+
539
+ if (entrypoints.length === 0) {
540
+ logger.info("No source files found in /src to build.");
541
+ return;
542
+ }
543
+
544
+ const outDir = path.join(DIST_DIR, "src");
545
+ await fs.mkdir(outDir, { recursive: true });
546
+
547
+ // Build in chunks to avoid memory issues
548
+ const CHUNK_SIZE = 10;
549
+ for (let i = 0; i < entrypoints.length; i += CHUNK_SIZE) {
550
+ const chunk = entrypoints.slice(i, i + CHUNK_SIZE);
551
+
552
+ await build({
553
+ entrypoints: chunk,
554
+ outdir: outDir,
555
+ root: TEMP_SRC_DIR,
556
+ naming: { entry: "[dir]/[name].js" },
557
+ jsxFactory: "e",
558
+ jsxFragment: "Fragment",
559
+ jsxImportSource: "vaderjs-native",
560
+ target: "browser",
561
+ publicPath: "/",
562
+ env: 'inline',
563
+ minify: false,
564
+ assetNaming: "assets/[name]-[hash].[ext]",
565
+ loader: {
566
+ '.png': 'file',
567
+ '.svg': 'file',
568
+ '.txt': 'text',
569
+ '.json': 'json'
570
+ },
571
+
572
+ external: ["vaderjs-native"],
573
+ splitting: false,
574
+ });
575
+ }
576
+ }
577
+
578
+ /**
579
+ * Step 5: Copy all assets from the `/public` directory to `/dist` (incremental)
580
+ */
581
+ async function copyPublicAssets(): Promise<void> {
582
+ if (!fsSync.existsSync(PUBLIC_DIR)) return;
583
+
584
+ await fs.mkdir(DIST_DIR, { recursive: true });
585
+
586
+ const items = await fs.readdir(PUBLIC_DIR, { withFileTypes: true });
587
+
588
+ await parallelForEach(items, async (item) => {
589
+ const srcPath = path.join(PUBLIC_DIR, item.name);
590
+ const destPath = path.join(DIST_DIR, item.name);
591
+
592
+ if (item.isDirectory()) {
593
+ await copyPublicAssetsRecursive(srcPath, destPath);
594
+ } else {
595
+ await copyIfNeeded(srcPath, destPath);
596
+ }
597
+ });
598
+ }
599
+
600
+ async function copyPublicAssetsRecursive(srcDir: string, destDir: string): Promise<void> {
601
+ await fs.mkdir(destDir, { recursive: true });
602
+ const items = await fs.readdir(srcDir, { withFileTypes: true });
603
+
604
+ await parallelForEach(items, async (item) => {
605
+ const srcPath = path.join(srcDir, item.name);
606
+ const destPath = path.join(destDir, item.name);
607
+
608
+ if (item.isDirectory()) {
609
+ await copyPublicAssetsRecursive(srcPath, destPath);
610
+ } else {
611
+ await copyIfNeeded(srcPath, destPath);
612
+ }
613
+ });
614
+ }
615
+
616
+ // --- SINGLE PAGE APPLICATION BUILD ---
617
+
618
+ const devClientScript = `
619
+ <script type="module">
620
+ // connect to ws server
621
+ // reload on changes
622
+ //
623
+ const ws = new WebSocket('ws://' + location.host + '/__hmr');
624
+ ws.onmessage = (event) => {
625
+ const msg = event.data;
626
+ if (msg === 'reload') {
627
+ console.log('[VaderJS] Reloading due to changes...');
628
+ location.reload();
629
+ }
630
+ };
631
+ </script>
632
+ `;
633
+
634
+ async function buildSPAHtml(): Promise<void> {
635
+ const html = `<!DOCTYPE html>
636
+ <html lang="en">
637
+ <head>
638
+ <meta charset="UTF-8" />
639
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
640
+ <title>${config.app?.name ?? "Vader App"}</title>
641
+ ${htmlInjections.join("\n")}
642
+ </head>
643
+ <body>
644
+ <div id="app"></div>
645
+ <script src="/index.js " type="module"></script>
646
+ ${isDev ? devClientScript : ""}
647
+ </body>
648
+ </html>`;
649
+
650
+ await fs.writeFile(path.join(DIST_DIR, "index.html"), html);
651
+ }
652
+
653
+ async function buildRouteComponents(isDev: boolean): Promise<Record<string, string>> {
654
+ const routeFiles = await findRouteFiles();
655
+ const routeMap: Record<string, string> = {};
656
+
657
+ if (routeFiles.length === 0) {
658
+ logger.info("No route files found in src/pages or src/routes");
659
+ return routeMap;
660
+ }
661
+
662
+ // Create routes directory
663
+ const routesDir = path.join(DIST_DIR, "routes");
664
+ await fs.mkdir(routesDir, { recursive: true });
665
+
666
+ // Build each route component separately
667
+ for (const routeFile of routeFiles) {
668
+ // Determine route path from file structure
669
+ const relativePath = path.relative(PROJECT_ROOT, routeFile);
670
+ let routePath = "/";
671
+
672
+ // Convert file path to route path
673
+ // Example: src/pages/home/index.tsx -> /home
674
+ // Example: src/pages/about.tsx -> /about
675
+ if (relativePath.includes("src/pages/")) {
676
+ routePath = "/" + relativePath
677
+ .replace("src/pages/", "")
678
+ .replace(/\/index\.(tsx|jsx)$/, "")
679
+ .replace(/\.(tsx|jsx)$/, "")
680
+ .replace(/\/$/, "");
681
+ } else if (relativePath.includes("src/routes/")) {
682
+ routePath = "/" + relativePath
683
+ .replace("src/routes/", "")
684
+ .replace(/\/index\.(tsx|jsx)$/, "")
685
+ .replace(/\.(tsx|jsx)$/, "")
686
+ .replace(/\/$/, "");
687
+ }
688
+
689
+ // Handle root route
690
+ if (routePath === "/index") routePath = "/";
691
+
692
+ // Generate component name from route
693
+ const componentName = routePath === "/" ? "Home" :
694
+ routePath.slice(1).split('/').map(part =>
695
+ part.charAt(0).toUpperCase() + part.slice(1)
696
+ ).join('');
697
+
698
+ // Build the component
699
+ const outputFile = `${componentName}.js`;
700
+ const outputPath = path.join(routesDir, outputFile);
701
+
702
+ await build({
703
+ entrypoints: [routeFile],
704
+ outdir: routesDir,
705
+ naming: { entry: componentName + ".js" },
706
+ target: "browser",
707
+ minify: !isDev,
708
+ sourcemap: isDev ? "inline" : "external",
709
+ jsxFactory: "Vader.createElement",
710
+ env: 'inline',
711
+ jsxFragment: "Fragment",
712
+ plugins: [publicAssetPlugin()],
713
+ external: ["vaderjs-native"],
714
+ });
715
+
716
+ routeMap[routePath] = `./routes/${outputFile}`;
717
+ }
718
+
719
+ return routeMap;
720
+ }
721
+
722
+
723
+
724
+ async function buildAppEntry(isDev: boolean): Promise<void> {
725
+ const appFile = findAppFile();
726
+
727
+ if (!appFile) {
728
+ logger.error("No App.tsx or App.jsx found in project root!");
729
+ throw new Error("Missing App.tsx/App.jsx in project root");
730
+ }
731
+
732
+ // Build the App component
733
+ await build({
734
+ entrypoints: [appFile],
735
+ outdir: DIST_DIR,
736
+ target: "browser",
737
+ minify: !isDev,
738
+ sourcemap: isDev ? "inline" : "external",
739
+ jsxFactory: "Vader.createElement",
740
+ jsxFragment: "Fragment",
741
+ env: "inline",
742
+ naming: "App.js",
743
+ plugins: [publicAssetPlugin()],
744
+ external: ["./routes.manifest.js"],
745
+ });
746
+ }
747
+
748
+ async function buildMainRuntime(isDev: boolean): Promise<void> {
749
+
750
+ }
751
+
752
+ async function buildSPA(isDev: boolean): Promise<void> {
753
+ logger.step("Building SPA");
754
+
755
+ // 1. Generate single HTML file
756
+ await buildSPAHtml();
757
+
758
+ // 2. Build route components separately
759
+ const routeMap = await buildRouteComponents(isDev);
760
+
761
+
762
+ // 4. Build App component
763
+ await buildAppEntry(isDev);
764
+
765
+ // 5. Build main runtime that ties everything together
766
+ await buildMainRuntime(isDev);
767
+ }
768
+
769
+ async function buildMPA(isDev: boolean): Promise<void> {
770
+ logger.step("Building MPA (Single page with all routes bundled)");
771
+
772
+ const appFile = findAppFile();
773
+
774
+ if (!appFile) {
775
+ logger.warn("No App.tsx or App.jsx found for MPA mode.");
776
+ return;
777
+ }
778
+
779
+ // For MPA, just bundle everything together
780
+ await buildSPAHtml();
781
+
782
+ // Build single bundle with all routes
783
+ await build({
784
+ entrypoints: [appFile],
785
+ outdir: DIST_DIR,
786
+ target: "browser",
787
+ minify: !isDev,
788
+ sourcemap: isDev ? "inline" : "external",
789
+ jsxFactory: "Vader.createElement",
790
+ jsxFragment: "Fragment",
791
+ naming: "index.js",
792
+ plugins: [publicAssetPlugin()],
793
+ external: [],
794
+ });
795
+ }
796
+
797
+ async function buildAppEntrypoints(isDev = false): Promise<void> {
798
+ const appFile = findAppFile();
799
+
800
+ if (!appFile) {
801
+ logger.warn("No App.tsx or App.jsx found in project root.");
802
+ return;
803
+ }
804
+
805
+ if (config.build_type === "spa") {
806
+ await buildSPA(isDev);
807
+ } else {
808
+ await buildMPA(isDev);
809
+ }
810
+ }
811
+
812
+ // --- MAIN BUILD FUNCTION ---
813
+ export async function buildAll(isDev = false): Promise<void> {
814
+ config = await loadConfig();
815
+ logger.info(`Starting VaderJS ${isDev ? 'development' : 'production'} build...`);
816
+ const totalTime = performance.now();
817
+
818
+ await runPluginHook("onBuildStart");
819
+
820
+ // Clean dist directory only if not in dev mode or if it doesn't exist
821
+ if (!isDev || !fsSync.existsSync(DIST_DIR)) {
822
+ await fs.rm(DIST_DIR, { recursive: true, force: true });
823
+ await fs.mkdir(DIST_DIR, { recursive: true });
824
+ } else if (isDev) {
825
+ // In dev mode, only clean if explicitly requested
826
+ const needsClean = process.env.VADER_CLEAN === 'true';
827
+ if (needsClean) {
828
+ await fs.rm(DIST_DIR, { recursive: true, force: true });
829
+ await fs.mkdir(DIST_DIR, { recursive: true });
830
+ }
831
+ }
832
+
833
+ // Run build steps in optimal order with parallelization where possible
834
+ const buildSteps = [
835
+ { name: "Building VaderJS Core", fn: buildVaderCore },
836
+ { name: "Building App Source (/src)", fn: buildSrc },
837
+ { name: "Copying Public Assets", fn: copyPublicAssets },
838
+ { name: "Building App Entrypoints", fn: () => buildAppEntrypoints(isDev) },
839
+ ];
840
+
841
+ for (const step of buildSteps) {
842
+ await timedStep(step.name, step.fn);
843
+ }
844
+
845
+ await runPluginHook("onBuildFinish");
846
+
847
+ // Cache cleanup for old entries
848
+ if (buildCache.size > 1000) {
849
+ const keys = Array.from(buildCache.keys()).slice(0, 500);
850
+ for (const key of keys) {
851
+ buildCache.delete(key);
852
+ }
853
+ }
854
+
855
+ const duration = (performance.now() - totalTime).toFixed(2);
856
+ logger.success(`Build completed in ${duration}ms. Output in ${DIST_DIR}`);
857
+ }
858
+
859
+ // --- SCRIPT ENTRYPOINT ---
860
+ async function main(): Promise<void> {
861
+ // Banner
862
+ console.log(`${colors.magenta}
863
+ __ __ ____ ____ _______ __
864
+ | | / |/ __ \\ / __ \\ / ____/ |/ /
865
+ | | / / / / // /_/ // /___ | /
866
+ | | / / /_/ / \\____// /___ / |
867
+ |____/____/_____/ /_____/ |_| |_|
868
+ ${colors.reset}`);
869
+
870
+ const command = process.argv[2];
871
+ const arg = process.argv[3];
872
+
873
+ // Set global flags
874
+ globalThis.isDev = command?.includes('dev') || false;
875
+ globalThis.isBuildingForWindows = command?.includes('windows') || false;
876
+
877
+ // Commands that don't require config
878
+ if (command === "init") {
879
+ await initProject(arg);
880
+ return;
881
+ }
882
+
883
+ // Load config for runtime commands
884
+ config = await loadConfig();
885
+ config.port = config.port || 3000;
886
+
887
+ // Command router
888
+ const commandHandlers: Record<string, () => Promise<void>> = {
889
+ 'add': async () => {
890
+ if (!arg) {
891
+ logger.error("Please specify a plugin to add.");
892
+ process.exit(1);
893
+ }
894
+ await addPlugin(arg);
895
+ },
896
+ 'list_plugins': async () => {
897
+ await listPlugins();
898
+ },
899
+ 'remove': async () => {
900
+ if (!arg) {
901
+ logger.error("Please specify a plugin to remove.");
902
+ process.exit(1);
903
+ }
904
+ await removePlugin(arg);
905
+ },
906
+ 'dev': async () => {
907
+ globalThis.isDev = true;
908
+ await runDevServer("web");
909
+ },
910
+ 'android:dev': async () => {
911
+ await buildAll(true);
912
+ await androidDev();
913
+ },
914
+ 'android:build': async () => {
915
+ await buildAll(false);
916
+ await buildAndroid(false);
917
+ logger.success("Android build completed 🚀");
918
+ },
919
+ 'windows:dev': async () => {
920
+ await buildAll(true);
921
+ await buildWindows(true);
922
+ await runDevServer("web");
923
+ await openWinApp();
924
+ },
925
+ 'windows:build': async () => {
926
+ await buildAll(false);
927
+ await buildWindows(false);
928
+ logger.success("Windows build completed 🚀");
929
+ },
930
+ 'build': async () => {
931
+ await buildAll(false);
932
+ },
933
+ 'serve': async () => {
934
+ await buildAll(false);
935
+ await runProdServer();
936
+ }
937
+ };
938
+
939
+ if (command && command in commandHandlers) {
940
+ await commandHandlers[command]();
941
+ } else {
942
+ logger.error(`Unknown command: ${command ?? ""}`);
943
+ logger.info(`
944
+ Available commands:
945
+ dev Start dev server
946
+ build Build for production
947
+ serve Build + serve production
948
+ init [dir] Create a new Vader project
949
+ add <plugin> Add a Vader plugin
950
+ remove <plugin> Remove a Vader plugin
951
+ list_plugins List currently installed Vaderjs plugins
952
+ android:dev Start Android development
953
+ android:build Build Android app
954
+ windows:dev Start Windows development
955
+ windows:build Build Windows app
956
+ `.trim());
957
+ process.exit(1);
958
+ }
959
+ }
960
+
961
+ // Stub functions for plugin management
962
+ async function addPlugin(pluginName: string): Promise<void> {
963
+ logger.info(`Adding plugin: ${pluginName}`);
964
+ // TODO: Implement plugin addition
965
+ logger.warn("Plugin addition not yet implemented");
966
+ }
967
+
968
+ async function removePlugin(pluginName: string): Promise<void> {
969
+ logger.info(`Removing plugin: ${pluginName}`);
970
+ // TODO: Implement plugin removal
971
+ logger.warn("Plugin removal not yet implemented");
972
+ }
973
+
974
+ async function listPlugins(): Promise<void> {
975
+ logger.info("Currently installed plugins:");
976
+ if (config.plugins && config.plugins.length > 0) {
977
+ config.plugins.forEach((plugin, index) => {
978
+ logger.info(` ${index + 1}. ${plugin.name || 'Unnamed plugin'}`);
979
+ });
980
+ } else {
981
+ logger.info(" No plugins installed.");
982
+ }
983
+ }
984
+
985
+ // Error handling
986
+ process.on("unhandledRejection", (err) => {
987
+ logger.error("Unhandled Promise rejection:", err);
988
+ });
989
+
990
+ process.on("uncaughtException", (err) => {
991
+ logger.error("Uncaught Exception:", err);
992
+ process.exit(1);
993
+ });
994
+
995
+ // Start the application
996
+ if (require.main === module) {
997
+ main().catch(err => {
998
+ logger.error("An unexpected error occurred:", err);
999
+ process.exit(1);
1000
+ });
1001
+ }
1002
+
1027
1003
  export default { buildAll, loadConfig, defineConfig };