vaderjs 2.3.14 → 2.3.16
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/README.md +89 -0
- package/cli.ts +227 -227
- package/config/index.ts +3 -0
- package/jsconfig.json +7 -7
- package/main.ts +744 -0
- package/package.json +16 -16
- package/plugins/index.ts +72 -72
- package/README.MD +0 -99
- package/main.js +0 -750
package/main.ts
ADDED
|
@@ -0,0 +1,744 @@
|
|
|
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 "vaderjs/cli";
|
|
8
|
+
|
|
9
|
+
const colors = {
|
|
10
|
+
reset: "\x1b[0m",
|
|
11
|
+
red: "\x1b[31m",
|
|
12
|
+
green: "\x1b[32m",
|
|
13
|
+
yellow: "\x1b[33m",
|
|
14
|
+
blue: "\x1b[34m",
|
|
15
|
+
magenta: "\x1b[35m",
|
|
16
|
+
cyan: "\x1b[36m",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
globalThis.isDev = process.argv[2] === "dev";
|
|
20
|
+
|
|
21
|
+
const logger = {
|
|
22
|
+
_log: (color, ...args) => console.log(color, ...args, colors.reset),
|
|
23
|
+
info: (...args) => logger._log(colors.cyan, "ℹ", ...args),
|
|
24
|
+
success: (...args) => logger._log(colors.green, "✅", ...args),
|
|
25
|
+
warn: (...args) => logger._log(colors.yellow, "⚠️", ...args),
|
|
26
|
+
error: (...args) => logger._log(colors.red, "❌", ...args),
|
|
27
|
+
step: (...args) => logger._log(colors.magenta, "\n🚀", ...args),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
async function timedStep(name, fn) {
|
|
31
|
+
logger.step(`${name}...`);
|
|
32
|
+
const start = performance.now();
|
|
33
|
+
try {
|
|
34
|
+
await fn();
|
|
35
|
+
const duration = (performance.now() - start).toFixed(2);
|
|
36
|
+
logger.success(`Finished '${name}' in ${duration}ms`);
|
|
37
|
+
} catch (e) {
|
|
38
|
+
logger.error(`Error during '${name}':`, e);
|
|
39
|
+
if (!isDev) process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// --- CONSTANTS ---
|
|
44
|
+
|
|
45
|
+
const PROJECT_ROOT = process.cwd();
|
|
46
|
+
const PUBLIC_DIR = path.join(PROJECT_ROOT, "public");
|
|
47
|
+
const DIST_DIR = path.join(PROJECT_ROOT, "dist");
|
|
48
|
+
const SRC_DIR = path.join(PROJECT_ROOT, "src");
|
|
49
|
+
const APP_DIR = path.join(PROJECT_ROOT, "app");
|
|
50
|
+
|
|
51
|
+
const VADER_SRC_PATH = path.join(
|
|
52
|
+
PROJECT_ROOT,
|
|
53
|
+
"node_modules",
|
|
54
|
+
"vaderjs",
|
|
55
|
+
"index.ts"
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const TEMP_SRC_DIR = path.join(PROJECT_ROOT, ".vader_temp_src");
|
|
59
|
+
|
|
60
|
+
let config: any = {};
|
|
61
|
+
let htmlInjections: string[] = [];
|
|
62
|
+
|
|
63
|
+
// --- Plugin Support ---
|
|
64
|
+
|
|
65
|
+
interface Plugin {
|
|
66
|
+
name: string;
|
|
67
|
+
version: string;
|
|
68
|
+
description?: string;
|
|
69
|
+
onBuildStart?: (api: PluginAPI) => Promise<void> | void;
|
|
70
|
+
onBuildFinish?: (api: PluginAPI) => Promise<void> | void;
|
|
71
|
+
onFileChange?: (file: string, api: PluginAPI) => Promise<void> | void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface PluginAPI {
|
|
75
|
+
injectHTML(html: string): void;
|
|
76
|
+
addWatchPath(path: string): void;
|
|
77
|
+
config: any;
|
|
78
|
+
isDev: boolean;
|
|
79
|
+
distDir: string;
|
|
80
|
+
srcDir: string;
|
|
81
|
+
publicDir: string;
|
|
82
|
+
projectRoot: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let plugins: Plugin[] = [];
|
|
86
|
+
|
|
87
|
+
// Create plugin API helper
|
|
88
|
+
function createPluginAPI(): PluginAPI {
|
|
89
|
+
return {
|
|
90
|
+
injectHTML: (html: string) => {
|
|
91
|
+
htmlInjections.push(html);
|
|
92
|
+
},
|
|
93
|
+
addWatchPath: (watchPath: string) => {
|
|
94
|
+
if (fsSync.existsSync(watchPath)) {
|
|
95
|
+
watcher.watch(watchPath);
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
config: config,
|
|
99
|
+
isDev: globalThis.isDev,
|
|
100
|
+
distDir: DIST_DIR,
|
|
101
|
+
srcDir: SRC_DIR,
|
|
102
|
+
publicDir: PUBLIC_DIR,
|
|
103
|
+
projectRoot: PROJECT_ROOT
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Run plugin hooks
|
|
108
|
+
async function runPluginHook(hookName: 'onBuildStart' | 'onBuildFinish', api: PluginAPI) {
|
|
109
|
+
for (const plugin of plugins) {
|
|
110
|
+
if (plugin[hookName]) {
|
|
111
|
+
try {
|
|
112
|
+
logger.info(`Running plugin hook: ${plugin.name} - ${hookName}`);
|
|
113
|
+
await plugin[hookName]!(api);
|
|
114
|
+
} catch (e) {
|
|
115
|
+
logger.error(`Error in plugin "${plugin.name}" during ${hookName}:`, e);
|
|
116
|
+
if (!globalThis.isDev) process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Load plugins from config
|
|
123
|
+
async function loadPluginsFromConfig() {
|
|
124
|
+
console.log(config)
|
|
125
|
+
if (!config.plugins || !Array.isArray(config.plugins)) {
|
|
126
|
+
logger.info("No plugins defined in config");
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const loadedPlugins: Plugin[] = [];
|
|
131
|
+
|
|
132
|
+
for (const pluginConfig of config.plugins) {
|
|
133
|
+
try {
|
|
134
|
+
let plugin;
|
|
135
|
+
|
|
136
|
+
// If plugin is a string, import it
|
|
137
|
+
if (typeof pluginConfig === 'string') {
|
|
138
|
+
const pluginPath = path.isAbsolute(pluginConfig)
|
|
139
|
+
? pluginConfig
|
|
140
|
+
: path.join(PROJECT_ROOT, pluginConfig);
|
|
141
|
+
|
|
142
|
+
// Check if file exists
|
|
143
|
+
if (fsSync.existsSync(pluginPath)) {
|
|
144
|
+
const pluginModule = await import(pluginPath);
|
|
145
|
+
plugin = pluginModule.default || pluginModule;
|
|
146
|
+
} else {
|
|
147
|
+
// Try as npm package
|
|
148
|
+
plugin = await import(pluginConfig);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// If plugin is an object with resolve property
|
|
152
|
+
else if (typeof pluginConfig === 'object' && pluginConfig.resolve) {
|
|
153
|
+
const pluginModule = await import(pluginConfig.resolve);
|
|
154
|
+
plugin = pluginModule.default || pluginModule;
|
|
155
|
+
|
|
156
|
+
// Pass options to plugin if it's a factory function
|
|
157
|
+
if (typeof plugin === 'function' && pluginConfig.options) {
|
|
158
|
+
plugin = await plugin(pluginConfig.options);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// If plugin is already a plugin object
|
|
162
|
+
else if (typeof pluginConfig === 'object' && pluginConfig.name) {
|
|
163
|
+
plugin = pluginConfig;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (plugin && typeof plugin === 'object' && plugin.name) {
|
|
167
|
+
loadedPlugins.push(plugin);
|
|
168
|
+
logger.success(`Loaded plugin: ${plugin.name} v${plugin.version || 'unknown'}`);
|
|
169
|
+
} else {
|
|
170
|
+
logger.warn(`Invalid plugin: missing name property`);
|
|
171
|
+
}
|
|
172
|
+
} catch (e) {
|
|
173
|
+
logger.error(`Failed to load plugin:`, e);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
plugins = loadedPlugins;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Also load from plugins directory (backward compatibility)
|
|
181
|
+
async function loadPluginsFromDirectory() {
|
|
182
|
+
const pluginsDir = path.join(PROJECT_ROOT, "plugins");
|
|
183
|
+
|
|
184
|
+
if (!fsSync.existsSync(pluginsDir)) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const pluginFiles = await fs.readdir(pluginsDir);
|
|
189
|
+
const loadedPlugins: Plugin[] = [];
|
|
190
|
+
|
|
191
|
+
for (const file of pluginFiles) {
|
|
192
|
+
if (file.endsWith('.js') || file.endsWith('.ts')) {
|
|
193
|
+
try {
|
|
194
|
+
const pluginPath = path.join(pluginsDir, file);
|
|
195
|
+
const pluginModule = await import(pluginPath);
|
|
196
|
+
const plugin = pluginModule.default || pluginModule;
|
|
197
|
+
|
|
198
|
+
if (plugin && typeof plugin === 'object' && plugin.name) {
|
|
199
|
+
// Check if plugin already loaded from config
|
|
200
|
+
if (!plugins.some(p => p.name === plugin.name)) {
|
|
201
|
+
loadedPlugins.push(plugin);
|
|
202
|
+
logger.info(`Loaded plugin from directory: ${plugin.name} v${plugin.version}`);
|
|
203
|
+
}
|
|
204
|
+
} else {
|
|
205
|
+
logger.warn(`Invalid plugin in ${file}: missing name property`);
|
|
206
|
+
}
|
|
207
|
+
} catch (e) {
|
|
208
|
+
logger.error(`Failed to load plugin ${file}:`, e);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
plugins = [...plugins, ...loadedPlugins];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// --- JSConfig Setup ---
|
|
217
|
+
|
|
218
|
+
async function ensureJSConfig() {
|
|
219
|
+
const jsconfigPath = path.join(PROJECT_ROOT, "jsconfig.json");
|
|
220
|
+
|
|
221
|
+
// Check if jsconfig.json already exists
|
|
222
|
+
let existingConfig = {};
|
|
223
|
+
if (fsSync.existsSync(jsconfigPath)) {
|
|
224
|
+
try {
|
|
225
|
+
const content = await fs.readFile(jsconfigPath, "utf8");
|
|
226
|
+
existingConfig = JSON.parse(content);
|
|
227
|
+
} catch (e) {
|
|
228
|
+
logger.warn("Existing jsconfig.json is invalid, will overwrite");
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Define the required VaderJS configuration
|
|
233
|
+
const vaderConfig = {
|
|
234
|
+
compilerOptions: {
|
|
235
|
+
jsx: "react",
|
|
236
|
+
jsxFactory: "Vader.createElement",
|
|
237
|
+
jsxFragmentFactory: "Fragment"
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// Merge with existing config (preserve other settings)
|
|
242
|
+
const mergedConfig = {
|
|
243
|
+
...existingConfig,
|
|
244
|
+
compilerOptions: {
|
|
245
|
+
...(existingConfig.compilerOptions || {}),
|
|
246
|
+
...vaderConfig.compilerOptions
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// Write the config
|
|
251
|
+
await fs.writeFile(jsconfigPath, JSON.stringify(mergedConfig, null, 2));
|
|
252
|
+
logger.success(`jsconfig.json created/updated at ${jsconfigPath}`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// --- FILE WATCHER ---
|
|
256
|
+
|
|
257
|
+
class FileWatcher {
|
|
258
|
+
watchers = new Map<string, any>();
|
|
259
|
+
callbacks: ((file: string) => void)[] = [];
|
|
260
|
+
|
|
261
|
+
watch(dir: string) {
|
|
262
|
+
if (!fsSync.existsSync(dir)) return;
|
|
263
|
+
|
|
264
|
+
const watcher = fsSync.watch(dir, { recursive: true }, (_, filename) => {
|
|
265
|
+
if (!filename) return;
|
|
266
|
+
|
|
267
|
+
const file = path.join(dir, filename);
|
|
268
|
+
|
|
269
|
+
if (
|
|
270
|
+
file.includes("node_modules") ||
|
|
271
|
+
file.includes("dist") ||
|
|
272
|
+
file.includes(".git")
|
|
273
|
+
)
|
|
274
|
+
return;
|
|
275
|
+
|
|
276
|
+
this.callbacks.forEach((cb) => cb(file));
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
this.watchers.set(dir, watcher);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
onChange(cb: (file: string) => void) {
|
|
283
|
+
this.callbacks.push(cb);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
clear() {
|
|
287
|
+
this.watchers.forEach((w) => w.close());
|
|
288
|
+
this.watchers.clear();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const watcher = new FileWatcher();
|
|
293
|
+
|
|
294
|
+
// --- CONFIG ---
|
|
295
|
+
|
|
296
|
+
export async function loadConfig() {
|
|
297
|
+
try {
|
|
298
|
+
const configPath = path.join(PROJECT_ROOT, "vaderjs.config.ts");
|
|
299
|
+
if (fsSync.existsSync(configPath)) {
|
|
300
|
+
const mod = await import(configPath);
|
|
301
|
+
return mod.default || mod;
|
|
302
|
+
}
|
|
303
|
+
return {};
|
|
304
|
+
} catch (error) {
|
|
305
|
+
logger.warn("Failed to load config, using defaults:", error);
|
|
306
|
+
return {};
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function defineConfig(cfg: any) {
|
|
311
|
+
return cfg;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// --- BUILD HELPERS ---
|
|
315
|
+
|
|
316
|
+
function patchHooksUsage(code: string) {
|
|
317
|
+
return code.replace(
|
|
318
|
+
/import\s+{[^}]*use(State|Effect|Memo)[^}]*}\s+from\s+['"]vaderjs['"];?\n?/g,
|
|
319
|
+
""
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async function preprocessSources(src: string, dest: string) {
|
|
324
|
+
await fs.mkdir(dest, { recursive: true });
|
|
325
|
+
|
|
326
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
327
|
+
|
|
328
|
+
for (const entry of entries) {
|
|
329
|
+
const srcPath = path.join(src, entry.name);
|
|
330
|
+
const destPath = path.join(dest, entry.name);
|
|
331
|
+
|
|
332
|
+
if (entry.isDirectory()) {
|
|
333
|
+
await preprocessSources(srcPath, destPath);
|
|
334
|
+
} else if (/\.(ts|tsx|js|jsx)$/.test(entry.name)) {
|
|
335
|
+
let content = await fs.readFile(srcPath, "utf8");
|
|
336
|
+
content = patchHooksUsage(content);
|
|
337
|
+
await fs.writeFile(destPath, content);
|
|
338
|
+
} else {
|
|
339
|
+
await fs.copyFile(srcPath, destPath);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function buildVaderCore() {
|
|
345
|
+
if (!fsSync.existsSync(VADER_SRC_PATH)) {
|
|
346
|
+
throw new Error("Missing vaderjs dependency");
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
await build({
|
|
350
|
+
entrypoints: [VADER_SRC_PATH],
|
|
351
|
+
outdir: path.join(DIST_DIR, "src", "vader"),
|
|
352
|
+
target: "browser",
|
|
353
|
+
jsxFactory: "e",
|
|
354
|
+
jsxFragment: "Fragment",
|
|
355
|
+
jsxImportSource: "vaderjs",
|
|
356
|
+
sourcemap: "external",
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function buildSrc() {
|
|
361
|
+
if (!fsSync.existsSync(SRC_DIR)) return;
|
|
362
|
+
|
|
363
|
+
if (fsSync.existsSync(TEMP_SRC_DIR)) {
|
|
364
|
+
await fs.rm(TEMP_SRC_DIR, { recursive: true, force: true });
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
await preprocessSources(SRC_DIR, TEMP_SRC_DIR);
|
|
368
|
+
|
|
369
|
+
const entrypoints: string[] = [];
|
|
370
|
+
|
|
371
|
+
function collect(dir: string) {
|
|
372
|
+
const files = fsSync.readdirSync(dir, { withFileTypes: true });
|
|
373
|
+
|
|
374
|
+
for (const f of files) {
|
|
375
|
+
const full = path.join(dir, f.name);
|
|
376
|
+
|
|
377
|
+
if (f.isDirectory()) collect(full);
|
|
378
|
+
else if (/\.(ts|tsx|js|jsx)$/.test(f.name)) entrypoints.push(full);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
collect(TEMP_SRC_DIR);
|
|
383
|
+
|
|
384
|
+
if (!entrypoints.length) return;
|
|
385
|
+
|
|
386
|
+
await build({
|
|
387
|
+
entrypoints,
|
|
388
|
+
outdir: path.join(DIST_DIR, "src"),
|
|
389
|
+
root: TEMP_SRC_DIR,
|
|
390
|
+
naming: { entry: "[dir]/[name].js" },
|
|
391
|
+
jsxFactory: "e",
|
|
392
|
+
jsxFragment: "Fragment",
|
|
393
|
+
jsxImportSource: "vaderjs",
|
|
394
|
+
target: "browser",
|
|
395
|
+
external: ["vaderjs"],
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async function copyPublicAssets() {
|
|
400
|
+
if (!fsSync.existsSync(PUBLIC_DIR)) return;
|
|
401
|
+
|
|
402
|
+
const items = await fs.readdir(PUBLIC_DIR);
|
|
403
|
+
|
|
404
|
+
for (const item of items) {
|
|
405
|
+
await fs.cp(
|
|
406
|
+
path.join(PUBLIC_DIR, item),
|
|
407
|
+
path.join(DIST_DIR, item),
|
|
408
|
+
{ recursive: true }
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Helper function to find App file
|
|
414
|
+
function findAppFile(): string | null {
|
|
415
|
+
const possiblePaths = [
|
|
416
|
+
path.join(PROJECT_ROOT, "App.tsx"),
|
|
417
|
+
path.join(PROJECT_ROOT, "App.jsx"),
|
|
418
|
+
path.join(PROJECT_ROOT, "App.ts"),
|
|
419
|
+
path.join(PROJECT_ROOT, "App.js"),
|
|
420
|
+
path.join(APP_DIR, "index.tsx"),
|
|
421
|
+
path.join(APP_DIR, "index.jsx"),
|
|
422
|
+
];
|
|
423
|
+
|
|
424
|
+
for (const appPath of possiblePaths) {
|
|
425
|
+
if (fsSync.existsSync(appPath)) {
|
|
426
|
+
return appPath;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
async function buildAppEntrypoints() {
|
|
434
|
+
// First check for root App file (VaderJS standard)
|
|
435
|
+
const appFile = findAppFile();
|
|
436
|
+
|
|
437
|
+
// Build HTML with injections from plugins
|
|
438
|
+
const htmlInjectionsString = htmlInjections.join('\n ');
|
|
439
|
+
|
|
440
|
+
if (appFile) {
|
|
441
|
+
logger.info(`Building App from: ${appFile}`);
|
|
442
|
+
|
|
443
|
+
await build({
|
|
444
|
+
entrypoints: [appFile],
|
|
445
|
+
outdir: DIST_DIR,
|
|
446
|
+
target: "browser",
|
|
447
|
+
jsxFactory: "e",
|
|
448
|
+
jsxFragment: "Fragment",
|
|
449
|
+
jsxImportSource: "vaderjs",
|
|
450
|
+
naming: "index.js",
|
|
451
|
+
external: [], // Bundle everything for website
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
const html = `<!DOCTYPE html>
|
|
455
|
+
<html>
|
|
456
|
+
<head>
|
|
457
|
+
<meta charset="UTF-8"/>
|
|
458
|
+
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
|
459
|
+
<title>${config.title || 'Vader App'}</title>
|
|
460
|
+
${htmlInjectionsString}
|
|
461
|
+
</head>
|
|
462
|
+
<body>
|
|
463
|
+
<div id="app"></div>
|
|
464
|
+
<script type="module" src="/index.js"></script>
|
|
465
|
+
</body>
|
|
466
|
+
</html>`;
|
|
467
|
+
|
|
468
|
+
await fs.writeFile(path.join(DIST_DIR, "index.html"), html);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Fallback to app directory structure
|
|
473
|
+
if (!fsSync.existsSync(APP_DIR)) {
|
|
474
|
+
logger.warn("No App.tsx or app directory found");
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const entries = fsSync
|
|
479
|
+
.readdirSync(APP_DIR, { recursive: true })
|
|
480
|
+
.filter((f) => /index\.(tsx|jsx)$/.test(f as string))
|
|
481
|
+
.map((f) => ({
|
|
482
|
+
name:
|
|
483
|
+
path.dirname(f as string) === "."
|
|
484
|
+
? "index"
|
|
485
|
+
: path.dirname(f as string),
|
|
486
|
+
path: path.join(APP_DIR, f as string),
|
|
487
|
+
}));
|
|
488
|
+
|
|
489
|
+
for (const entry of entries) {
|
|
490
|
+
var outDir = path.join(DIST_DIR, entry.name === "index" ? "" : entry.name);
|
|
491
|
+
|
|
492
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
493
|
+
console.log("Building entrypoint:", entry.path);
|
|
494
|
+
await build({
|
|
495
|
+
entrypoints: [entry.path],
|
|
496
|
+
outdir: outDir,
|
|
497
|
+
target: "browser",
|
|
498
|
+
jsxFactory: "e",
|
|
499
|
+
jsxFragment: "Fragment",
|
|
500
|
+
jsxImportSource: "vaderjs",
|
|
501
|
+
external: [], // Bundle everything for website
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
const html = `<!DOCTYPE html>
|
|
505
|
+
<html>
|
|
506
|
+
<head>
|
|
507
|
+
<meta charset="UTF-8"/>
|
|
508
|
+
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
|
509
|
+
<title>${config.title || 'Vader App'}</title>
|
|
510
|
+
${htmlInjectionsString}
|
|
511
|
+
</head>
|
|
512
|
+
<body>
|
|
513
|
+
<div id="app"></div>
|
|
514
|
+
<script type="module" src="/index.js"></script>
|
|
515
|
+
</body>
|
|
516
|
+
</html>`;
|
|
517
|
+
|
|
518
|
+
await fs.writeFile(path.join(outDir, "index.html"), html);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
async function buildAll(dev = false) {
|
|
523
|
+
const start = performance.now();
|
|
524
|
+
|
|
525
|
+
// Reset HTML injections before build
|
|
526
|
+
htmlInjections = [];
|
|
527
|
+
|
|
528
|
+
// Load plugins from config first
|
|
529
|
+
await loadPluginsFromConfig();
|
|
530
|
+
|
|
531
|
+
// Also load from plugins directory (backward compatibility)
|
|
532
|
+
await loadPluginsFromDirectory();
|
|
533
|
+
|
|
534
|
+
// Create plugin API
|
|
535
|
+
const pluginAPI = createPluginAPI();
|
|
536
|
+
|
|
537
|
+
// Run onBuildStart hooks
|
|
538
|
+
await runPluginHook('onBuildStart', pluginAPI);
|
|
539
|
+
|
|
540
|
+
// Ensure jsconfig.json exists before building
|
|
541
|
+
await ensureJSConfig();
|
|
542
|
+
|
|
543
|
+
if (fsSync.existsSync(DIST_DIR)) {
|
|
544
|
+
await fs.rm(DIST_DIR, { recursive: true, force: true });
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
await fs.mkdir(DIST_DIR, { recursive: true });
|
|
548
|
+
|
|
549
|
+
await timedStep("Build Vader Core", buildVaderCore);
|
|
550
|
+
await timedStep("Build Src", buildSrc);
|
|
551
|
+
await timedStep("Copy Public", copyPublicAssets);
|
|
552
|
+
await timedStep("Build App", buildAppEntrypoints);
|
|
553
|
+
|
|
554
|
+
// Run onBuildFinish hooks
|
|
555
|
+
await runPluginHook('onBuildFinish', pluginAPI);
|
|
556
|
+
|
|
557
|
+
logger.success(
|
|
558
|
+
`Build finished in ${(performance.now() - start).toFixed(1)}ms`
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// --- DEV SERVER ---
|
|
563
|
+
|
|
564
|
+
async function runDevServer() {
|
|
565
|
+
await buildAll(true);
|
|
566
|
+
|
|
567
|
+
const clients = new Set<any>();
|
|
568
|
+
|
|
569
|
+
const port = config.port || 3000;
|
|
570
|
+
|
|
571
|
+
const server = serve({
|
|
572
|
+
port,
|
|
573
|
+
|
|
574
|
+
fetch(req, server) {
|
|
575
|
+
const url = new URL(req.url);
|
|
576
|
+
|
|
577
|
+
if (url.pathname === "/__hmr" && server.upgrade(req)) return;
|
|
578
|
+
|
|
579
|
+
let filePath = path.join(DIST_DIR, url.pathname);
|
|
580
|
+
|
|
581
|
+
if (!path.extname(filePath)) {
|
|
582
|
+
filePath = path.join(filePath, "index.html");
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Ensure we're serving from dist directory
|
|
586
|
+
if (url.pathname === "/" || url.pathname === "") {
|
|
587
|
+
filePath = path.join(DIST_DIR, "index.html");
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
console.log("Serving:", filePath);
|
|
591
|
+
|
|
592
|
+
const file = Bun.file(filePath);
|
|
593
|
+
|
|
594
|
+
return file.exists().then((exists) =>
|
|
595
|
+
exists
|
|
596
|
+
? new Response(file)
|
|
597
|
+
: new Response("Not Found", { status: 404 })
|
|
598
|
+
);
|
|
599
|
+
},
|
|
600
|
+
|
|
601
|
+
websocket: {
|
|
602
|
+
open(ws) {
|
|
603
|
+
clients.add(ws);
|
|
604
|
+
},
|
|
605
|
+
close(ws) {
|
|
606
|
+
clients.delete(ws);
|
|
607
|
+
},
|
|
608
|
+
},
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
watcher.watch(APP_DIR);
|
|
612
|
+
watcher.watch(SRC_DIR);
|
|
613
|
+
watcher.watch(PUBLIC_DIR);
|
|
614
|
+
|
|
615
|
+
// Watch config file
|
|
616
|
+
watcher.watch(path.join(PROJECT_ROOT, "vaderjs.config.js"));
|
|
617
|
+
|
|
618
|
+
// Watch plugins directory
|
|
619
|
+
const pluginsDir = path.join(PROJECT_ROOT, "plugins");
|
|
620
|
+
if (fsSync.existsSync(pluginsDir)) {
|
|
621
|
+
watcher.watch(pluginsDir);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Also watch for root App file
|
|
625
|
+
const rootAppDir = path.dirname(findAppFile() || "");
|
|
626
|
+
if (rootAppDir && rootAppDir !== PROJECT_ROOT) {
|
|
627
|
+
watcher.watch(rootAppDir);
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
watcher.onChange(async (file) => {
|
|
631
|
+
logger.info("Changes detected, rebuilding...");
|
|
632
|
+
|
|
633
|
+
// If config or plugin file changed, reload config and plugins
|
|
634
|
+
if (file.includes('vaderjs.config.js') || file.includes('plugins')) {
|
|
635
|
+
logger.info("Config or plugin changed, reloading...");
|
|
636
|
+
config = await loadConfig();
|
|
637
|
+
htmlInjections = [];
|
|
638
|
+
plugins = [];
|
|
639
|
+
await loadPluginsFromConfig();
|
|
640
|
+
await loadPluginsFromDirectory();
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Run onFileChange hooks for plugins
|
|
644
|
+
const pluginAPI = createPluginAPI();
|
|
645
|
+
for (const plugin of plugins) {
|
|
646
|
+
if (plugin.onFileChange) {
|
|
647
|
+
await plugin.onFileChange(file, pluginAPI);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
await buildAll(true);
|
|
652
|
+
|
|
653
|
+
for (const c of clients) c.send("reload");
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
logger.success(`Dev server running http://localhost:${port}`);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// --- PROD SERVER ---
|
|
660
|
+
|
|
661
|
+
async function runProdServer() {
|
|
662
|
+
const port = config.port || 3000;
|
|
663
|
+
|
|
664
|
+
const server = serve({
|
|
665
|
+
port,
|
|
666
|
+
fetch(req) {
|
|
667
|
+
const url = new URL(req.url);
|
|
668
|
+
let filePath = path.join(DIST_DIR, url.pathname);
|
|
669
|
+
|
|
670
|
+
if (!path.extname(filePath)) {
|
|
671
|
+
filePath = path.join(filePath, "index.html");
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
if (url.pathname === "/" || url.pathname === "") {
|
|
675
|
+
filePath = path.join(DIST_DIR, "index.html");
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const file = Bun.file(filePath);
|
|
679
|
+
|
|
680
|
+
return file.exists().then((exists) =>
|
|
681
|
+
exists
|
|
682
|
+
? new Response(file)
|
|
683
|
+
: new Response("Not Found", { status: 404 })
|
|
684
|
+
);
|
|
685
|
+
},
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
logger.success(`Serving production http://localhost:${port}`);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// --- CLI ENTRY ---
|
|
692
|
+
|
|
693
|
+
async function main() {
|
|
694
|
+
console.log(`${colors.magenta}
|
|
695
|
+
__ __ ____ ____ _______ __
|
|
696
|
+
| | / |/ __ \\ / __ \\ / ____/ |/ /
|
|
697
|
+
| | / / / / // /_/ // /___ | /
|
|
698
|
+
| | / / /_/ / \\____// /___ / |
|
|
699
|
+
|____/____/_____/ /_____/ |_| |_|
|
|
700
|
+
${colors.reset}`);
|
|
701
|
+
|
|
702
|
+
const cmd = process.argv[2];
|
|
703
|
+
|
|
704
|
+
if (cmd === "init") {
|
|
705
|
+
await initProject(process.argv[3]);
|
|
706
|
+
// Also create jsconfig.json when initializing a new project
|
|
707
|
+
await ensureJSConfig();
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
config = await loadConfig();
|
|
712
|
+
config.port ||= 3000;
|
|
713
|
+
|
|
714
|
+
switch (cmd) {
|
|
715
|
+
case "dev":
|
|
716
|
+
await runDevServer();
|
|
717
|
+
break;
|
|
718
|
+
|
|
719
|
+
case "build":
|
|
720
|
+
await buildAll(false);
|
|
721
|
+
break;
|
|
722
|
+
|
|
723
|
+
case "serve":
|
|
724
|
+
await buildAll(false);
|
|
725
|
+
await runProdServer();
|
|
726
|
+
break;
|
|
727
|
+
|
|
728
|
+
default:
|
|
729
|
+
logger.info(`
|
|
730
|
+
Commands:
|
|
731
|
+
dev Start development server with hot reload
|
|
732
|
+
build Build for production
|
|
733
|
+
serve Build and serve production build
|
|
734
|
+
init Initialize a new VaderJS project
|
|
735
|
+
|
|
736
|
+
Make sure you have:
|
|
737
|
+
- App.tsx or App.jsx in your project root
|
|
738
|
+
- OR an app/ directory with index.tsx/jsx files
|
|
739
|
+
- vaderjs installed as a dependency
|
|
740
|
+
`);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
main();
|