vaderjs-native 1.0.36 → 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/LICENSE +21 -21
- package/bun.lock +48 -0
- package/cli/android/build.ts +378 -378
- package/cli/android/dev.ts +129 -129
- package/cli.ts +353 -279
- package/index.ts +494 -340
- package/jsconfig.json +6 -6
- package/main.ts +1002 -1026
- package/package.json +18 -18
- package/plugins/index.ts +63 -63
- /package/{README.MD → README.md} +0 -0
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
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
if (
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
const
|
|
549
|
-
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
<
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
//
|
|
693
|
-
const
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
//
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
async function
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
//
|
|
780
|
-
await buildSPAHtml();
|
|
781
|
-
|
|
782
|
-
//
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
if (
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
const
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
//
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
await
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
},
|
|
930
|
-
'
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
await
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
|
|
985
|
-
//
|
|
986
|
-
|
|
987
|
-
logger.
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
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 };
|