vaderjs 2.3.3 → 2.3.5
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/index.ts +158 -83
- package/main.js +124 -57
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -13,7 +13,9 @@ let deletions: Fiber[] | null = null;
|
|
|
13
13
|
let wipFiber: Fiber | null = null;
|
|
14
14
|
let hookIndex = 0;
|
|
15
15
|
let isRenderScheduled = false;
|
|
16
|
-
|
|
16
|
+
// Add to the top of your Vader.js file
|
|
17
|
+
|
|
18
|
+
|
|
17
19
|
interface Fiber {
|
|
18
20
|
type?: string | Function;
|
|
19
21
|
dom?: Node;
|
|
@@ -28,9 +30,13 @@ interface Fiber {
|
|
|
28
30
|
effectTag?: "PLACEMENT" | "UPDATE" | "DELETION";
|
|
29
31
|
hooks?: Hook[];
|
|
30
32
|
key?: string | number | null;
|
|
33
|
+
propsCache?: Record<string, any>;
|
|
34
|
+
__compareProps?: (prev: any, next: any) => boolean;
|
|
35
|
+
__skipMemo?: boolean;
|
|
36
|
+
_needsUpdate?: boolean;
|
|
31
37
|
}
|
|
32
38
|
|
|
33
|
-
interface VNode {
|
|
39
|
+
export interface VNode {
|
|
34
40
|
type: string | Function;
|
|
35
41
|
props: {
|
|
36
42
|
children: VNode[];
|
|
@@ -84,33 +90,55 @@ const isGone = (prev: object, next: object) => (key: string) => !(key in next);
|
|
|
84
90
|
* @returns {Node} The created DOM node.
|
|
85
91
|
*/
|
|
86
92
|
function createDom(fiber: Fiber): Node {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
93
|
+
let dom: Node;
|
|
94
|
+
|
|
95
|
+
if (fiber.type === "TEXT_ELEMENT") {
|
|
96
|
+
dom = document.createTextNode("");
|
|
97
|
+
} else {
|
|
98
|
+
const isSvg = isSvgElement(fiber);
|
|
99
|
+
if (isSvg) {
|
|
100
|
+
dom = document.createElementNS("http://www.w3.org/2000/svg", fiber.type as string);
|
|
101
|
+
} else {
|
|
102
|
+
dom = document.createElement(fiber.type as string);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
91
105
|
|
|
92
106
|
updateDom(dom, {}, fiber.props);
|
|
93
107
|
return dom;
|
|
94
108
|
}
|
|
95
109
|
|
|
110
|
+
function isSvgElement(fiber: Fiber): boolean {
|
|
111
|
+
// Check if the fiber is an <svg> itself or inside an <svg>
|
|
112
|
+
let parent = fiber.parent;
|
|
113
|
+
if (fiber.type === "svg") return true;
|
|
114
|
+
while (parent) {
|
|
115
|
+
if (parent.type === "svg") return true;
|
|
116
|
+
parent = parent.parent;
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
96
122
|
/**
|
|
97
123
|
* Applies updated props to a DOM node.
|
|
98
124
|
* @param {Node} dom - The DOM node to update.
|
|
99
125
|
* @param {object} prevProps - The previous properties.
|
|
100
126
|
* @param {object} nextProps - The new properties.
|
|
101
127
|
*/
|
|
102
|
-
|
|
128
|
+
function updateDom(dom: Node, prevProps: any, nextProps: any): void {
|
|
103
129
|
prevProps = prevProps || {};
|
|
104
130
|
nextProps = nextProps || {};
|
|
105
131
|
|
|
132
|
+
const isSvg = dom instanceof SVGElement;
|
|
133
|
+
|
|
106
134
|
// Remove old or changed event listeners
|
|
107
135
|
Object.keys(prevProps)
|
|
108
136
|
.filter(isEvent)
|
|
109
|
-
.filter(
|
|
110
|
-
.forEach(
|
|
137
|
+
.filter(key => !(key in nextProps) || isNew(prevProps, nextProps)(key))
|
|
138
|
+
.forEach(name => {
|
|
111
139
|
const eventType = name.toLowerCase().substring(2);
|
|
112
140
|
if (typeof prevProps[name] === 'function') {
|
|
113
|
-
dom.removeEventListener(eventType, prevProps[name]);
|
|
141
|
+
(dom as Element).removeEventListener(eventType, prevProps[name]);
|
|
114
142
|
}
|
|
115
143
|
});
|
|
116
144
|
|
|
@@ -118,12 +146,17 @@ function createDom(fiber: Fiber): Node {
|
|
|
118
146
|
Object.keys(prevProps)
|
|
119
147
|
.filter(isProperty)
|
|
120
148
|
.filter(isGone(prevProps, nextProps))
|
|
121
|
-
.forEach(
|
|
122
|
-
// FIX: Handle both `class` and `className`
|
|
149
|
+
.forEach(name => {
|
|
123
150
|
if (name === 'className' || name === 'class') {
|
|
124
|
-
(dom as
|
|
151
|
+
(dom as Element).removeAttribute('class');
|
|
152
|
+
} else if (name === 'style') {
|
|
153
|
+
(dom as HTMLElement).style.cssText = '';
|
|
125
154
|
} else {
|
|
126
|
-
|
|
155
|
+
if (isSvg) {
|
|
156
|
+
(dom as Element).removeAttribute(name);
|
|
157
|
+
} else {
|
|
158
|
+
(dom as any)[name] = '';
|
|
159
|
+
}
|
|
127
160
|
}
|
|
128
161
|
});
|
|
129
162
|
|
|
@@ -131,14 +164,24 @@ function createDom(fiber: Fiber): Node {
|
|
|
131
164
|
Object.keys(nextProps)
|
|
132
165
|
.filter(isProperty)
|
|
133
166
|
.filter(isNew(prevProps, nextProps))
|
|
134
|
-
.forEach(
|
|
135
|
-
if (name === 'style'
|
|
136
|
-
|
|
167
|
+
.forEach(name => {
|
|
168
|
+
if (name === 'style') {
|
|
169
|
+
const style = nextProps[name];
|
|
170
|
+
if (typeof style === 'string') {
|
|
171
|
+
(dom as HTMLElement).style.cssText = style;
|
|
172
|
+
} else if (typeof style === 'object' && style !== null) {
|
|
173
|
+
for (const [key, value] of Object.entries(style)) {
|
|
174
|
+
(dom as HTMLElement).style[key] = value;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
137
177
|
} else if (name === 'className' || name === 'class') {
|
|
138
|
-
|
|
139
|
-
(dom as HTMLElement).className = nextProps[name];
|
|
178
|
+
(dom as Element).setAttribute('class', nextProps[name]);
|
|
140
179
|
} else {
|
|
141
|
-
|
|
180
|
+
if (isSvg) {
|
|
181
|
+
(dom as Element).setAttribute(name, nextProps[name]);
|
|
182
|
+
} else {
|
|
183
|
+
(dom as any)[name] = nextProps[name];
|
|
184
|
+
}
|
|
142
185
|
}
|
|
143
186
|
});
|
|
144
187
|
|
|
@@ -146,15 +189,32 @@ function createDom(fiber: Fiber): Node {
|
|
|
146
189
|
Object.keys(nextProps)
|
|
147
190
|
.filter(isEvent)
|
|
148
191
|
.filter(isNew(prevProps, nextProps))
|
|
149
|
-
.forEach(
|
|
192
|
+
.forEach(name => {
|
|
150
193
|
const eventType = name.toLowerCase().substring(2);
|
|
151
194
|
const handler = nextProps[name];
|
|
152
195
|
if (typeof handler === 'function') {
|
|
153
|
-
dom.addEventListener(eventType, handler);
|
|
196
|
+
(dom as Element).addEventListener(eventType, handler);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
Object.keys(nextProps)
|
|
201
|
+
.filter(isEvent)
|
|
202
|
+
.filter(isNew(prevProps, nextProps))
|
|
203
|
+
.forEach(name => {
|
|
204
|
+
const eventType = name.toLowerCase().substring(2);
|
|
205
|
+
const handler = nextProps[name];
|
|
206
|
+
if (typeof handler === 'function') {
|
|
207
|
+
// Remove old listener first if it exists
|
|
208
|
+
if (prevProps[name]) {
|
|
209
|
+
dom.removeEventListener(eventType, prevProps[name]);
|
|
210
|
+
}
|
|
211
|
+
// Add new listener with passive: true for better performance
|
|
212
|
+
dom.addEventListener(eventType, handler, { passive: true });
|
|
154
213
|
}
|
|
155
214
|
});
|
|
156
215
|
}
|
|
157
216
|
|
|
217
|
+
|
|
158
218
|
/**
|
|
159
219
|
* Commits the entire work-in-progress tree to the DOM.
|
|
160
220
|
*/
|
|
@@ -227,11 +287,7 @@ export function render(element: VNode, container: Node): void {
|
|
|
227
287
|
};
|
|
228
288
|
deletions = [];
|
|
229
289
|
nextUnitOfWork = wipRoot;
|
|
230
|
-
|
|
231
|
-
if (!isRenderScheduled) {
|
|
232
|
-
isRenderScheduled = true;
|
|
233
|
-
requestAnimationFrame(workLoop);
|
|
234
|
-
}
|
|
290
|
+
requestAnimationFrame(workLoop);
|
|
235
291
|
}
|
|
236
292
|
|
|
237
293
|
/**
|
|
@@ -247,7 +303,7 @@ function workLoop(): void {
|
|
|
247
303
|
deletions = [];
|
|
248
304
|
nextUnitOfWork = wipRoot;
|
|
249
305
|
}
|
|
250
|
-
|
|
306
|
+
|
|
251
307
|
while (nextUnitOfWork) {
|
|
252
308
|
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
|
|
253
309
|
}
|
|
@@ -257,6 +313,7 @@ function workLoop(): void {
|
|
|
257
313
|
}
|
|
258
314
|
}
|
|
259
315
|
|
|
316
|
+
|
|
260
317
|
/**
|
|
261
318
|
* Performs work on a single fiber unit.
|
|
262
319
|
* @param {Fiber} fiber - The fiber to perform work on.
|
|
@@ -264,7 +321,7 @@ function workLoop(): void {
|
|
|
264
321
|
*/
|
|
265
322
|
function performUnitOfWork(fiber: Fiber): Fiber | null {
|
|
266
323
|
const isFunctionComponent = fiber.type instanceof Function;
|
|
267
|
-
|
|
324
|
+
|
|
268
325
|
if (isFunctionComponent) {
|
|
269
326
|
updateFunctionComponent(fiber);
|
|
270
327
|
} else {
|
|
@@ -289,19 +346,20 @@ function performUnitOfWork(fiber: Fiber): Fiber | null {
|
|
|
289
346
|
* Updates a function component fiber.
|
|
290
347
|
* @param {Fiber} fiber - The function component fiber to update.
|
|
291
348
|
*/
|
|
292
|
-
function updateFunctionComponent(fiber: Fiber)
|
|
349
|
+
function updateFunctionComponent(fiber: Fiber) {
|
|
293
350
|
wipFiber = fiber;
|
|
294
351
|
hookIndex = 0;
|
|
295
|
-
|
|
352
|
+
fiber.hooks = fiber.alternate?.hooks || [];
|
|
296
353
|
|
|
297
|
-
|
|
354
|
+
// Directly call the component function without memoization
|
|
355
|
+
// The 'createComponent' call is removed.
|
|
356
|
+
const children = [(fiber.type as Function)(fiber.props)]
|
|
298
357
|
.flat()
|
|
299
358
|
.filter(child => child != null && typeof child !== 'boolean')
|
|
300
|
-
.map(child => typeof child ===
|
|
359
|
+
.map(child => typeof child === 'object' ? child : createTextElement(child));
|
|
301
360
|
|
|
302
361
|
reconcileChildren(fiber, children);
|
|
303
362
|
}
|
|
304
|
-
|
|
305
363
|
/**
|
|
306
364
|
* Updates a host component fiber (DOM element).
|
|
307
365
|
* @param {Fiber} fiber - The host component fiber to update.
|
|
@@ -318,32 +376,31 @@ function updateHostComponent(fiber: Fiber): void {
|
|
|
318
376
|
* @param {Fiber} wipFiber - The work-in-progress fiber.
|
|
319
377
|
* @param {VNode[]} elements - The new child elements.
|
|
320
378
|
*/
|
|
321
|
-
function reconcileChildren(wipFiber: Fiber, elements: VNode[])
|
|
379
|
+
function reconcileChildren(wipFiber: Fiber, elements: VNode[]) {
|
|
322
380
|
let index = 0;
|
|
323
381
|
let oldFiber = wipFiber.alternate?.child;
|
|
324
|
-
let prevSibling = null;
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
382
|
+
let prevSibling: Fiber | null = null;
|
|
383
|
+
|
|
384
|
+
// Create map of existing fibers by key
|
|
385
|
+
const existingFibers = new Map<string | number | null, Fiber>();
|
|
328
386
|
while (oldFiber) {
|
|
329
|
-
const key = oldFiber.key
|
|
330
|
-
|
|
387
|
+
const key = oldFiber.key ?? index;
|
|
388
|
+
existingFibers.set(key, oldFiber);
|
|
331
389
|
oldFiber = oldFiber.sibling;
|
|
332
390
|
index++;
|
|
333
391
|
}
|
|
334
392
|
|
|
335
393
|
index = 0;
|
|
336
|
-
prevSibling = null;
|
|
337
|
-
|
|
338
394
|
for (; index < elements.length; index++) {
|
|
339
395
|
const element = elements[index];
|
|
340
|
-
const key = element
|
|
341
|
-
const oldFiber =
|
|
342
|
-
|
|
343
|
-
|
|
396
|
+
const key = element?.key ?? index;
|
|
397
|
+
const oldFiber = existingFibers.get(key);
|
|
398
|
+
|
|
399
|
+
const sameType = oldFiber && element && element.type === oldFiber.type;
|
|
344
400
|
let newFiber: Fiber | null = null;
|
|
345
401
|
|
|
346
402
|
if (sameType) {
|
|
403
|
+
// Reuse the fiber
|
|
347
404
|
newFiber = {
|
|
348
405
|
type: oldFiber.type,
|
|
349
406
|
props: element.props,
|
|
@@ -351,37 +408,44 @@ function reconcileChildren(wipFiber: Fiber, elements: VNode[]): void {
|
|
|
351
408
|
parent: wipFiber,
|
|
352
409
|
alternate: oldFiber,
|
|
353
410
|
effectTag: "UPDATE",
|
|
354
|
-
|
|
411
|
+
hooks: oldFiber.hooks,
|
|
412
|
+
key
|
|
355
413
|
};
|
|
356
|
-
|
|
357
|
-
} else {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
414
|
+
existingFibers.delete(key);
|
|
415
|
+
} else if (element) {
|
|
416
|
+
// Create new fiber
|
|
417
|
+
newFiber = {
|
|
418
|
+
type: element.type,
|
|
419
|
+
props: element.props,
|
|
420
|
+
dom: null,
|
|
421
|
+
parent: wipFiber,
|
|
422
|
+
alternate: null,
|
|
423
|
+
effectTag: "PLACEMENT",
|
|
424
|
+
key
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (oldFiber && !sameType) {
|
|
429
|
+
oldFiber.effectTag = "DELETION";
|
|
430
|
+
deletions.push(oldFiber);
|
|
369
431
|
}
|
|
370
432
|
|
|
371
|
-
if (
|
|
433
|
+
if (index === 0) {
|
|
372
434
|
wipFiber.child = newFiber;
|
|
373
|
-
} else {
|
|
435
|
+
} else if (prevSibling && newFiber) {
|
|
374
436
|
prevSibling.sibling = newFiber;
|
|
375
437
|
}
|
|
376
|
-
|
|
438
|
+
|
|
439
|
+
if (newFiber) {
|
|
440
|
+
prevSibling = newFiber;
|
|
441
|
+
}
|
|
377
442
|
}
|
|
378
443
|
|
|
379
|
-
|
|
444
|
+
// Mark remaining old fibers for deletion
|
|
445
|
+
existingFibers.forEach(fiber => {
|
|
380
446
|
fiber.effectTag = "DELETION";
|
|
381
447
|
deletions.push(fiber);
|
|
382
448
|
});
|
|
383
|
-
|
|
384
|
-
if (prevSibling) prevSibling.sibling = null;
|
|
385
449
|
}
|
|
386
450
|
|
|
387
451
|
/**
|
|
@@ -432,39 +496,45 @@ function createTextElement(text: string): VNode {
|
|
|
432
496
|
* @param {T|(() => T)} initial - The initial state value or initializer function.
|
|
433
497
|
* @returns {[T, (action: T | ((prevState: T) => T)) => void]} A stateful value and a function to update it.
|
|
434
498
|
*/
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
export function useState<T>(initial: T | (() => T)): [T, (action: T | ((prevState: T) => T)) => void] {
|
|
438
503
|
if (!wipFiber) {
|
|
439
504
|
throw new Error("Hooks can only be called inside a Vader.js function component.");
|
|
440
505
|
}
|
|
441
506
|
|
|
442
|
-
let hook = wipFiber.hooks[hookIndex];
|
|
507
|
+
let hook = wipFiber.hooks[hookIndex];
|
|
443
508
|
if (!hook) {
|
|
444
509
|
hook = {
|
|
445
|
-
state: typeof initial === "function" ? (initial as () => T)() : initial,
|
|
446
|
-
queue: []
|
|
510
|
+
state: typeof initial === "function" ? (initial as () => T)() : initial,
|
|
511
|
+
queue: [],
|
|
512
|
+
_needsUpdate: false
|
|
447
513
|
};
|
|
448
514
|
wipFiber.hooks[hookIndex] = hook;
|
|
449
515
|
}
|
|
450
516
|
|
|
451
|
-
hook.queue.forEach((action) => {
|
|
452
|
-
hook.state = typeof action === "function" ? action(hook.state) : action;
|
|
453
|
-
});
|
|
454
|
-
hook.queue = [];
|
|
455
|
-
|
|
456
517
|
const setState = (action: T | ((prevState: T) => T)) => {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
518
|
+
// Calculate new state based on current state
|
|
519
|
+
const newState = typeof action === "function"
|
|
520
|
+
? (action as (prevState: T) => T)(hook.state)
|
|
521
|
+
: action;
|
|
522
|
+
|
|
523
|
+
hook.state = newState;
|
|
524
|
+
|
|
525
|
+
// Reset work-in-progress root to trigger re-r
|
|
526
|
+
|
|
527
|
+
deletions = [];
|
|
528
|
+
nextUnitOfWork = wipRoot;
|
|
529
|
+
|
|
530
|
+
// Start the render process
|
|
460
531
|
requestAnimationFrame(workLoop);
|
|
461
|
-
}
|
|
462
532
|
};
|
|
463
533
|
|
|
464
534
|
hookIndex++;
|
|
465
535
|
return [hook.state, setState];
|
|
466
536
|
}
|
|
467
|
-
|
|
537
|
+
|
|
468
538
|
/**
|
|
469
539
|
* A React-like useEffect hook for side effects.
|
|
470
540
|
* @param {Function} callback - The effect callback.
|
|
@@ -529,6 +599,10 @@ export function Match({ when, children }: { when: boolean, children: VNode[] }):
|
|
|
529
599
|
return when ? children : null;
|
|
530
600
|
}
|
|
531
601
|
|
|
602
|
+
export function Show({ when, children }: { when: boolean, children: VNode[] }): VNode | null {
|
|
603
|
+
return when ? children : null;
|
|
604
|
+
}
|
|
605
|
+
|
|
532
606
|
/**
|
|
533
607
|
* A React-like useRef hook for mutable references.
|
|
534
608
|
* @template T
|
|
@@ -923,3 +997,4 @@ export function useOnClickOutside(ref: { current: HTMLElement | null }, handler:
|
|
|
923
997
|
};
|
|
924
998
|
}, [ref, handler]);
|
|
925
999
|
}
|
|
1000
|
+
|
package/main.js
CHANGED
|
@@ -1,15 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
|
|
3
|
-
* VaderJS Build & Development Script
|
|
4
|
-
*
|
|
5
|
-
* This script handles building the VaderJS framework, your application code,
|
|
6
|
-
* and serving it in a local development environment with live reloading.
|
|
7
|
-
*
|
|
8
|
-
* Commands:
|
|
9
|
-
* bun run vaderjs build - Builds the project for production.
|
|
10
|
-
* bun run vaderjs dev - Starts the dev server with HMR and file watching.
|
|
11
|
-
* bun run vaderjs serve - Builds and serves the production output.
|
|
12
|
-
*/
|
|
2
|
+
|
|
13
3
|
|
|
14
4
|
import { build, serve } from "bun";
|
|
15
5
|
import fs from "fs/promises";
|
|
@@ -29,6 +19,17 @@ const colors = {
|
|
|
29
19
|
cyan: "\x1b[36m",
|
|
30
20
|
};
|
|
31
21
|
|
|
22
|
+
function safeWatch(dir, cb) {
|
|
23
|
+
try {
|
|
24
|
+
const watcher = fsSync.watch(dir, { recursive: true }, cb);
|
|
25
|
+
watcher.on("error", (err) => logger.warn(`Watcher error on ${dir}:`, err));
|
|
26
|
+
return watcher;
|
|
27
|
+
} catch (err) {
|
|
28
|
+
logger.warn(`Failed to watch ${dir}:`, err);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
32
33
|
const logger = {
|
|
33
34
|
_log: (color, ...args) => console.log(color, ...args, colors.reset),
|
|
34
35
|
info: (...args) => logger._log(colors.cyan, "ℹ", ...args),
|
|
@@ -46,8 +47,8 @@ async function timedStep(name, fn) {
|
|
|
46
47
|
const duration = (performance.now() - start).toFixed(2);
|
|
47
48
|
logger.success(`Finished '${name}' in ${duration}ms`);
|
|
48
49
|
} catch (e) {
|
|
49
|
-
logger.error(`Error during '${name}':`, e
|
|
50
|
-
process.exit(1);
|
|
50
|
+
logger.error(`Error during '${name}':`, e);
|
|
51
|
+
if (!isDev) process.exit(1);
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
|
|
@@ -82,9 +83,10 @@ const vaderAPI = {
|
|
|
82
83
|
|
|
83
84
|
async function loadConfig() {
|
|
84
85
|
try {
|
|
85
|
-
const configModule = await import(path.join(PROJECT_ROOT, "vaderjs.config.js"));
|
|
86
|
+
const configModule = await import(path.join(PROJECT_ROOT, "vaderjs.config.js"));
|
|
86
87
|
return configModule.default || configModule;
|
|
87
88
|
} catch {
|
|
89
|
+
console.log(path.join(PROJECT_ROOT, "vaderjs.config.js"))
|
|
88
90
|
logger.warn("No 'vader.config.js' found, using defaults.");
|
|
89
91
|
return {};
|
|
90
92
|
}
|
|
@@ -101,13 +103,14 @@ async function runPluginHook(hookName) {
|
|
|
101
103
|
try {
|
|
102
104
|
await plugin[hookName](vaderAPI);
|
|
103
105
|
} catch (e) {
|
|
104
|
-
logger.error(`Plugin hook error (${hookName} in ${plugin.name || 'anonymous
|
|
106
|
+
logger.error(`Plugin hook error (${hookName} in ${plugin.name || 'anonymous'}):`, e);
|
|
105
107
|
}
|
|
106
108
|
}
|
|
107
109
|
}
|
|
108
110
|
}
|
|
109
111
|
|
|
110
112
|
|
|
113
|
+
|
|
111
114
|
// --- BUILD LOGIC ---
|
|
112
115
|
|
|
113
116
|
/**
|
|
@@ -137,10 +140,43 @@ async function buildVaderCore() {
|
|
|
137
140
|
function patchHooksUsage(code) {
|
|
138
141
|
return code.replace(/import\s+{[^}]*use(State|Effect|Memo|Navigation)[^}]*}\s+from\s+['"]vaderjs['"];?\n?/g, "");
|
|
139
142
|
}
|
|
143
|
+
function publicAssetPlugin() {
|
|
144
|
+
return {
|
|
145
|
+
name: "public-asset-replacer",
|
|
146
|
+
setup(build) {
|
|
147
|
+
build.onLoad({ filter: /\.(js|ts|jsx|tsx|html)$/ }, async (args) => {
|
|
148
|
+
let code = await fs.readFile(args.path, "utf8");
|
|
149
|
+
|
|
150
|
+
code = code.replace(/\{\{public:(.+?)\}\}/g, (_, relPath) => {
|
|
151
|
+
const absPath = path.join(PUBLIC_DIR, relPath.trim());
|
|
152
|
+
if (fsSync.existsSync(absPath)) {
|
|
153
|
+
return "/" + relPath.trim().replace(/\\/g, "/");
|
|
154
|
+
}
|
|
155
|
+
logger.warn(`Public asset not found: ${relPath}`);
|
|
156
|
+
return relPath;
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
contents: code,
|
|
161
|
+
loader: args.path.endsWith(".html")
|
|
162
|
+
? "text"
|
|
163
|
+
: args.path.endsWith(".tsx")
|
|
164
|
+
? "tsx"
|
|
165
|
+
: args.path.endsWith(".jsx")
|
|
166
|
+
? "jsx"
|
|
167
|
+
: args.path.endsWith(".ts")
|
|
168
|
+
? "ts"
|
|
169
|
+
: "js",
|
|
170
|
+
};
|
|
171
|
+
});
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
140
175
|
|
|
141
176
|
/**
|
|
142
177
|
* Step 3: Pre-processes all files in `/src` into a temporary directory.
|
|
143
178
|
*/
|
|
179
|
+
|
|
144
180
|
async function preprocessSources(srcDir, tempDir) {
|
|
145
181
|
await fs.mkdir(tempDir, { recursive: true });
|
|
146
182
|
for (const entry of await fs.readdir(srcDir, { withFileTypes: true })) {
|
|
@@ -151,7 +187,7 @@ async function preprocessSources(srcDir, tempDir) {
|
|
|
151
187
|
await preprocessSources(srcPath, destPath);
|
|
152
188
|
} else if (/\.(tsx|jsx|ts|js)$/.test(entry.name)) {
|
|
153
189
|
let content = await fs.readFile(srcPath, "utf8");
|
|
154
|
-
content = patchHooksUsage(content);
|
|
190
|
+
content = patchHooksUsage(content);
|
|
155
191
|
await fs.writeFile(destPath, content);
|
|
156
192
|
} else {
|
|
157
193
|
await fs.copyFile(srcPath, destPath);
|
|
@@ -189,6 +225,9 @@ async function buildSrc() {
|
|
|
189
225
|
jsxImportSource: "vaderjs",
|
|
190
226
|
target: "browser",
|
|
191
227
|
minify: false,
|
|
228
|
+
plugins: [
|
|
229
|
+
publicAssetPlugin(),
|
|
230
|
+
],
|
|
192
231
|
external: ["vaderjs"],
|
|
193
232
|
});
|
|
194
233
|
}
|
|
@@ -210,57 +249,61 @@ async function copyPublicAssets() {
|
|
|
210
249
|
return;
|
|
211
250
|
}
|
|
212
251
|
|
|
213
|
-
// Ensure the dist directory exists
|
|
214
252
|
if (!fsSync.existsSync(DIST_DIR)) {
|
|
215
253
|
await fs.mkdir(DIST_DIR, { recursive: true });
|
|
216
254
|
}
|
|
217
255
|
|
|
218
|
-
const devClientScript = isDev
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
256
|
+
const devClientScript = isDev
|
|
257
|
+
? `<script>
|
|
258
|
+
new WebSocket("ws://" + location.host + "/__hmr").onmessage = (msg) => {
|
|
259
|
+
if (msg.data === "reload") location.reload();
|
|
260
|
+
};
|
|
261
|
+
</script>`
|
|
262
|
+
: "";
|
|
224
263
|
|
|
225
264
|
const entries = fsSync.readdirSync(APP_DIR, { recursive: true })
|
|
226
265
|
.filter(file => /index\.(jsx|tsx)$/.test(file))
|
|
227
266
|
.map(file => ({
|
|
228
|
-
|
|
229
|
-
|
|
267
|
+
name: path.dirname(file) === '.' ? 'index' : path.dirname(file).replace(/\\/g, '/'),
|
|
268
|
+
path: path.join(APP_DIR, file)
|
|
230
269
|
}));
|
|
231
270
|
|
|
271
|
+
// Helper to resolve any asset path from /public
|
|
272
|
+
function resolvePublicPath(p) {
|
|
273
|
+
const assetPath = p.replace(/^(\.\/|\/)/, ""); // strip leading ./ or /
|
|
274
|
+
const absPath = path.join(PUBLIC_DIR, assetPath);
|
|
275
|
+
if (fsSync.existsSync(absPath)) {
|
|
276
|
+
return "/" + assetPath.replace(/\\/g, "/");
|
|
277
|
+
}
|
|
278
|
+
return p; // leave unchanged if not in public
|
|
279
|
+
}
|
|
280
|
+
|
|
232
281
|
for (const { name, path: entryPath } of entries) {
|
|
233
|
-
// Check for the specific case where 'name' could be 'index.js' and prevent duplication
|
|
234
282
|
const outDir = path.join(DIST_DIR, name === 'index' ? '' : name);
|
|
235
|
-
const outJsPath = path.join(outDir, 'index.js');
|
|
236
|
-
|
|
237
|
-
// Ensure the output directory exists
|
|
283
|
+
const outJsPath = path.join(outDir, 'index.js');
|
|
238
284
|
await fs.mkdir(outDir, { recursive: true });
|
|
239
285
|
|
|
240
|
-
//
|
|
286
|
+
// --- CSS HANDLING ---
|
|
241
287
|
const cssLinks = [];
|
|
242
|
-
|
|
243
|
-
const cssImports = [...
|
|
244
|
-
|
|
288
|
+
let content = await fs.readFile(entryPath, "utf8");
|
|
289
|
+
const cssImports = [...content.matchAll(/import\s+['"](.*\.css)['"]/g)];
|
|
245
290
|
for (const match of cssImports) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
logger.warn(`CSS file not found: ${sourceCssPath}`);
|
|
259
|
-
}
|
|
291
|
+
const cssImportPath = match[1];
|
|
292
|
+
const sourceCssPath = path.resolve(path.dirname(entryPath), cssImportPath);
|
|
293
|
+
if (fsSync.existsSync(sourceCssPath)) {
|
|
294
|
+
const relativeCssPath = path.relative(APP_DIR, sourceCssPath);
|
|
295
|
+
const destCssPath = path.join(DIST_DIR, relativeCssPath);
|
|
296
|
+
await fs.mkdir(path.dirname(destCssPath), { recursive: true });
|
|
297
|
+
await fs.copyFile(sourceCssPath, destCssPath);
|
|
298
|
+
const htmlRelativePath = path.relative(outDir, destCssPath).replace(/\\/g, '/');
|
|
299
|
+
cssLinks.push(`<link rel="stylesheet" href="${htmlRelativePath}">`);
|
|
300
|
+
} else {
|
|
301
|
+
logger.warn(`CSS file not found: ${sourceCssPath}`);
|
|
302
|
+
}
|
|
260
303
|
}
|
|
261
304
|
|
|
262
|
-
//
|
|
263
|
-
|
|
305
|
+
// --- HTML GENERATION ---
|
|
306
|
+
let htmlContent = `<!DOCTYPE html>
|
|
264
307
|
<html lang="en">
|
|
265
308
|
<head>
|
|
266
309
|
<meta charset="UTF-8" />
|
|
@@ -272,39 +315,56 @@ async function copyPublicAssets() {
|
|
|
272
315
|
<body>
|
|
273
316
|
<div id="app"></div>
|
|
274
317
|
<script type="module">
|
|
275
|
-
import App from '${name !== 'index' ? name : ''}/index.js';
|
|
318
|
+
import App from '${name !== 'index' ? "/" + name : ''}/index.js';
|
|
276
319
|
import * as Vader from '/src/vader/index.js';
|
|
277
|
-
window.Vader = Vader
|
|
320
|
+
window.Vader = Vader;
|
|
278
321
|
Vader.render(Vader.createElement(App, null), document.getElementById("app"));
|
|
279
322
|
</script>
|
|
280
323
|
${devClientScript}
|
|
281
324
|
</body>
|
|
282
325
|
</html>`;
|
|
283
326
|
|
|
284
|
-
|
|
327
|
+
// --- FIX ASSET PATHS IN HTML ---
|
|
328
|
+
htmlContent = htmlContent.replace(
|
|
329
|
+
/(["'(])([^"'()]+?\.(png|jpe?g|gif|svg|webp|ico))(["')])/gi,
|
|
330
|
+
(match, p1, assetPath, ext, p4) => p1 + resolvePublicPath(assetPath) + p4
|
|
331
|
+
);
|
|
285
332
|
|
|
286
|
-
|
|
333
|
+
await fs.writeFile(path.join(outDir, "index.html"), htmlContent);
|
|
287
334
|
|
|
288
|
-
//
|
|
335
|
+
// --- JS BUILD ---
|
|
289
336
|
await build({
|
|
290
337
|
entrypoints: [entryPath],
|
|
291
|
-
outdir: outDir,
|
|
338
|
+
outdir: outDir,
|
|
292
339
|
target: "browser",
|
|
293
340
|
minify: false,
|
|
294
341
|
sourcemap: "external",
|
|
295
342
|
external: ["vaderjs"],
|
|
296
343
|
jsxFactory: "e",
|
|
297
344
|
jsxFragment: "Fragment",
|
|
345
|
+
plugins: [
|
|
346
|
+
publicAssetPlugin(),
|
|
347
|
+
],
|
|
298
348
|
jsxImportSource: "vaderjs",
|
|
299
349
|
});
|
|
300
350
|
|
|
301
|
-
//
|
|
351
|
+
// --- FIX IMPORT PATHS IN JS ---
|
|
302
352
|
let jsContent = await fs.readFile(outJsPath, "utf8");
|
|
353
|
+
|
|
354
|
+
// Vader import fix
|
|
303
355
|
jsContent = jsContent.replace(/from\s+['"]vaderjs['"]/g, `from '/src/vader/index.js'`);
|
|
356
|
+
|
|
357
|
+
// Asset path fix for JS
|
|
358
|
+
jsContent = jsContent.replace(
|
|
359
|
+
/(["'(])([^"'()]+?\.(png|jpe?g|gif|svg|webp|ico))(["')])/gi,
|
|
360
|
+
(match, p1, assetPath, ext, p4) => p1 + resolvePublicPath(assetPath) + p4
|
|
361
|
+
);
|
|
362
|
+
|
|
304
363
|
await fs.writeFile(outJsPath, jsContent);
|
|
305
364
|
}
|
|
306
365
|
}
|
|
307
366
|
|
|
367
|
+
|
|
308
368
|
|
|
309
369
|
|
|
310
370
|
async function buildAll(isDev = false) {
|
|
@@ -379,7 +439,7 @@ async function runDevServer() {
|
|
|
379
439
|
|
|
380
440
|
const watchDirs = [APP_DIR, SRC_DIR, PUBLIC_DIR].filter(fsSync.existsSync);
|
|
381
441
|
for (const dir of watchDirs) {
|
|
382
|
-
|
|
442
|
+
safeWatch(dir, debouncedBuild);
|
|
383
443
|
}
|
|
384
444
|
}
|
|
385
445
|
|
|
@@ -430,6 +490,7 @@ console.log(banner);
|
|
|
430
490
|
const command = process.argv[2];
|
|
431
491
|
|
|
432
492
|
if (command === "dev") {
|
|
493
|
+
globalThis.isDev = true
|
|
433
494
|
await runDevServer();
|
|
434
495
|
} else if (command === "build") {
|
|
435
496
|
await buildAll(false);
|
|
@@ -454,3 +515,9 @@ main().catch(err => {
|
|
|
454
515
|
logger.error("An unexpected error occurred:", err);
|
|
455
516
|
process.exit(1);
|
|
456
517
|
});
|
|
518
|
+
process.on("unhandledRejection", (err) => {
|
|
519
|
+
logger.error("Unhandled Promise rejection:", err);
|
|
520
|
+
});
|
|
521
|
+
process.on("uncaughtException", (err) => {
|
|
522
|
+
logger.error("Uncaught Exception:", err);
|
|
523
|
+
});
|