create-aziz-app 2.0.14 → 3.0.0
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/README.md +227 -0
- package/bin/create-aziz-app.js +9 -9
- package/package.json +10 -11
- package/template/README.md +862 -138
- package/template/_framework/boot.js +36 -0
- package/template/_framework/hooks.js +207 -0
- package/template/{modules → _framework}/next-response.js +5 -13
- package/template/_framework/router.js +279 -0
- package/template/_framework/runtime.js +86 -0
- package/template/api/createUser.js +1 -1
- package/template/api/hello.js +8 -17
- package/template/api/loginUser.js +1 -1
- package/template/api/users.js +38 -134
- package/template/app/about/page.js +45 -0
- package/template/app/layout.js +23 -0
- package/template/app/page.js +54 -0
- package/template/app/test/page.js +55 -0
- package/template/components/Footer.js +10 -0
- package/template/components/Navbar.js +30 -0
- package/template/index.html +24 -0
- package/template/package.json +5 -14
- package/template/scripts/dev-server.js +330 -302
- package/template/scripts/live-reload-client.js +0 -1
- package/template/.vscode/settings.json +0 -15
- package/template/api/getUser.js +0 -60
- package/template/api/getUserWithDB.js +0 -65
- package/template/app.config.js +0 -41
- package/template/app.html +0 -55
- package/template/components/Counter.jsx +0 -36
- package/template/components/Footer.jsx +0 -9
- package/template/components/Navbar.jsx +0 -17
- package/template/docs/API-GUIDE.md +0 -196
- package/template/docs/API-ROUTES.md +0 -538
- package/template/docs/AUTO-IMPORT.md +0 -99
- package/template/docs/AUTO-REGISTRATION.md +0 -248
- package/template/docs/CSS-FRAMEWORKS.md +0 -221
- package/template/docs/DEV-SERVER.md +0 -184
- package/template/docs/GETTING-STARTED.md +0 -205
- package/template/docs/HOW-IT-WORKS.md +0 -362
- package/template/docs/NEXTJS-APP-ROUTER.md +0 -536
- package/template/docs/NEXTJS-COMPARISON.md +0 -473
- package/template/docs/QUICK-START.md +0 -111
- package/template/docs/STYLING.md +0 -269
- package/template/docs/TROUBLESHOOTING.md +0 -282
- package/template/modules/README.md +0 -129
- package/template/modules/api.js +0 -44
- package/template/modules/auto-import.js +0 -130
- package/template/modules/router.js +0 -218
- package/template/modules/state.js +0 -248
- package/template/package-lock.json +0 -13
- package/template/pages/About.jsx +0 -48
- package/template/pages/ApiDemo.jsx +0 -209
- package/template/pages/Contact.jsx +0 -25
- package/template/pages/Example.jsx +0 -59
- package/template/pages/Home.jsx +0 -39
- package/template/pages/HooksDemo.jsx +0 -194
- package/template/pages/test.jsx +0 -72
- package/template/scripts/build.js +0 -81
- package/template/scripts/generate-imports.js +0 -88
- package/template/tailwind.config.js +0 -17
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Aziz.js Framework Boot
|
|
3
|
+
// Loads layout.js + page.js for the current route
|
|
4
|
+
// Handles client-side navigation like Next.js App Router
|
|
5
|
+
// ============================================================
|
|
6
|
+
|
|
7
|
+
import { h, render, Fragment } from '/_framework/runtime.js';
|
|
8
|
+
import { initHooks, resetHooks } from '/_framework/hooks.js';
|
|
9
|
+
import { router } from '/_framework/router.js';
|
|
10
|
+
|
|
11
|
+
// Make h and Fragment globally available for JSX
|
|
12
|
+
window.h = h;
|
|
13
|
+
window.Fragment = Fragment;
|
|
14
|
+
|
|
15
|
+
// Boot the app
|
|
16
|
+
async function boot() {
|
|
17
|
+
console.log('⚡ Aziz.js booting...');
|
|
18
|
+
|
|
19
|
+
// Initialize hooks system
|
|
20
|
+
initHooks();
|
|
21
|
+
|
|
22
|
+
// Start the router
|
|
23
|
+
await router.init();
|
|
24
|
+
|
|
25
|
+
console.log('✅ Aziz.js ready');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
boot().catch(err => {
|
|
29
|
+
console.error('❌ Boot failed:', err);
|
|
30
|
+
document.getElementById('root').innerHTML = `
|
|
31
|
+
<div style="padding:40px;font-family:monospace;color:red">
|
|
32
|
+
<h2>Boot Error</h2>
|
|
33
|
+
<pre>${err.message}</pre>
|
|
34
|
+
</div>
|
|
35
|
+
`;
|
|
36
|
+
});
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Aziz.js Hooks
|
|
3
|
+
// React-identical hooks: useState, useEffect, useMemo,
|
|
4
|
+
// useCallback, useRef, useReducer, useContext
|
|
5
|
+
// ============================================================
|
|
6
|
+
|
|
7
|
+
let currentFiber = null; // current component being rendered
|
|
8
|
+
let hookIndex = 0; // current hook slot
|
|
9
|
+
|
|
10
|
+
// Called before rendering a component
|
|
11
|
+
export function prepareHooks(fiber) {
|
|
12
|
+
currentFiber = fiber;
|
|
13
|
+
hookIndex = 0;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Called after rendering a component
|
|
17
|
+
export function finishHooks() {
|
|
18
|
+
currentFiber = null;
|
|
19
|
+
hookIndex = 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Global reset (called before full page re-render)
|
|
23
|
+
export function resetHooks() {
|
|
24
|
+
currentFiber = null;
|
|
25
|
+
hookIndex = 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function initHooks() {
|
|
29
|
+
// Expose all hooks globally (no import needed in pages)
|
|
30
|
+
window.useState = useState;
|
|
31
|
+
window.useEffect = useEffect;
|
|
32
|
+
window.useMemo = useMemo;
|
|
33
|
+
window.useCallback = useCallback;
|
|
34
|
+
window.useRef = useRef;
|
|
35
|
+
window.useReducer = useReducer;
|
|
36
|
+
window.useContext = useContext;
|
|
37
|
+
window.createContext = createContext;
|
|
38
|
+
window.useLocalStorage = useLocalStorage;
|
|
39
|
+
window.useFetch = useFetch;
|
|
40
|
+
window.resetHooks = resetHooks;
|
|
41
|
+
window.prepareHooks = prepareHooks;
|
|
42
|
+
window.finishHooks = finishHooks;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ─── useState ────────────────────────────────────────────────
|
|
46
|
+
export function useState(initialValue) {
|
|
47
|
+
const fiber = currentFiber;
|
|
48
|
+
const idx = hookIndex++;
|
|
49
|
+
|
|
50
|
+
if (!fiber) throw new Error('useState called outside component');
|
|
51
|
+
|
|
52
|
+
if (fiber.hooks[idx] === undefined) {
|
|
53
|
+
fiber.hooks[idx] = {
|
|
54
|
+
value: typeof initialValue === 'function' ? initialValue() : initialValue
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const hook = fiber.hooks[idx];
|
|
59
|
+
|
|
60
|
+
const setState = (next) => {
|
|
61
|
+
const nextVal = typeof next === 'function' ? next(hook.value) : next;
|
|
62
|
+
if (nextVal !== hook.value) {
|
|
63
|
+
hook.value = nextVal;
|
|
64
|
+
scheduleRender();
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return [hook.value, setState];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ─── useReducer ──────────────────────────────────────────────
|
|
72
|
+
export function useReducer(reducer, initialState, init) {
|
|
73
|
+
const [state, setState] = useState(init ? init(initialState) : initialState);
|
|
74
|
+
const dispatch = (action) => setState(prev => reducer(prev, action));
|
|
75
|
+
return [state, dispatch];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── useEffect ───────────────────────────────────────────────
|
|
79
|
+
export function useEffect(callback, deps) {
|
|
80
|
+
const fiber = currentFiber;
|
|
81
|
+
const idx = hookIndex++;
|
|
82
|
+
|
|
83
|
+
if (!fiber) throw new Error('useEffect called outside component');
|
|
84
|
+
|
|
85
|
+
const prev = fiber.hooks[idx];
|
|
86
|
+
const changed = !prev || !deps || deps.some((d, i) => d !== prev.deps?.[i]);
|
|
87
|
+
|
|
88
|
+
if (changed) {
|
|
89
|
+
// Queue cleanup + effect after render
|
|
90
|
+
queueMicrotask(() => {
|
|
91
|
+
if (prev?.cleanup) prev.cleanup();
|
|
92
|
+
const cleanup = callback();
|
|
93
|
+
if (fiber.hooks[idx]) {
|
|
94
|
+
fiber.hooks[idx].cleanup = typeof cleanup === 'function' ? cleanup : undefined;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
fiber.hooks[idx] = { deps, cleanup: prev?.cleanup };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ─── useMemo ─────────────────────────────────────────────────
|
|
103
|
+
export function useMemo(factory, deps) {
|
|
104
|
+
const fiber = currentFiber;
|
|
105
|
+
const idx = hookIndex++;
|
|
106
|
+
|
|
107
|
+
if (!fiber) throw new Error('useMemo called outside component');
|
|
108
|
+
|
|
109
|
+
const prev = fiber.hooks[idx];
|
|
110
|
+
const changed = !prev || deps.some((d, i) => d !== prev.deps?.[i]);
|
|
111
|
+
|
|
112
|
+
if (changed) {
|
|
113
|
+
fiber.hooks[idx] = { value: factory(), deps };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return fiber.hooks[idx].value;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ─── useCallback ─────────────────────────────────────────────
|
|
120
|
+
export function useCallback(fn, deps) {
|
|
121
|
+
return useMemo(() => fn, deps);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ─── useRef ──────────────────────────────────────────────────
|
|
125
|
+
export function useRef(initial) {
|
|
126
|
+
const fiber = currentFiber;
|
|
127
|
+
const idx = hookIndex++;
|
|
128
|
+
|
|
129
|
+
if (!fiber) throw new Error('useRef called outside component');
|
|
130
|
+
|
|
131
|
+
if (!fiber.hooks[idx]) {
|
|
132
|
+
fiber.hooks[idx] = { current: initial };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return fiber.hooks[idx];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ─── useContext ───────────────────────────────────────────────
|
|
139
|
+
const contexts = new Map();
|
|
140
|
+
|
|
141
|
+
export function createContext(defaultValue) {
|
|
142
|
+
const ctx = {
|
|
143
|
+
_value: defaultValue,
|
|
144
|
+
Provider({ value, children }) {
|
|
145
|
+
ctx._value = value;
|
|
146
|
+
return children;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
return ctx;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function useContext(ctx) {
|
|
153
|
+
return ctx._value;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ─── useLocalStorage ─────────────────────────────────────────
|
|
157
|
+
export function useLocalStorage(key, initial) {
|
|
158
|
+
const [value, setValue] = useState(() => {
|
|
159
|
+
try {
|
|
160
|
+
const stored = localStorage.getItem(key);
|
|
161
|
+
return stored !== null ? JSON.parse(stored) : initial;
|
|
162
|
+
} catch { return initial; }
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const set = (next) => {
|
|
166
|
+
const val = typeof next === 'function' ? next(value) : next;
|
|
167
|
+
setValue(val);
|
|
168
|
+
try { localStorage.setItem(key, JSON.stringify(val)); } catch {}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
return [value, set];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ─── useFetch ────────────────────────────────────────────────
|
|
175
|
+
export function useFetch(url, options = {}) {
|
|
176
|
+
const [data, setData] = useState(null);
|
|
177
|
+
const [loading, setLoading] = useState(true);
|
|
178
|
+
const [error, setError] = useState(null);
|
|
179
|
+
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
let cancelled = false;
|
|
182
|
+
setLoading(true);
|
|
183
|
+
|
|
184
|
+
fetch(url, options)
|
|
185
|
+
.then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); })
|
|
186
|
+
.then(d => { if (!cancelled) { setData(d); setLoading(false); } })
|
|
187
|
+
.catch(e => { if (!cancelled) { setError(e.message); setLoading(false); } });
|
|
188
|
+
|
|
189
|
+
return () => { cancelled = true; };
|
|
190
|
+
}, [url]);
|
|
191
|
+
|
|
192
|
+
return { data, loading, error };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ─── Render scheduler ────────────────────────────────────────
|
|
196
|
+
let renderScheduled = false;
|
|
197
|
+
|
|
198
|
+
function scheduleRender() {
|
|
199
|
+
if (renderScheduled) return;
|
|
200
|
+
renderScheduled = true;
|
|
201
|
+
requestAnimationFrame(() => {
|
|
202
|
+
renderScheduled = false;
|
|
203
|
+
if (typeof window.__rerenderPage === 'function') {
|
|
204
|
+
window.__rerenderPage();
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
// NextResponse
|
|
1
|
+
// NextResponse — identical to Next.js App Router API
|
|
2
|
+
// Used in api/ route handlers
|
|
3
|
+
|
|
2
4
|
class NextResponse {
|
|
3
5
|
constructor(body, init = {}) {
|
|
4
6
|
this.body = body;
|
|
@@ -40,19 +42,9 @@ class NextResponse {
|
|
|
40
42
|
static error() {
|
|
41
43
|
return new NextResponse(JSON.stringify({ error: 'Internal Server Error' }), {
|
|
42
44
|
status: 500,
|
|
43
|
-
headers: {
|
|
44
|
-
'Content-Type': 'application/json'
|
|
45
|
-
}
|
|
45
|
+
headers: { 'Content-Type': 'application/json' }
|
|
46
46
|
});
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
if (typeof module !== 'undefined' && module.exports) {
|
|
52
|
-
module.exports = { NextResponse };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Export for browser (if needed)
|
|
56
|
-
if (typeof window !== 'undefined') {
|
|
57
|
-
window.NextResponse = NextResponse;
|
|
58
|
-
}
|
|
50
|
+
module.exports = { NextResponse };
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Aziz.js Router
|
|
3
|
+
// File-based routing like Next.js App Router
|
|
4
|
+
//
|
|
5
|
+
// app/
|
|
6
|
+
// layout.js → root layout (wraps everything)
|
|
7
|
+
// page.js → /
|
|
8
|
+
// about/
|
|
9
|
+
// page.js → /about
|
|
10
|
+
// layout.js → layout for /about and children
|
|
11
|
+
// blog/
|
|
12
|
+
// [slug]/
|
|
13
|
+
// page.js → /blog/:slug
|
|
14
|
+
// ============================================================
|
|
15
|
+
|
|
16
|
+
import { h, render } from '/_framework/runtime.js';
|
|
17
|
+
import { prepareHooks, finishHooks, resetHooks } from '/_framework/hooks.js';
|
|
18
|
+
|
|
19
|
+
// All registered routes (populated by dev server scan)
|
|
20
|
+
const routes = {};
|
|
21
|
+
|
|
22
|
+
// Current fiber tree for hooks
|
|
23
|
+
const fibers = new Map();
|
|
24
|
+
|
|
25
|
+
function getFiber(id) {
|
|
26
|
+
if (!fibers.has(id)) {
|
|
27
|
+
fibers.set(id, { hooks: [] });
|
|
28
|
+
}
|
|
29
|
+
return fibers.get(id);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Wrap a component so hooks work correctly
|
|
33
|
+
function withHooks(id, Component) {
|
|
34
|
+
return function WrappedComponent(props) {
|
|
35
|
+
const fiber = getFiber(id);
|
|
36
|
+
prepareHooks(fiber);
|
|
37
|
+
const result = Component(props);
|
|
38
|
+
finishHooks();
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Navigate to a path
|
|
44
|
+
export function navigate(path) {
|
|
45
|
+
history.pushState(null, '', path);
|
|
46
|
+
router.render();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Link component (like Next.js <Link>)
|
|
50
|
+
export function Link({ href, children, className, style }) {
|
|
51
|
+
return h('a', {
|
|
52
|
+
href,
|
|
53
|
+
class: className,
|
|
54
|
+
style,
|
|
55
|
+
onClick: (e) => {
|
|
56
|
+
e.preventDefault();
|
|
57
|
+
navigate(href);
|
|
58
|
+
}
|
|
59
|
+
}, ...(Array.isArray(children) ? children : [children]));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// useRouter hook
|
|
63
|
+
export function useRouter() {
|
|
64
|
+
return {
|
|
65
|
+
pathname: window.location.pathname,
|
|
66
|
+
push: navigate,
|
|
67
|
+
back: () => history.back(),
|
|
68
|
+
forward: () => history.forward(),
|
|
69
|
+
params: router.currentParams || {}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// useParams hook
|
|
74
|
+
export function useParams() {
|
|
75
|
+
return router.currentParams || {};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// useSearchParams hook
|
|
79
|
+
export function useSearchParams() {
|
|
80
|
+
const params = new URLSearchParams(window.location.search);
|
|
81
|
+
return {
|
|
82
|
+
get: (key) => params.get(key),
|
|
83
|
+
getAll: (key) => params.getAll(key),
|
|
84
|
+
has: (key) => params.has(key),
|
|
85
|
+
toString: () => params.toString()
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Match a route pattern to a pathname
|
|
90
|
+
// Supports: /about, /blog/[slug], /blog/[...slug]
|
|
91
|
+
function matchRoute(pattern, pathname) {
|
|
92
|
+
const patternParts = pattern.split('/').filter(Boolean);
|
|
93
|
+
const pathParts = pathname.split('/').filter(Boolean);
|
|
94
|
+
const params = {};
|
|
95
|
+
|
|
96
|
+
// Catch-all: /blog/[...slug]
|
|
97
|
+
const catchAll = patternParts.findIndex(p => p.startsWith('[...'));
|
|
98
|
+
if (catchAll !== -1) {
|
|
99
|
+
if (pathParts.length < catchAll) return null;
|
|
100
|
+
const paramName = patternParts[catchAll].slice(4, -1);
|
|
101
|
+
params[paramName] = pathParts.slice(catchAll);
|
|
102
|
+
return params;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (patternParts.length !== pathParts.length) return null;
|
|
106
|
+
|
|
107
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
108
|
+
if (patternParts[i].startsWith('[') && patternParts[i].endsWith(']')) {
|
|
109
|
+
// Dynamic segment
|
|
110
|
+
params[patternParts[i].slice(1, -1)] = pathParts[i];
|
|
111
|
+
} else if (patternParts[i] !== pathParts[i]) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return params;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Find the best matching route for a pathname
|
|
120
|
+
function resolveRoute(pathname) {
|
|
121
|
+
// Exact match first
|
|
122
|
+
if (routes[pathname]) {
|
|
123
|
+
return { route: routes[pathname], params: {} };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Pattern match
|
|
127
|
+
for (const pattern in routes) {
|
|
128
|
+
if (pattern.includes('[')) {
|
|
129
|
+
const params = matchRoute(pattern, pathname);
|
|
130
|
+
if (params !== null) {
|
|
131
|
+
return { route: routes[pattern], params };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Collect layout chain for a path
|
|
140
|
+
// /about/team → [app/layout.js, app/about/layout.js]
|
|
141
|
+
function getLayoutChain(pathname) {
|
|
142
|
+
return router.layouts.filter(({ path }) => {
|
|
143
|
+
if (path === '/') return true;
|
|
144
|
+
return pathname === path || pathname.startsWith(path + '/');
|
|
145
|
+
}).sort((a, b) => a.path.length - b.path.length);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export const router = {
|
|
149
|
+
layouts: [],
|
|
150
|
+
currentParams: {},
|
|
151
|
+
|
|
152
|
+
// Register a route (called by dev server scan result)
|
|
153
|
+
register(pathname, module) {
|
|
154
|
+
routes[pathname] = module;
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
// Register a layout
|
|
158
|
+
registerLayout(pathname, module) {
|
|
159
|
+
this.layouts.push({ path: pathname, module });
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
async init() {
|
|
163
|
+
// Expose globals
|
|
164
|
+
window.navigate = navigate;
|
|
165
|
+
window.useRouter = useRouter;
|
|
166
|
+
window.useParams = useParams;
|
|
167
|
+
window.useSearchParams = useSearchParams;
|
|
168
|
+
window.Link = Link;
|
|
169
|
+
window.__rerenderPage = () => this.render();
|
|
170
|
+
|
|
171
|
+
// Load routes from server
|
|
172
|
+
await this.loadRoutes();
|
|
173
|
+
|
|
174
|
+
// Handle browser back/forward
|
|
175
|
+
window.addEventListener('popstate', () => this.render());
|
|
176
|
+
|
|
177
|
+
// Initial render
|
|
178
|
+
await this.render();
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
async loadRoutes() {
|
|
182
|
+
try {
|
|
183
|
+
// Dev server exposes the route manifest
|
|
184
|
+
const res = await fetch('/_framework/routes.json');
|
|
185
|
+
const manifest = await res.json();
|
|
186
|
+
|
|
187
|
+
// Dynamically import each page and layout
|
|
188
|
+
for (const { pathname, filePath, type } of manifest) {
|
|
189
|
+
try {
|
|
190
|
+
const mod = await import(filePath + '?t=' + Date.now());
|
|
191
|
+
if (type === 'page') {
|
|
192
|
+
this.register(pathname, mod);
|
|
193
|
+
} else if (type === 'layout') {
|
|
194
|
+
this.registerLayout(pathname, mod);
|
|
195
|
+
}
|
|
196
|
+
} catch (err) {
|
|
197
|
+
console.error(`Failed to load ${filePath}:`, err);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
console.log(`📄 Loaded ${Object.keys(routes).length} routes`);
|
|
202
|
+
} catch (err) {
|
|
203
|
+
console.error('Failed to load routes:', err);
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
async render() {
|
|
208
|
+
const pathname = window.location.pathname;
|
|
209
|
+
const root = document.getElementById('root');
|
|
210
|
+
|
|
211
|
+
// Find matching route
|
|
212
|
+
const match = resolveRoute(pathname);
|
|
213
|
+
|
|
214
|
+
if (!match) {
|
|
215
|
+
root.innerHTML = '';
|
|
216
|
+
const notFound = h('div', { style: { padding: '40px', textAlign: 'center' } },
|
|
217
|
+
h('h1', { style: { fontSize: '80px', color: '#61dafb' } }, '404'),
|
|
218
|
+
h('p', {}, 'Page not found'),
|
|
219
|
+
h('button', {
|
|
220
|
+
style: { padding: '10px 24px', cursor: 'pointer' },
|
|
221
|
+
onClick: () => navigate('/')
|
|
222
|
+
}, 'Go Home')
|
|
223
|
+
);
|
|
224
|
+
root.appendChild(notFound);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
this.currentParams = match.params;
|
|
229
|
+
|
|
230
|
+
const { route } = match;
|
|
231
|
+
const PageComponent = route.default;
|
|
232
|
+
|
|
233
|
+
if (!PageComponent) {
|
|
234
|
+
console.error(`Route has no default export: ${pathname}`);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Get layout chain
|
|
239
|
+
const layoutChain = getLayoutChain(pathname);
|
|
240
|
+
|
|
241
|
+
// Wrap page in layouts (innermost first, outermost last)
|
|
242
|
+
let content = () => {
|
|
243
|
+
const fiber = getFiber('page:' + pathname);
|
|
244
|
+
prepareHooks(fiber);
|
|
245
|
+
const result = PageComponent({});
|
|
246
|
+
finishHooks();
|
|
247
|
+
return result;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// Apply layouts from innermost to outermost
|
|
251
|
+
for (let i = layoutChain.length - 1; i >= 0; i--) {
|
|
252
|
+
const { module: layoutMod, path: layoutPath } = layoutChain[i];
|
|
253
|
+
const LayoutComponent = layoutMod.default;
|
|
254
|
+
if (!LayoutComponent) continue;
|
|
255
|
+
|
|
256
|
+
const innerContent = content;
|
|
257
|
+
content = () => {
|
|
258
|
+
const fiber = getFiber('layout:' + layoutPath);
|
|
259
|
+
prepareHooks(fiber);
|
|
260
|
+
const result = LayoutComponent({ children: innerContent() });
|
|
261
|
+
finishHooks();
|
|
262
|
+
return result;
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Render
|
|
267
|
+
root.innerHTML = '';
|
|
268
|
+
const tree = content();
|
|
269
|
+
if (Array.isArray(tree)) {
|
|
270
|
+
tree.forEach(node => {
|
|
271
|
+
if (node instanceof Node) root.appendChild(node);
|
|
272
|
+
});
|
|
273
|
+
} else if (tree instanceof Node) {
|
|
274
|
+
root.appendChild(tree);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
export default router;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Aziz.js Runtime
|
|
3
|
+
// h() — JSX factory (like React.createElement)
|
|
4
|
+
// render() — mount component tree to DOM
|
|
5
|
+
// ============================================================
|
|
6
|
+
|
|
7
|
+
export const Fragment = Symbol('Fragment');
|
|
8
|
+
|
|
9
|
+
// JSX factory — Babel compiles <div> → h("div", props, ...children)
|
|
10
|
+
export function h(tag, props, ...children) {
|
|
11
|
+
// Fragment: just return children
|
|
12
|
+
if (tag === Fragment) {
|
|
13
|
+
return flatChildren(children);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Component function
|
|
17
|
+
if (typeof tag === 'function') {
|
|
18
|
+
return tag({ ...(props || {}), children: flatChildren(children) });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const el = document.createElement(tag);
|
|
22
|
+
|
|
23
|
+
if (props) {
|
|
24
|
+
for (const key in props) {
|
|
25
|
+
if (key === 'children') continue;
|
|
26
|
+
|
|
27
|
+
if (key === 'ref' && props[key] && typeof props[key] === 'object') {
|
|
28
|
+
// useRef support
|
|
29
|
+
props[key].current = el;
|
|
30
|
+
} else if (key.startsWith('on')) {
|
|
31
|
+
// onClick → click
|
|
32
|
+
el.addEventListener(key.slice(2).toLowerCase(), props[key]);
|
|
33
|
+
} else if (key === 'className' || key === 'class') {
|
|
34
|
+
el.className = props[key];
|
|
35
|
+
} else if (key === 'htmlFor') {
|
|
36
|
+
el.setAttribute('for', props[key]);
|
|
37
|
+
} else if (key === 'style' && typeof props[key] === 'object') {
|
|
38
|
+
Object.assign(el.style, props[key]);
|
|
39
|
+
} else if (key === 'dangerouslySetInnerHTML') {
|
|
40
|
+
el.innerHTML = props[key].__html;
|
|
41
|
+
} else if (key === 'value' && (tag === 'input' || tag === 'textarea' || tag === 'select')) {
|
|
42
|
+
el.value = props[key];
|
|
43
|
+
} else if (key === 'checked' && tag === 'input') {
|
|
44
|
+
el.checked = props[key];
|
|
45
|
+
} else if (key === 'disabled') {
|
|
46
|
+
if (props[key]) el.setAttribute('disabled', '');
|
|
47
|
+
} else if (key === 'key') {
|
|
48
|
+
// ignore reconciliation key for now
|
|
49
|
+
} else {
|
|
50
|
+
el.setAttribute(key, props[key]);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
appendChildren(el, flatChildren(children));
|
|
56
|
+
return el;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function flatChildren(children) {
|
|
60
|
+
return children.flat(Infinity).filter(c => c !== null && c !== undefined && c !== false && c !== true);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function appendChildren(parent, children) {
|
|
64
|
+
for (const child of children) {
|
|
65
|
+
if (child instanceof Node) {
|
|
66
|
+
parent.appendChild(child);
|
|
67
|
+
} else if (Array.isArray(child)) {
|
|
68
|
+
appendChildren(parent, child);
|
|
69
|
+
} else {
|
|
70
|
+
parent.appendChild(document.createTextNode(String(child)));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Mount a component tree into a DOM element
|
|
76
|
+
export function render(component, container) {
|
|
77
|
+
container.innerHTML = '';
|
|
78
|
+
const result = typeof component === 'function' ? component() : component;
|
|
79
|
+
if (Array.isArray(result)) {
|
|
80
|
+
appendChildren(container, result);
|
|
81
|
+
} else if (result instanceof Node) {
|
|
82
|
+
container.appendChild(result);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export default { h, render, Fragment };
|
package/template/api/hello.js
CHANGED
|
@@ -1,26 +1,17 @@
|
|
|
1
1
|
// API Route: /api/hello
|
|
2
|
-
//
|
|
2
|
+
// Next.js App Router style
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
const { method, query } = req;
|
|
4
|
+
const { NextResponse } = require('../_framework/next-response');
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
error: 'Method not allowed',
|
|
11
|
-
message: `Method ${method} is not allowed. Use GET instead.`
|
|
12
|
-
});
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const name = query.name || 'World';
|
|
6
|
+
async function GET(request) {
|
|
7
|
+
const { searchParams } = new URL(request.url);
|
|
8
|
+
const name = searchParams.get('name') || 'World';
|
|
16
9
|
|
|
17
|
-
return
|
|
10
|
+
return NextResponse.json({
|
|
18
11
|
message: `Hello, ${name}!`,
|
|
19
12
|
timestamp: new Date().toISOString(),
|
|
20
|
-
method: method
|
|
21
|
-
query: query
|
|
13
|
+
method: request.method
|
|
22
14
|
});
|
|
23
15
|
}
|
|
24
16
|
|
|
25
|
-
|
|
26
|
-
module.exports = handler;
|
|
17
|
+
module.exports = { GET };
|