vaderjs 2.3.2 → 2.3.4
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/cli.ts +157 -124
- package/index.ts +158 -83
- package/main.js +23 -5
- package/package.json +1 -1
package/cli.ts
CHANGED
|
@@ -2,77 +2,77 @@
|
|
|
2
2
|
|
|
3
3
|
import fs from "fs/promises";
|
|
4
4
|
import fsSync from "fs";
|
|
5
|
-
import path from "path";
|
|
5
|
+
import path from "path";
|
|
6
6
|
import readline from "readline";
|
|
7
7
|
|
|
8
8
|
function ask(question) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
9
|
+
const rl = readline.createInterface({
|
|
10
|
+
input: process.stdin,
|
|
11
|
+
output: process.stdout,
|
|
12
|
+
});
|
|
13
|
+
return new Promise((resolve) =>
|
|
14
|
+
rl.question(question + " ", (answer) => {
|
|
15
|
+
rl.close();
|
|
16
|
+
resolve(answer.trim());
|
|
17
|
+
})
|
|
18
|
+
);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
async function run(cmd: string, args: string[] = []) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
22
|
+
try {
|
|
23
|
+
const proc = Bun.spawn([cmd, ...args], {
|
|
24
|
+
stdout: "inherit",
|
|
25
|
+
stderr: "inherit",
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const status = await proc.exited;
|
|
29
|
+
if (status !== 0) {
|
|
30
|
+
console.error(`Command failed: ${cmd} ${args.join(" ")}`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error(`Error executing command: ${error}`);
|
|
35
|
+
process.exit(1);
|
|
32
36
|
}
|
|
33
|
-
} catch (error) {
|
|
34
|
-
console.error(`Error executing command: ${error}`);
|
|
35
|
-
process.exit(1);
|
|
36
|
-
}
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
export async function init() {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
// Confirm Tailwind usage
|
|
55
|
-
let useTailwind = await ask("Include TailwindCSS v4 support? (y/n):");
|
|
56
|
-
while (!["y", "n", "yes", "no"].includes(useTailwind)) {
|
|
57
|
-
useTailwind = await ask("Please answer 'y' or 'n':");
|
|
58
|
-
}
|
|
59
|
-
const wantsTailwind = useTailwind === "y" || useTailwind === "yes";
|
|
60
|
-
|
|
61
|
-
// Create folders: app, src, public
|
|
62
|
-
const appDir = path.join(projectDir, "app");
|
|
63
|
-
const srcDir = path.join(projectDir, "src");
|
|
64
|
-
const publicDir = path.join(projectDir, "public");
|
|
40
|
+
console.log("🚀 Welcome to Vader.js project initializer!");
|
|
41
|
+
|
|
42
|
+
const cwd = process.cwd();
|
|
43
|
+
let projectDir = await ask(
|
|
44
|
+
`Enter the directory to initialize the project (default: current dir):`
|
|
45
|
+
);
|
|
46
|
+
if (!projectDir) projectDir = ".";
|
|
47
|
+
|
|
48
|
+
projectDir = path.resolve(cwd, projectDir);
|
|
49
|
+
if (!fsSync.existsSync(projectDir)) {
|
|
50
|
+
await fs.mkdir(projectDir, { recursive: true });
|
|
51
|
+
console.log(`Created directory: ${projectDir}`);
|
|
52
|
+
}
|
|
65
53
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
54
|
+
// Confirm Tailwind usage
|
|
55
|
+
let useTailwind = await ask("Include TailwindCSS v4 support? (y/n):");
|
|
56
|
+
while (!["y", "n", "yes", "no"].includes(useTailwind)) {
|
|
57
|
+
useTailwind = await ask("Please answer 'y' or 'n':");
|
|
58
|
+
}
|
|
59
|
+
const wantsTailwind = useTailwind === "y" || useTailwind === "yes";
|
|
60
|
+
|
|
61
|
+
// Create folders: app, src, public
|
|
62
|
+
const appDir = path.join(projectDir, "app");
|
|
63
|
+
const srcDir = path.join(projectDir, "src");
|
|
64
|
+
const publicDir = path.join(projectDir, "public");
|
|
65
|
+
|
|
66
|
+
for (const dir of [appDir, srcDir, publicDir]) {
|
|
67
|
+
if (!fsSync.existsSync(dir)) {
|
|
68
|
+
await fs.mkdir(dir, { recursive: true });
|
|
69
|
+
console.log(`Created folder: ${dir}`);
|
|
70
|
+
}
|
|
70
71
|
}
|
|
71
|
-
}
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
// Create example app/index.jsx with counter
|
|
74
|
+
const counterCode = wantsTailwind
|
|
75
|
+
? `import { useState } from "vaderjs";
|
|
76
76
|
|
|
77
77
|
export default function Counter() {
|
|
78
78
|
let [count, setCount] = useState(0);
|
|
@@ -90,7 +90,7 @@ export default function Counter() {
|
|
|
90
90
|
);
|
|
91
91
|
}
|
|
92
92
|
`
|
|
93
|
-
|
|
93
|
+
: `import { useState } from "vaderjs";
|
|
94
94
|
|
|
95
95
|
export default function Counter() {
|
|
96
96
|
let [count, setCount] = useState(0);
|
|
@@ -104,69 +104,53 @@ export default function Counter() {
|
|
|
104
104
|
}
|
|
105
105
|
`;
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
await fs.writeFile(path.join(appDir, "index.jsx"), counterCode);
|
|
108
|
+
console.log(`Created example route: ${path.join("app", "index.jsx")}`);
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
// Create minimal package.json if not exist
|
|
119
|
-
const pkgJsonPath = path.join(projectDir, "package.json");
|
|
120
|
-
if (!fsSync.existsSync(pkgJsonPath)) {
|
|
121
|
-
const pkg = {
|
|
122
|
-
name: path.basename(projectDir),
|
|
123
|
-
version: "1.0.0",
|
|
124
|
-
scripts: {
|
|
125
|
-
start: "bun run vaderjs build && bun run vaderjs serve",
|
|
126
|
-
build: "bun run vaderjs build",
|
|
127
|
-
dev: "bun run vaderjs dev",
|
|
128
|
-
},
|
|
129
|
-
dependencies: {
|
|
130
|
-
vaderjs: "latest",
|
|
131
|
-
},
|
|
132
|
-
};
|
|
133
|
-
await fs.writeFile(pkgJsonPath, JSON.stringify(pkg, null, 2));
|
|
134
|
-
console.log(`Created package.json`);
|
|
135
|
-
}
|
|
110
|
+
// Create public/styles.css
|
|
111
|
+
if (wantsTailwind) {
|
|
112
|
+
await fs.writeFile(path.join(publicDir, "styles.css"), `@import 'tailwindcss';\n`);
|
|
113
|
+
} else {
|
|
114
|
+
await fs.writeFile(path.join(publicDir, "styles.css"), `/* Add your styles here */\n`);
|
|
115
|
+
}
|
|
116
|
+
console.log(`Created public/styles.css`);
|
|
136
117
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
118
|
+
// Create minimal package.json if not exist
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
// Install dependencies: vaderjs + optionally tailwindcss, postcss plugins, autoprefixer
|
|
122
|
+
console.log("Installing dependencies with Bun...");
|
|
123
|
+
const deps = ["vaderjs", "autoprefixer"];
|
|
124
|
+
if (wantsTailwind) {
|
|
125
|
+
deps.push("tailwindcss@4", "@tailwindcss/postcss", "postcss-cli");
|
|
126
|
+
}
|
|
127
|
+
await run("bun", ["install", ...deps]);
|
|
128
|
+
console.log("✅ Dependencies installed.");
|
|
145
129
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
130
|
+
// If Tailwind requested, create minimal tailwind.config.cjs and postcss.config.cjs
|
|
131
|
+
if (wantsTailwind) {
|
|
132
|
+
const tailwindConfig = `module.exports = {
|
|
149
133
|
content: ["./app/**/*.{js,jsx,ts,tsx}"],
|
|
150
134
|
theme: {
|
|
151
135
|
extend: {},
|
|
152
136
|
},
|
|
153
137
|
plugins: [],
|
|
154
138
|
};`;
|
|
155
|
-
|
|
156
|
-
|
|
139
|
+
await fs.writeFile(path.join(projectDir, "tailwind.config.cjs"), tailwindConfig);
|
|
140
|
+
console.log("Created tailwind.config.cjs");
|
|
157
141
|
|
|
158
|
-
|
|
142
|
+
const postcssConfig = `export default {
|
|
159
143
|
plugins: {
|
|
160
144
|
"@tailwindcss/postcss": {},
|
|
161
145
|
autoprefixer: {},
|
|
162
146
|
}
|
|
163
147
|
};`;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
148
|
+
await fs.writeFile(path.join(projectDir, "postcss.config.cjs"), postcssConfig);
|
|
149
|
+
console.log("Created postcss.config.cjs");
|
|
150
|
+
}
|
|
167
151
|
|
|
168
|
-
|
|
169
|
-
|
|
152
|
+
// Create vaderjs.config.ts regardless, add Tailwind plugin if needed
|
|
153
|
+
const vaderConfig = `import defineConfig from "vaderjs/config";
|
|
170
154
|
${wantsTailwind ? 'import tailwind from "vaderjs/plugins/tailwind";' : ''}
|
|
171
155
|
|
|
172
156
|
export default defineConfig({
|
|
@@ -174,22 +158,71 @@ export default defineConfig({
|
|
|
174
158
|
plugins: [${wantsTailwind ? "tailwind" : ""}],
|
|
175
159
|
});`;
|
|
176
160
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
161
|
+
await fs.writeFile(path.join(projectDir, "vaderjs.config.ts"), vaderConfig);
|
|
162
|
+
console.log("Created vaderjs.config.ts");
|
|
163
|
+
|
|
164
|
+
// Create jsconfig.json for VSCode/IDE support
|
|
165
|
+
const jsConfig = {
|
|
166
|
+
compilerOptions: {
|
|
167
|
+
jsx: "react",
|
|
168
|
+
jsxFactory: "Vader.createElement",
|
|
169
|
+
jsxFragmentFactory: "Fragment",
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
await fs.writeFile(path.join(projectDir, "jsconfig.json"), JSON.stringify(jsConfig, null, 2));
|
|
173
|
+
console.log("Created jsconfig.json");
|
|
174
|
+
|
|
175
|
+
// Final instructions
|
|
176
|
+
const pkgJsonPath = path.join(projectDir, "package.json");
|
|
177
|
+
|
|
178
|
+
if (!fsSync.existsSync(pkgJsonPath)) {
|
|
179
|
+
// If package.json doesn't exist, create it with basic content
|
|
180
|
+
const pkg = {
|
|
181
|
+
name: path.basename(projectDir),
|
|
182
|
+
version: "1.0.0",
|
|
183
|
+
scripts: {
|
|
184
|
+
start: "bun run vaderjs build && bun run vaderjs serve",
|
|
185
|
+
build: "bun run vaderjs build",
|
|
186
|
+
dev: "bun run vaderjs dev",
|
|
187
|
+
},
|
|
188
|
+
dependencies: {
|
|
189
|
+
vaderjs: "latest",
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// If Tailwind is requested, add it to the dependencies
|
|
194
|
+
if (wantsTailwind) {
|
|
195
|
+
pkg.dependencies.tailwindcss = "latest";
|
|
196
|
+
pkg.dependencies["@tailwindcss/postcss"] = "latest";
|
|
197
|
+
pkg.dependencies.postcss = "latest";
|
|
198
|
+
pkg.dependencies.autoprefixer = "latest";
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
await fs.writeFile(pkgJsonPath, JSON.stringify(pkg, null, 2));
|
|
202
|
+
console.log(`Created package.json`);
|
|
203
|
+
} else {
|
|
204
|
+
// If package.json exists, update it by adding Tailwind if it's not there
|
|
205
|
+
const pkgJson = JSON.parse(await fs.readFile(pkgJsonPath, "utf8"));
|
|
206
|
+
|
|
207
|
+
// Only update the dependencies and scripts if Tailwind is enabled
|
|
208
|
+
if (wantsTailwind && !pkgJson.dependencies.tailwindcss) {
|
|
209
|
+
pkgJson.dependencies.tailwindcss = "latest";
|
|
210
|
+
pkgJson.dependencies["@tailwindcss/postcss"] = "latest";
|
|
211
|
+
pkgJson.dependencies.postcss = "latest";
|
|
212
|
+
pkgJson.dependencies.autoprefixer = "latest";
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Ensure the scripts are in place (if they're not there already)
|
|
216
|
+
if (!pkgJson.scripts) pkgJson.scripts = {};
|
|
217
|
+
pkgJson.scripts.start = pkgJson.scripts.start || "bun run vaderjs build && bun run vaderjs serve";
|
|
218
|
+
pkgJson.scripts.build = pkgJson.scripts.build || "bun run vaderjs build";
|
|
219
|
+
pkgJson.scripts.dev = pkgJson.scripts.dev || "bun run vaderjs dev";
|
|
220
|
+
|
|
221
|
+
await fs.writeFile(pkgJsonPath, JSON.stringify(pkgJson, null, 2));
|
|
222
|
+
console.log(`Updated package.json`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
console.log(`\n🎉 Vader.js project initialized at:\n${projectDir}`);
|
|
226
|
+
console.log(`Run cd ${projectDir} to navigate into the project folder`);
|
|
227
|
+
console.log("Run `bun run dev` or your build script to get started.");
|
|
195
228
|
}
|
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
|
@@ -29,6 +29,17 @@ const colors = {
|
|
|
29
29
|
cyan: "\x1b[36m",
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
+
function safeWatch(dir, cb) {
|
|
33
|
+
try {
|
|
34
|
+
const watcher = fsSync.watch(dir, { recursive: true }, cb);
|
|
35
|
+
watcher.on("error", (err) => logger.warn(`Watcher error on ${dir}:`, err));
|
|
36
|
+
return watcher;
|
|
37
|
+
} catch (err) {
|
|
38
|
+
logger.warn(`Failed to watch ${dir}:`, err);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
32
43
|
const logger = {
|
|
33
44
|
_log: (color, ...args) => console.log(color, ...args, colors.reset),
|
|
34
45
|
info: (...args) => logger._log(colors.cyan, "ℹ", ...args),
|
|
@@ -46,8 +57,8 @@ async function timedStep(name, fn) {
|
|
|
46
57
|
const duration = (performance.now() - start).toFixed(2);
|
|
47
58
|
logger.success(`Finished '${name}' in ${duration}ms`);
|
|
48
59
|
} catch (e) {
|
|
49
|
-
logger.error(`Error during '${name}':`, e
|
|
50
|
-
process.exit(1);
|
|
60
|
+
logger.error(`Error during '${name}':`, e);
|
|
61
|
+
if (!isDev) process.exit(1);
|
|
51
62
|
}
|
|
52
63
|
}
|
|
53
64
|
|
|
@@ -101,13 +112,14 @@ async function runPluginHook(hookName) {
|
|
|
101
112
|
try {
|
|
102
113
|
await plugin[hookName](vaderAPI);
|
|
103
114
|
} catch (e) {
|
|
104
|
-
logger.error(`Plugin hook error (${hookName} in ${plugin.name || 'anonymous
|
|
115
|
+
logger.error(`Plugin hook error (${hookName} in ${plugin.name || 'anonymous'}):`, e);
|
|
105
116
|
}
|
|
106
117
|
}
|
|
107
118
|
}
|
|
108
119
|
}
|
|
109
120
|
|
|
110
121
|
|
|
122
|
+
|
|
111
123
|
// --- BUILD LOGIC ---
|
|
112
124
|
|
|
113
125
|
/**
|
|
@@ -272,7 +284,7 @@ async function copyPublicAssets() {
|
|
|
272
284
|
<body>
|
|
273
285
|
<div id="app"></div>
|
|
274
286
|
<script type="module">
|
|
275
|
-
import App from '${name !== 'index' ? name : ''}/index.js';
|
|
287
|
+
import App from '${name !== 'index' ? "/" + name : ''}/index.js';
|
|
276
288
|
import * as Vader from '/src/vader/index.js';
|
|
277
289
|
window.Vader = Vader
|
|
278
290
|
Vader.render(Vader.createElement(App, null), document.getElementById("app"));
|
|
@@ -379,7 +391,7 @@ async function runDevServer() {
|
|
|
379
391
|
|
|
380
392
|
const watchDirs = [APP_DIR, SRC_DIR, PUBLIC_DIR].filter(fsSync.existsSync);
|
|
381
393
|
for (const dir of watchDirs) {
|
|
382
|
-
|
|
394
|
+
safeWatch(dir, debouncedBuild);
|
|
383
395
|
}
|
|
384
396
|
}
|
|
385
397
|
|
|
@@ -454,3 +466,9 @@ main().catch(err => {
|
|
|
454
466
|
logger.error("An unexpected error occurred:", err);
|
|
455
467
|
process.exit(1);
|
|
456
468
|
});
|
|
469
|
+
process.on("unhandledRejection", (err) => {
|
|
470
|
+
logger.error("Unhandled Promise rejection:", err);
|
|
471
|
+
});
|
|
472
|
+
process.on("uncaughtException", (err) => {
|
|
473
|
+
logger.error("Uncaught Exception:", err);
|
|
474
|
+
});
|