create-aziz-app 2.0.11 ā 2.0.13
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/package.json +3 -2
- package/template/README.md +26 -28
- package/template/api/hello.js +19 -7
- package/template/api/users.js +133 -40
- package/template/app.html +21 -25
- package/template/components/Navbar.jsx +7 -6
- package/template/docs/API-ROUTES.md +538 -0
- package/template/docs/NEXTJS-COMPARISON.md +473 -0
- package/template/modules/api.js +20 -124
- package/template/modules/state.js +225 -31
- package/template/pages/HooksDemo.jsx +194 -0
- package/template/scripts/dev-server.js +170 -1
- package/template/scripts/generate-imports.js +0 -10
|
@@ -1,54 +1,248 @@
|
|
|
1
1
|
/** @jsx h */
|
|
2
2
|
|
|
3
3
|
// ============================================================
|
|
4
|
-
// STATE MANAGEMENT
|
|
5
|
-
//
|
|
4
|
+
// STATE MANAGEMENT
|
|
5
|
+
// React-like hooks system (useState, useEffect, etc.)
|
|
6
|
+
// Exactly like React/Next.js!
|
|
6
7
|
// ============================================================
|
|
7
8
|
|
|
8
|
-
// Component state storage - keyed by component instance
|
|
9
|
-
const componentStates = new Map();
|
|
10
9
|
let currentComponent = null;
|
|
11
|
-
let
|
|
10
|
+
let currentHookIndex = 0;
|
|
11
|
+
const componentStates = new Map();
|
|
12
|
+
|
|
13
|
+
// Get or create state for current component
|
|
14
|
+
function getComponentState() {
|
|
15
|
+
if (!currentComponent) {
|
|
16
|
+
currentComponent = { hooks: [] };
|
|
17
|
+
}
|
|
18
|
+
return currentComponent;
|
|
19
|
+
}
|
|
12
20
|
|
|
13
|
-
//
|
|
21
|
+
// useState - Exactly like React!
|
|
14
22
|
function useState(initialValue) {
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
const component = getComponentState();
|
|
24
|
+
const hookIndex = currentHookIndex++;
|
|
25
|
+
|
|
26
|
+
// Initialize state if first render
|
|
27
|
+
if (component.hooks[hookIndex] === undefined) {
|
|
28
|
+
component.hooks[hookIndex] = {
|
|
29
|
+
value: typeof initialValue === 'function' ? initialValue() : initialValue
|
|
30
|
+
};
|
|
23
31
|
}
|
|
32
|
+
|
|
33
|
+
const hook = component.hooks[hookIndex];
|
|
34
|
+
|
|
35
|
+
const setState = (newValue) => {
|
|
36
|
+
const nextValue = typeof newValue === 'function'
|
|
37
|
+
? newValue(hook.value)
|
|
38
|
+
: newValue;
|
|
39
|
+
|
|
40
|
+
if (hook.value !== nextValue) {
|
|
41
|
+
hook.value = nextValue;
|
|
42
|
+
// Trigger re-render
|
|
43
|
+
if (typeof renderPage === 'function') {
|
|
44
|
+
renderPage();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return [hook.value, setState];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// useEffect - Exactly like React!
|
|
53
|
+
function useEffect(callback, dependencies) {
|
|
54
|
+
const component = getComponentState();
|
|
55
|
+
const hookIndex = currentHookIndex++;
|
|
56
|
+
|
|
57
|
+
const oldDeps = component.hooks[hookIndex]?.deps;
|
|
24
58
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
//
|
|
32
|
-
hookIndex
|
|
33
|
-
|
|
34
|
-
if (typeof renderPage === 'function') {
|
|
35
|
-
renderPage();
|
|
59
|
+
let hasChanged = true;
|
|
60
|
+
if (oldDeps) {
|
|
61
|
+
hasChanged = dependencies.some((dep, i) => dep !== oldDeps[i]);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (hasChanged) {
|
|
65
|
+
// Clean up previous effect
|
|
66
|
+
if (component.hooks[hookIndex]?.cleanup) {
|
|
67
|
+
component.hooks[hookIndex].cleanup();
|
|
36
68
|
}
|
|
69
|
+
|
|
70
|
+
// Run effect after render
|
|
71
|
+
setTimeout(() => {
|
|
72
|
+
const cleanup = callback();
|
|
73
|
+
if (component.hooks[hookIndex]) {
|
|
74
|
+
component.hooks[hookIndex].cleanup = cleanup;
|
|
75
|
+
}
|
|
76
|
+
}, 0);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
component.hooks[hookIndex] = {
|
|
80
|
+
deps: dependencies,
|
|
81
|
+
cleanup: component.hooks[hookIndex]?.cleanup
|
|
37
82
|
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// useMemo - Exactly like React!
|
|
86
|
+
function useMemo(factory, dependencies) {
|
|
87
|
+
const component = getComponentState();
|
|
88
|
+
const hookIndex = currentHookIndex++;
|
|
89
|
+
|
|
90
|
+
const oldDeps = component.hooks[hookIndex]?.deps;
|
|
38
91
|
|
|
39
|
-
|
|
92
|
+
let hasChanged = true;
|
|
93
|
+
if (oldDeps) {
|
|
94
|
+
hasChanged = dependencies.some((dep, i) => dep !== oldDeps[i]);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (hasChanged || !component.hooks[hookIndex]) {
|
|
98
|
+
const value = factory();
|
|
99
|
+
component.hooks[hookIndex] = {
|
|
100
|
+
value,
|
|
101
|
+
deps: dependencies
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return component.hooks[hookIndex].value;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// useCallback - Exactly like React!
|
|
109
|
+
function useCallback(callback, dependencies) {
|
|
110
|
+
return useMemo(() => callback, dependencies);
|
|
40
111
|
}
|
|
41
112
|
|
|
42
|
-
//
|
|
113
|
+
// useRef - Exactly like React!
|
|
114
|
+
function useRef(initialValue) {
|
|
115
|
+
const component = getComponentState();
|
|
116
|
+
const hookIndex = currentHookIndex++;
|
|
117
|
+
|
|
118
|
+
if (!component.hooks[hookIndex]) {
|
|
119
|
+
component.hooks[hookIndex] = {
|
|
120
|
+
current: initialValue
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return component.hooks[hookIndex];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// useReducer - Exactly like React!
|
|
128
|
+
function useReducer(reducer, initialState, init) {
|
|
129
|
+
const [state, setState] = useState(
|
|
130
|
+
init ? init(initialState) : initialState
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const dispatch = (action) => {
|
|
134
|
+
setState(prevState => reducer(prevState, action));
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
return [state, dispatch];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Custom hook: useLocalStorage
|
|
141
|
+
function useLocalStorage(key, initialValue) {
|
|
142
|
+
const [storedValue, setStoredValue] = useState(() => {
|
|
143
|
+
try {
|
|
144
|
+
const item = window.localStorage.getItem(key);
|
|
145
|
+
return item ? JSON.parse(item) : initialValue;
|
|
146
|
+
} catch (error) {
|
|
147
|
+
console.error(error);
|
|
148
|
+
return initialValue;
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const setValue = (value) => {
|
|
153
|
+
try {
|
|
154
|
+
const valueToStore = value instanceof Function ? value(storedValue) : value;
|
|
155
|
+
setStoredValue(valueToStore);
|
|
156
|
+
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.error(error);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
return [storedValue, setValue];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Custom hook: useFetch
|
|
166
|
+
function useFetch(url, options = {}) {
|
|
167
|
+
const [data, setData] = useState(null);
|
|
168
|
+
const [loading, setLoading] = useState(true);
|
|
169
|
+
const [error, setError] = useState(null);
|
|
170
|
+
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
let cancelled = false;
|
|
173
|
+
|
|
174
|
+
const fetchData = async () => {
|
|
175
|
+
try {
|
|
176
|
+
setLoading(true);
|
|
177
|
+
const response = await fetch(url, options);
|
|
178
|
+
|
|
179
|
+
if (!response.ok) {
|
|
180
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const json = await response.json();
|
|
184
|
+
|
|
185
|
+
if (!cancelled) {
|
|
186
|
+
setData(json);
|
|
187
|
+
setError(null);
|
|
188
|
+
}
|
|
189
|
+
} catch (e) {
|
|
190
|
+
if (!cancelled) {
|
|
191
|
+
setError(e.message);
|
|
192
|
+
setData(null);
|
|
193
|
+
}
|
|
194
|
+
} finally {
|
|
195
|
+
if (!cancelled) {
|
|
196
|
+
setLoading(false);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
fetchData();
|
|
202
|
+
|
|
203
|
+
return () => {
|
|
204
|
+
cancelled = true;
|
|
205
|
+
};
|
|
206
|
+
}, [url, JSON.stringify(options)]);
|
|
207
|
+
|
|
208
|
+
return { data, loading, error };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Reset hooks before each render
|
|
43
212
|
function resetHooks() {
|
|
44
|
-
|
|
213
|
+
currentHookIndex = 0;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Initialize component state
|
|
217
|
+
function initComponent() {
|
|
218
|
+
currentComponent = { hooks: [] };
|
|
219
|
+
currentHookIndex = 0;
|
|
45
220
|
}
|
|
46
221
|
|
|
47
|
-
// Export to global scope
|
|
222
|
+
// Export to global scope (like React)
|
|
48
223
|
window.useState = useState;
|
|
224
|
+
window.useEffect = useEffect;
|
|
225
|
+
window.useMemo = useMemo;
|
|
226
|
+
window.useCallback = useCallback;
|
|
227
|
+
window.useRef = useRef;
|
|
228
|
+
window.useReducer = useReducer;
|
|
229
|
+
window.useLocalStorage = useLocalStorage;
|
|
230
|
+
window.useFetch = useFetch;
|
|
49
231
|
window.resetHooks = resetHooks;
|
|
50
|
-
|
|
232
|
+
window.initComponent = initComponent;
|
|
233
|
+
|
|
234
|
+
// Also make available without window prefix
|
|
51
235
|
if (typeof globalThis !== 'undefined') {
|
|
52
236
|
globalThis.useState = useState;
|
|
237
|
+
globalThis.useEffect = useEffect;
|
|
238
|
+
globalThis.useMemo = useMemo;
|
|
239
|
+
globalThis.useCallback = useCallback;
|
|
240
|
+
globalThis.useRef = useRef;
|
|
241
|
+
globalThis.useReducer = useReducer;
|
|
242
|
+
globalThis.useLocalStorage = useLocalStorage;
|
|
243
|
+
globalThis.useFetch = useFetch;
|
|
53
244
|
globalThis.resetHooks = resetHooks;
|
|
54
|
-
|
|
245
|
+
globalThis.initComponent = initComponent;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
console.log('ā
React-like hooks loaded: useState, useEffect, useMemo, useCallback, useRef, useReducer');
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/** @jsx h */
|
|
2
|
+
|
|
3
|
+
// Demo page showing all React-like hooks
|
|
4
|
+
function HooksDemoPage() {
|
|
5
|
+
// useState - Basic state
|
|
6
|
+
const [count, setCount] = useState(0);
|
|
7
|
+
const [name, setName] = useState('');
|
|
8
|
+
|
|
9
|
+
// useEffect - Side effects
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
console.log('Component mounted!');
|
|
12
|
+
document.title = `Count: ${count}`;
|
|
13
|
+
|
|
14
|
+
return () => {
|
|
15
|
+
console.log('Cleanup!');
|
|
16
|
+
};
|
|
17
|
+
}, [count]);
|
|
18
|
+
|
|
19
|
+
// useMemo - Memoized value
|
|
20
|
+
const expensiveValue = useMemo(() => {
|
|
21
|
+
console.log('Computing expensive value...');
|
|
22
|
+
return count * 2;
|
|
23
|
+
}, [count]);
|
|
24
|
+
|
|
25
|
+
// useCallback - Memoized function
|
|
26
|
+
const handleIncrement = useCallback(() => {
|
|
27
|
+
setCount(c => c + 1);
|
|
28
|
+
}, []);
|
|
29
|
+
|
|
30
|
+
// useRef - Mutable ref
|
|
31
|
+
const inputRef = useRef(null);
|
|
32
|
+
|
|
33
|
+
const focusInput = () => {
|
|
34
|
+
if (inputRef.current) {
|
|
35
|
+
inputRef.current.focus();
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// useReducer - Complex state
|
|
40
|
+
const [state, dispatch] = useReducer((state, action) => {
|
|
41
|
+
switch (action.type) {
|
|
42
|
+
case 'increment':
|
|
43
|
+
return { ...state, value: state.value + 1 };
|
|
44
|
+
case 'decrement':
|
|
45
|
+
return { ...state, value: state.value - 1 };
|
|
46
|
+
case 'reset':
|
|
47
|
+
return { ...state, value: 0 };
|
|
48
|
+
default:
|
|
49
|
+
return state;
|
|
50
|
+
}
|
|
51
|
+
}, { value: 0 });
|
|
52
|
+
|
|
53
|
+
// useLocalStorage - Custom hook
|
|
54
|
+
const [savedName, setSavedName] = useLocalStorage('userName', '');
|
|
55
|
+
|
|
56
|
+
// useFetch - Custom hook
|
|
57
|
+
const { data, loading, error } = useFetch('/api/hello?name=Hooks');
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div>
|
|
61
|
+
<h1>React Hooks Demo šŖ</h1>
|
|
62
|
+
|
|
63
|
+
<div class="card">
|
|
64
|
+
<h2>useState</h2>
|
|
65
|
+
<p>Basic state management</p>
|
|
66
|
+
<div class="count-display">{count}</div>
|
|
67
|
+
<div class="counter-actions">
|
|
68
|
+
<button class="btn btn-primary" onClick={() => setCount(count + 1)}>
|
|
69
|
+
Increment
|
|
70
|
+
</button>
|
|
71
|
+
<button class="btn btn-danger" onClick={() => setCount(count - 1)}>
|
|
72
|
+
Decrement
|
|
73
|
+
</button>
|
|
74
|
+
<button class="btn btn-grey" onClick={() => setCount(0)}>
|
|
75
|
+
Reset
|
|
76
|
+
</button>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div class="card">
|
|
81
|
+
<h2>useEffect</h2>
|
|
82
|
+
<p>Check the browser console and page title!</p>
|
|
83
|
+
<p>Current count: <strong>{count}</strong></p>
|
|
84
|
+
<p style={{ color: '#666', fontSize: '14px' }}>
|
|
85
|
+
useEffect runs after render and updates the document title
|
|
86
|
+
</p>
|
|
87
|
+
</div>
|
|
88
|
+
|
|
89
|
+
<div class="card">
|
|
90
|
+
<h2>useMemo</h2>
|
|
91
|
+
<p>Memoized expensive computation</p>
|
|
92
|
+
<p>Count Ć 2 = <strong>{expensiveValue}</strong></p>
|
|
93
|
+
<p style={{ color: '#666', fontSize: '14px' }}>
|
|
94
|
+
Check console - computation only runs when count changes
|
|
95
|
+
</p>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div class="card">
|
|
99
|
+
<h2>useCallback</h2>
|
|
100
|
+
<p>Memoized callback function</p>
|
|
101
|
+
<button class="btn btn-primary" onClick={handleIncrement}>
|
|
102
|
+
Increment (Memoized)
|
|
103
|
+
</button>
|
|
104
|
+
<p style={{ color: '#666', fontSize: '14px', marginTop: '8px' }}>
|
|
105
|
+
Function reference stays the same between renders
|
|
106
|
+
</p>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<div class="card">
|
|
110
|
+
<h2>useRef</h2>
|
|
111
|
+
<p>Mutable reference to DOM element</p>
|
|
112
|
+
<input
|
|
113
|
+
ref={inputRef}
|
|
114
|
+
type="text"
|
|
115
|
+
placeholder="Click button to focus me"
|
|
116
|
+
value={name}
|
|
117
|
+
onInput={(e) => setName(e.target.value)}
|
|
118
|
+
/>
|
|
119
|
+
<button class="btn btn-primary" onClick={focusInput} style={{ marginTop: '8px' }}>
|
|
120
|
+
Focus Input
|
|
121
|
+
</button>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<div class="card">
|
|
125
|
+
<h2>useReducer</h2>
|
|
126
|
+
<p>Complex state management with actions</p>
|
|
127
|
+
<div class="count-display">{state.value}</div>
|
|
128
|
+
<div class="counter-actions">
|
|
129
|
+
<button class="btn btn-primary" onClick={() => dispatch({ type: 'increment' })}>
|
|
130
|
+
+
|
|
131
|
+
</button>
|
|
132
|
+
<button class="btn btn-danger" onClick={() => dispatch({ type: 'decrement' })}>
|
|
133
|
+
ā
|
|
134
|
+
</button>
|
|
135
|
+
<button class="btn btn-grey" onClick={() => dispatch({ type: 'reset' })}>
|
|
136
|
+
Reset
|
|
137
|
+
</button>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<div class="card">
|
|
142
|
+
<h2>useLocalStorage</h2>
|
|
143
|
+
<p>Persistent state in localStorage</p>
|
|
144
|
+
<input
|
|
145
|
+
type="text"
|
|
146
|
+
placeholder="Enter your name"
|
|
147
|
+
value={savedName}
|
|
148
|
+
onInput={(e) => setSavedName(e.target.value)}
|
|
149
|
+
/>
|
|
150
|
+
<p style={{ marginTop: '8px' }}>
|
|
151
|
+
Saved name: <strong>{savedName || 'Not set'}</strong>
|
|
152
|
+
</p>
|
|
153
|
+
<p style={{ color: '#666', fontSize: '14px' }}>
|
|
154
|
+
Refresh the page - your name persists!
|
|
155
|
+
</p>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<div class="card">
|
|
159
|
+
<h2>useFetch</h2>
|
|
160
|
+
<p>Data fetching with loading and error states</p>
|
|
161
|
+
{loading && <p>Loading...</p>}
|
|
162
|
+
{error && <p style={{ color: 'red' }}>Error: {error}</p>}
|
|
163
|
+
{data && (
|
|
164
|
+
<div style={{
|
|
165
|
+
background: '#f0f0f0',
|
|
166
|
+
padding: '12px',
|
|
167
|
+
borderRadius: '8px',
|
|
168
|
+
fontFamily: 'monospace',
|
|
169
|
+
fontSize: '14px'
|
|
170
|
+
}}>
|
|
171
|
+
<pre>{JSON.stringify(data, null, 2)}</pre>
|
|
172
|
+
</div>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
<div class="card">
|
|
177
|
+
<h2>All Hooks Available</h2>
|
|
178
|
+
<ul style={{ lineHeight: '1.8', marginLeft: '20px' }}>
|
|
179
|
+
<li><code>useState</code> - State management</li>
|
|
180
|
+
<li><code>useEffect</code> - Side effects</li>
|
|
181
|
+
<li><code>useMemo</code> - Memoized values</li>
|
|
182
|
+
<li><code>useCallback</code> - Memoized functions</li>
|
|
183
|
+
<li><code>useRef</code> - Mutable refs</li>
|
|
184
|
+
<li><code>useReducer</code> - Complex state</li>
|
|
185
|
+
<li><code>useLocalStorage</code> - Persistent state</li>
|
|
186
|
+
<li><code>useFetch</code> - Data fetching</li>
|
|
187
|
+
</ul>
|
|
188
|
+
<p style={{ marginTop: '12px', color: '#666' }}>
|
|
189
|
+
Just like React/Next.js! š
|
|
190
|
+
</p>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
@@ -7,9 +7,152 @@ const { exec } = require('child_process');
|
|
|
7
7
|
|
|
8
8
|
const PORT = process.env.PORT || 3000;
|
|
9
9
|
const clients = new Set();
|
|
10
|
+
const apiHandlers = {};
|
|
10
11
|
|
|
11
12
|
console.log('š Dev server starting...\n');
|
|
12
13
|
|
|
14
|
+
// Load API routes from api/ folder
|
|
15
|
+
function loadApiRoutes() {
|
|
16
|
+
const apiDir = path.join(process.cwd(), 'api');
|
|
17
|
+
if (!fs.existsSync(apiDir)) {
|
|
18
|
+
console.log('ā ļø No api/ folder found\n');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const files = fs.readdirSync(apiDir).filter(f => f.endsWith('.js'));
|
|
23
|
+
console.log(`š” Loading ${files.length} API routes...\n`);
|
|
24
|
+
|
|
25
|
+
files.forEach(file => {
|
|
26
|
+
const routeName = path.basename(file, '.js');
|
|
27
|
+
const filePath = path.join(apiDir, file);
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
// Clear require cache to allow hot reload
|
|
31
|
+
delete require.cache[require.resolve(filePath)];
|
|
32
|
+
|
|
33
|
+
// Load the API handler
|
|
34
|
+
const module = require(filePath);
|
|
35
|
+
|
|
36
|
+
// Support both default export and module.exports
|
|
37
|
+
const handler = module.default || module;
|
|
38
|
+
|
|
39
|
+
if (typeof handler === 'function') {
|
|
40
|
+
apiHandlers[routeName] = handler;
|
|
41
|
+
console.log(` ā
/api/${routeName}`);
|
|
42
|
+
} else {
|
|
43
|
+
console.log(` ā ļø /api/${routeName} - No handler function exported`);
|
|
44
|
+
}
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error(` ā /api/${routeName} - ${error.message}`);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
console.log('');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Parse request body
|
|
54
|
+
function parseBody(req) {
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
let body = '';
|
|
57
|
+
req.on('data', chunk => {
|
|
58
|
+
body += chunk.toString();
|
|
59
|
+
});
|
|
60
|
+
req.on('end', () => {
|
|
61
|
+
try {
|
|
62
|
+
if (body && req.headers['content-type']?.includes('application/json')) {
|
|
63
|
+
resolve(JSON.parse(body));
|
|
64
|
+
} else {
|
|
65
|
+
resolve(body);
|
|
66
|
+
}
|
|
67
|
+
} catch (e) {
|
|
68
|
+
resolve(body);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
req.on('error', reject);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Parse query string
|
|
76
|
+
function parseQuery(url) {
|
|
77
|
+
const query = {};
|
|
78
|
+
const queryString = url.split('?')[1];
|
|
79
|
+
if (queryString) {
|
|
80
|
+
queryString.split('&').forEach(param => {
|
|
81
|
+
const [key, value] = param.split('=');
|
|
82
|
+
query[decodeURIComponent(key)] = decodeURIComponent(value || '');
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return query;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Handle API requests
|
|
89
|
+
async function handleApiRequest(req, res) {
|
|
90
|
+
const urlPath = req.url.split('?')[0];
|
|
91
|
+
const routeName = urlPath.replace('/api/', '');
|
|
92
|
+
|
|
93
|
+
const handler = apiHandlers[routeName];
|
|
94
|
+
|
|
95
|
+
if (!handler) {
|
|
96
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
97
|
+
res.end(JSON.stringify({ error: 'API route not found' }));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
// Parse request
|
|
103
|
+
const body = await parseBody(req);
|
|
104
|
+
const query = parseQuery(req.url);
|
|
105
|
+
|
|
106
|
+
// Create Next.js-like req/res objects
|
|
107
|
+
const apiReq = {
|
|
108
|
+
method: req.method,
|
|
109
|
+
body: body,
|
|
110
|
+
query: query,
|
|
111
|
+
headers: req.headers,
|
|
112
|
+
url: req.url
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const apiRes = {
|
|
116
|
+
statusCode: 200,
|
|
117
|
+
headers: {},
|
|
118
|
+
status(code) {
|
|
119
|
+
this.statusCode = code;
|
|
120
|
+
return this;
|
|
121
|
+
},
|
|
122
|
+
json(data) {
|
|
123
|
+
res.writeHead(this.statusCode, {
|
|
124
|
+
'Content-Type': 'application/json',
|
|
125
|
+
'Access-Control-Allow-Origin': '*',
|
|
126
|
+
...this.headers
|
|
127
|
+
});
|
|
128
|
+
res.end(JSON.stringify(data));
|
|
129
|
+
return this;
|
|
130
|
+
},
|
|
131
|
+
send(data) {
|
|
132
|
+
res.writeHead(this.statusCode, {
|
|
133
|
+
'Content-Type': 'text/plain',
|
|
134
|
+
'Access-Control-Allow-Origin': '*',
|
|
135
|
+
...this.headers
|
|
136
|
+
});
|
|
137
|
+
res.end(data);
|
|
138
|
+
return this;
|
|
139
|
+
},
|
|
140
|
+
setHeader(key, value) {
|
|
141
|
+
this.headers[key] = value;
|
|
142
|
+
return this;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Call the handler
|
|
147
|
+
await handler(apiReq, apiRes);
|
|
148
|
+
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.error(`ā API Error on /api/${routeName}:`, error);
|
|
151
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
152
|
+
res.end(JSON.stringify({ error: error.message }));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
13
156
|
// Function to generate imports
|
|
14
157
|
function generateImports() {
|
|
15
158
|
console.log('š Regenerating imports...');
|
|
@@ -38,7 +181,24 @@ function notifyClients() {
|
|
|
38
181
|
}
|
|
39
182
|
|
|
40
183
|
// Create HTTP server
|
|
41
|
-
const server = http.createServer((req, res) => {
|
|
184
|
+
const server = http.createServer(async (req, res) => {
|
|
185
|
+
// Handle API routes
|
|
186
|
+
if (req.url.startsWith('/api/')) {
|
|
187
|
+
await handleApiRequest(req, res);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Handle CORS preflight
|
|
192
|
+
if (req.method === 'OPTIONS') {
|
|
193
|
+
res.writeHead(200, {
|
|
194
|
+
'Access-Control-Allow-Origin': '*',
|
|
195
|
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
|
196
|
+
'Access-Control-Allow-Headers': 'Content-Type'
|
|
197
|
+
});
|
|
198
|
+
res.end();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
42
202
|
// Strip query parameters for file serving (cache busting)
|
|
43
203
|
let filePath = '.' + req.url.split('?')[0];
|
|
44
204
|
if (filePath === './') {
|
|
@@ -170,6 +330,12 @@ watchDirs.forEach(dir => {
|
|
|
170
330
|
clearTimeout(reloadTimeout);
|
|
171
331
|
}
|
|
172
332
|
|
|
333
|
+
// Reload API routes if api/ folder changed
|
|
334
|
+
if (dir === 'api' && filename.endsWith('.js')) {
|
|
335
|
+
console.log('š Reloading API routes...');
|
|
336
|
+
loadApiRoutes();
|
|
337
|
+
}
|
|
338
|
+
|
|
173
339
|
// For CSS changes, just reload without regenerating imports
|
|
174
340
|
if (filename.endsWith('.css')) {
|
|
175
341
|
reloadTimeout = setTimeout(() => {
|
|
@@ -190,6 +356,9 @@ console.log('\nā
Dev server running with live reload!');
|
|
|
190
356
|
console.log('š” Create/modify files and browser will auto-refresh');
|
|
191
357
|
console.log('ā¹ļø Press Ctrl+C to stop\n');
|
|
192
358
|
|
|
359
|
+
// Load API routes
|
|
360
|
+
loadApiRoutes();
|
|
361
|
+
|
|
193
362
|
// Start server
|
|
194
363
|
startServer(PORT);
|
|
195
364
|
|
|
@@ -31,11 +31,9 @@ function scanDirectory(dir, extensions = []) {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
// Scan for files
|
|
34
|
-
const apiRoutes = scanDirectory('api', ['.js']);
|
|
35
34
|
const components = scanDirectory('components', ['.jsx', '.js']);
|
|
36
35
|
const pages = scanDirectory('pages', ['.jsx', '.js']);
|
|
37
36
|
|
|
38
|
-
console.log(`š” Found ${apiRoutes.length} API routes`);
|
|
39
37
|
console.log(`š§© Found ${components.length} components`);
|
|
40
38
|
console.log(`š Found ${pages.length} pages\n`);
|
|
41
39
|
|
|
@@ -48,14 +46,6 @@ scriptTags += ' <script type="text/babel" src="modules/router.js"></script>\n
|
|
|
48
46
|
scriptTags += ' <script type="text/babel" src="modules/state.js"></script>\n';
|
|
49
47
|
scriptTags += ' <script type="text/babel" src="modules/api.js"></script>\n\n';
|
|
50
48
|
|
|
51
|
-
if (apiRoutes.length > 0) {
|
|
52
|
-
scriptTags += ' <!-- Load API routes -->\n';
|
|
53
|
-
apiRoutes.forEach(route => {
|
|
54
|
-
scriptTags += ` <script type="text/babel" src="${route}"></script>\n`;
|
|
55
|
-
});
|
|
56
|
-
scriptTags += '\n';
|
|
57
|
-
}
|
|
58
|
-
|
|
59
49
|
if (components.length > 0) {
|
|
60
50
|
scriptTags += ' <!-- Load shared components -->\n';
|
|
61
51
|
components.forEach(component => {
|