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.
- package/main.ts +454 -141
- 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
|
-
|
|
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
|
|
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
|
-
|
|
265
|
-
|
|
304
|
+
try {
|
|
305
|
+
const watcher = fsSync.watch(dir, { recursive: true }, (event, filename) => {
|
|
306
|
+
if (!filename) return;
|
|
266
307
|
|
|
267
|
-
|
|
308
|
+
const file = path.join(dir, filename);
|
|
268
309
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
434
|
-
|
|
435
|
-
const
|
|
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
|
-
|
|
441
|
-
|
|
503
|
+
for (const injection of injections) {
|
|
504
|
+
let key = injection;
|
|
442
505
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
640
|
+
logger.warn("No app directory found");
|
|
475
641
|
return;
|
|
476
642
|
}
|
|
477
643
|
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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>${
|
|
723
|
+
<title>${pageTitle}</title>
|
|
510
724
|
${htmlInjectionsString}
|
|
511
725
|
</head>
|
|
512
726
|
<body>
|
|
513
727
|
<div id="app"></div>
|
|
514
|
-
<script type="module" src="
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
:
|
|
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
|
-
|
|
616
|
-
|
|
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
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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
|
}
|