create-aziz-app 2.0.12 → 2.0.14
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 +18 -24
- package/template/api/createUser.js +69 -0
- package/template/api/getUser.js +60 -0
- package/template/api/getUserWithDB.js +65 -0
- package/template/api/hello.js +19 -6
- package/template/api/loginUser.js +61 -0
- package/template/api/users.js +132 -37
- package/template/components/Navbar.jsx +7 -6
- package/template/docs/NEXTJS-APP-ROUTER.md +536 -0
- package/template/docs/NEXTJS-COMPARISON.md +473 -0
- package/template/lib/db.js +67 -0
- package/template/models/User.js +102 -0
- package/template/modules/next-response.js +58 -0
- package/template/modules/state.js +225 -31
- package/template/pages/HooksDemo.jsx +194 -0
- package/template/scripts/dev-server.js +108 -47
|
@@ -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
|
+
}
|
|
@@ -30,17 +30,34 @@ function loadApiRoutes() {
|
|
|
30
30
|
// Clear require cache to allow hot reload
|
|
31
31
|
delete require.cache[require.resolve(filePath)];
|
|
32
32
|
|
|
33
|
-
// Load the API
|
|
34
|
-
const
|
|
33
|
+
// Load the API module
|
|
34
|
+
const module = require(filePath);
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
// Support both styles:
|
|
37
|
+
// 1. Default export function (old style)
|
|
38
|
+
// 2. Named exports: GET, POST, PUT, DELETE (new Next.js App Router style)
|
|
39
|
+
|
|
40
|
+
if (module.GET || module.POST || module.PUT || module.DELETE || module.PATCH) {
|
|
41
|
+
// New style: Named exports
|
|
42
|
+
const methods = {};
|
|
43
|
+
if (module.GET) methods.GET = module.GET;
|
|
44
|
+
if (module.POST) methods.POST = module.POST;
|
|
45
|
+
if (module.PUT) methods.PUT = module.PUT;
|
|
46
|
+
if (module.DELETE) methods.DELETE = module.DELETE;
|
|
47
|
+
if (module.PATCH) methods.PATCH = module.PATCH;
|
|
48
|
+
|
|
49
|
+
apiHandlers[routeName] = methods;
|
|
50
|
+
console.log(` ✅ /api/${routeName} [${Object.keys(methods).join(', ')}]`);
|
|
42
51
|
} else {
|
|
43
|
-
|
|
52
|
+
// Old style: Default export
|
|
53
|
+
const handler = module.default || module;
|
|
54
|
+
|
|
55
|
+
if (typeof handler === 'function') {
|
|
56
|
+
apiHandlers[routeName] = handler;
|
|
57
|
+
console.log(` ✅ /api/${routeName}`);
|
|
58
|
+
} else {
|
|
59
|
+
console.log(` ⚠️ /api/${routeName} - No handler function exported`);
|
|
60
|
+
}
|
|
44
61
|
}
|
|
45
62
|
} catch (error) {
|
|
46
63
|
console.error(` ❌ /api/${routeName} - ${error.message}`);
|
|
@@ -49,6 +66,7 @@ function loadApiRoutes() {
|
|
|
49
66
|
|
|
50
67
|
console.log('');
|
|
51
68
|
}
|
|
69
|
+
}
|
|
52
70
|
|
|
53
71
|
// Parse request body
|
|
54
72
|
function parseBody(req) {
|
|
@@ -103,48 +121,91 @@ async function handleApiRequest(req, res) {
|
|
|
103
121
|
const body = await parseBody(req);
|
|
104
122
|
const query = parseQuery(req.url);
|
|
105
123
|
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
+
// Check if handler is a function (old style) or object with HTTP methods (new style)
|
|
125
|
+
let result;
|
|
126
|
+
|
|
127
|
+
if (typeof handler === 'function') {
|
|
128
|
+
// Old style: default export function
|
|
129
|
+
const apiReq = {
|
|
130
|
+
method: req.method,
|
|
131
|
+
body: body,
|
|
132
|
+
query: query,
|
|
133
|
+
headers: req.headers,
|
|
134
|
+
url: req.url
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const apiRes = {
|
|
138
|
+
statusCode: 200,
|
|
139
|
+
headers: {},
|
|
140
|
+
status(code) {
|
|
141
|
+
this.statusCode = code;
|
|
142
|
+
return this;
|
|
143
|
+
},
|
|
144
|
+
json(data) {
|
|
145
|
+
res.writeHead(this.statusCode, {
|
|
146
|
+
'Content-Type': 'application/json',
|
|
147
|
+
'Access-Control-Allow-Origin': '*',
|
|
148
|
+
...this.headers
|
|
149
|
+
});
|
|
150
|
+
res.end(JSON.stringify(data));
|
|
151
|
+
return this;
|
|
152
|
+
},
|
|
153
|
+
send(data) {
|
|
154
|
+
res.writeHead(this.statusCode, {
|
|
155
|
+
'Content-Type': 'text/plain',
|
|
156
|
+
'Access-Control-Allow-Origin': '*',
|
|
157
|
+
...this.headers
|
|
158
|
+
});
|
|
159
|
+
res.end(data);
|
|
160
|
+
return this;
|
|
161
|
+
},
|
|
162
|
+
setHeader(key, value) {
|
|
163
|
+
this.headers[key] = value;
|
|
164
|
+
return this;
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
await handler(apiReq, apiRes);
|
|
169
|
+
} else if (typeof handler === 'object') {
|
|
170
|
+
// New style: Named exports (GET, POST, PUT, DELETE)
|
|
171
|
+
const methodHandler = handler[req.method];
|
|
172
|
+
|
|
173
|
+
if (!methodHandler) {
|
|
174
|
+
res.writeHead(405, {
|
|
124
175
|
'Content-Type': 'application/json',
|
|
125
|
-
'
|
|
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
|
|
176
|
+
'Allow': Object.keys(handler).join(', ')
|
|
136
177
|
});
|
|
137
|
-
res.end(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
return this;
|
|
178
|
+
res.end(JSON.stringify({
|
|
179
|
+
error: 'Method not allowed',
|
|
180
|
+
allowedMethods: Object.keys(handler)
|
|
181
|
+
}));
|
|
182
|
+
return;
|
|
143
183
|
}
|
|
144
|
-
};
|
|
145
184
|
|
|
146
|
-
|
|
147
|
-
|
|
185
|
+
// Create Next.js-style request object
|
|
186
|
+
const request = {
|
|
187
|
+
method: req.method,
|
|
188
|
+
headers: req.headers,
|
|
189
|
+
url: `http://localhost:${PORT}${req.url}`,
|
|
190
|
+
json: async () => body,
|
|
191
|
+
text: async () => typeof body === 'string' ? body : JSON.stringify(body)
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// Call the handler
|
|
195
|
+
result = await methodHandler(request);
|
|
196
|
+
|
|
197
|
+
// Handle NextResponse
|
|
198
|
+
if (result && result.body !== undefined) {
|
|
199
|
+
const statusCode = result.status || 200;
|
|
200
|
+
const headers = {
|
|
201
|
+
'Access-Control-Allow-Origin': '*',
|
|
202
|
+
...Object.fromEntries(result.headers || [])
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
res.writeHead(statusCode, headers);
|
|
206
|
+
res.end(result.body);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
148
209
|
|
|
149
210
|
} catch (error) {
|
|
150
211
|
console.error(`❌ API Error on /api/${routeName}:`, error);
|