create-aziz-app 3.1.2 → 3.1.3
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 +658 -207
- package/bin/create-aziz-app.js +302 -302
- package/package.json +1 -1
- package/template/README.md +402 -402
- package/template/_framework/api.js +219 -219
- package/template/_framework/boot.js +33 -33
- package/template/_framework/hooks.js +367 -291
- package/template/_framework/next-response.js +128 -128
- package/template/_framework/route-match.js +145 -145
- package/template/_framework/router.js +465 -259
- package/template/_framework/runtime.js +264 -264
- package/template/app/layout.js +18 -18
- package/template/app/page.js +33 -33
- package/template/docs/01-getting-started.md +151 -151
- package/template/docs/02-project-structure.md +109 -109
- package/template/docs/03-pages-and-layouts.md +140 -140
- package/template/docs/04-routing.md +120 -120
- package/template/docs/05-hooks.md +169 -169
- package/template/docs/06-api-routes.md +210 -210
- package/template/docs/07-api-client.md +151 -151
- package/template/docs/08-styling.md +94 -94
- package/template/docs/09-imports-and-modules.md +97 -97
- package/template/docs/10-database-and-models.md +154 -154
- package/template/docs/11-authentication.md +171 -171
- package/template/docs/12-production-and-deployment.md +121 -121
- package/template/docs/13-internals.md +179 -179
- package/template/docs/README.md +67 -67
- package/template/index.html +20 -20
- package/template/package.json +16 -16
- package/template/scripts/dev-server.js +493 -381
- package/template/scripts/lib/api-router.js +525 -525
- package/template/scripts/lib/js-transform.js +213 -195
- package/template/scripts/lib/module-resolver.js +132 -132
|
@@ -1,261 +1,467 @@
|
|
|
1
|
-
// ============================================================
|
|
2
|
-
// Aziz.js Router
|
|
3
|
-
// File-based routing like the Next.js App Router.
|
|
4
|
-
//
|
|
5
|
-
// app/
|
|
6
|
-
// layout.js → root layout (wraps everything)
|
|
7
|
-
// page.js → /
|
|
8
|
-
// not-found.js → custom 404
|
|
9
|
-
// about/
|
|
10
|
-
// page.js → /about
|
|
11
|
-
// layout.js → layout for /about and children
|
|
12
|
-
// blog/
|
|
13
|
-
// [slug]/
|
|
14
|
-
// page.js → /blog/:slug
|
|
15
|
-
// ============================================================
|
|
16
|
-
|
|
1
|
+
// ============================================================
|
|
2
|
+
// Aziz.js Router
|
|
3
|
+
// File-based routing like the Next.js App Router.
|
|
4
|
+
//
|
|
5
|
+
// app/
|
|
6
|
+
// layout.js → root layout (wraps everything)
|
|
7
|
+
// page.js → /
|
|
8
|
+
// not-found.js → custom 404
|
|
9
|
+
// about/
|
|
10
|
+
// page.js → /about
|
|
11
|
+
// layout.js → layout for /about and children
|
|
12
|
+
// blog/
|
|
13
|
+
// [slug]/
|
|
14
|
+
// page.js → /blog/:slug
|
|
15
|
+
// ============================================================
|
|
16
|
+
|
|
17
17
|
import { h, mountPage, disposeFibers, beginRender, materialize, appendNodes } from '/_framework/runtime.js';
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
export
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
18
|
+
import { useState, useCallback, getPendingLoads, onLoadState } from '/_framework/hooks.js';
|
|
19
|
+
import { normalizePath, resolveRoute, collectLayouts } from '/_framework/route-match.js';
|
|
20
|
+
|
|
21
|
+
// Registered routes / layouts (populated by the dev server manifest)
|
|
22
|
+
const routes = {};
|
|
23
|
+
const layouts = [];
|
|
24
|
+
const loadings = [];
|
|
25
|
+
const errors = [];
|
|
26
|
+
let notFoundModule = null;
|
|
27
|
+
let dev = true;
|
|
28
|
+
|
|
29
|
+
// How long a route transition may stay pending before loading.js appears.
|
|
30
|
+
// Below this the route is "fast enough" and showing a spinner would only flash.
|
|
31
|
+
const LOADING_DELAY = 200;
|
|
32
|
+
|
|
33
|
+
// The nearest loading.js / error.js for a path — same prefix-inheritance
|
|
34
|
+
// rules as layouts, so app/loading.js covers every route beneath it and a
|
|
35
|
+
// deeper app/blog/loading.js overrides it for /blog only.
|
|
36
|
+
function collectNearest(list, pathname) {
|
|
37
|
+
const matches = list.filter(e => matchesSegment(e.path, pathname));
|
|
38
|
+
if (matches.length === 0) return null;
|
|
39
|
+
matches.sort((a, b) => b.path.split('/').filter(Boolean).length - a.path.split('/').filter(Boolean).length);
|
|
40
|
+
return matches[0];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function matchesSegment(pattern, pathname) {
|
|
44
|
+
const p = pattern.split('/').filter(Boolean);
|
|
45
|
+
const s = pathname.split('/').filter(Boolean);
|
|
46
|
+
if (p.length > s.length) return false;
|
|
47
|
+
for (let i = 0; i < p.length; i++) if (p[i] !== s[i]) return false;
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── Navigation ──────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
export function navigate(path, { replace = false, scroll = true } = {}) {
|
|
54
|
+
if (replace) history.replaceState(null, '', path);
|
|
55
|
+
else history.pushState(null, '', path);
|
|
56
|
+
router.render();
|
|
57
|
+
if (scroll) window.scrollTo(0, 0);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function replace(path) {
|
|
61
|
+
navigate(path, { replace: true });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function back() { history.back(); }
|
|
65
|
+
export function forward() { history.forward(); }
|
|
66
|
+
export function refresh() { router.render(); }
|
|
67
|
+
|
|
68
|
+
// Server-style redirect from inside a component or an effect.
|
|
69
|
+
export function redirect(url, options) {
|
|
70
|
+
navigate(url, { replace: true, ...options });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Thrown by notFound() to render the 404 screen from a page.
|
|
74
|
+
const NOT_FOUND_SIGNAL = 'AZIZ_NOT_FOUND';
|
|
75
|
+
|
|
76
|
+
export function notFound() {
|
|
77
|
+
throw new Error(NOT_FOUND_SIGNAL);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// <Link href="/about">About</Link>
|
|
81
|
+
export function Link({ href, children, className, style, onClick, replace = false, scroll = true, prefetch = false, ...rest }) {
|
|
82
|
+
return h('a', {
|
|
83
|
+
...rest,
|
|
84
|
+
href,
|
|
85
|
+
class: className,
|
|
86
|
+
style,
|
|
87
|
+
onClick: (e) => {
|
|
88
|
+
if (typeof onClick === 'function') onClick(e);
|
|
89
|
+
if (e.defaultPrevented) return;
|
|
90
|
+
// Respect modifier clicks, middle-click, external links
|
|
91
|
+
if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
92
|
+
if (!href || /^([a-z]+:)?\/\//i.test(href) || href.startsWith('#') || href.startsWith('mailto:')) return;
|
|
93
|
+
e.preventDefault();
|
|
94
|
+
navigate(href, { replace, scroll });
|
|
95
|
+
},
|
|
96
|
+
}, ...(Array.isArray(children) ? children : [children]));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ─── Router hooks ────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
export function useRouter() {
|
|
102
|
+
return {
|
|
103
|
+
pathname: normalizePath(window.location.pathname),
|
|
104
|
+
push: (path, options) => navigate(path, options),
|
|
105
|
+
replace: (path, options) => navigate(path, { ...options, replace: true }),
|
|
106
|
+
back,
|
|
107
|
+
forward,
|
|
108
|
+
refresh,
|
|
109
|
+
// Aziz.js has no route prefetching (see README) — kept for API parity.
|
|
110
|
+
prefetch: () => Promise.resolve(),
|
|
111
|
+
params: router.currentParams || {},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function useParams() {
|
|
116
|
+
return router.currentParams || {};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Next.js parity: read the current path without reaching for window.location.
|
|
120
|
+
export function usePathname() {
|
|
121
|
+
return normalizePath(window.location.pathname);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Next.js parity: transition state. Client-side routing in Aziz.js is
|
|
125
|
+
// synchronous — there is no server round-trip to await — so isPending is
|
|
126
|
+
// always false between events.
|
|
127
|
+
export function useNavigation() {
|
|
128
|
+
return {
|
|
129
|
+
isPending: false,
|
|
130
|
+
state: 'idle',
|
|
131
|
+
...useRouter(),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Next.js parity: the real App Router transition hook. Wraps a callback so
|
|
136
|
+
// async work reports isPending while it runs; synchronous callbacks resolve
|
|
137
|
+
// immediately because there is nothing to suspend on.
|
|
138
|
+
export function useTransition() {
|
|
139
|
+
const [isPending, setIsPending] = useState(false);
|
|
140
|
+
|
|
141
|
+
const startTransition = useCallback((callback) => {
|
|
142
|
+
let result;
|
|
143
|
+
try {
|
|
144
|
+
result = callback();
|
|
145
|
+
} catch (err) {
|
|
146
|
+
setIsPending(false);
|
|
147
|
+
throw err;
|
|
148
|
+
}
|
|
149
|
+
if (result && typeof result.then === 'function') {
|
|
150
|
+
setIsPending(true);
|
|
151
|
+
return result.finally(() => setIsPending(false));
|
|
152
|
+
}
|
|
153
|
+
return result;
|
|
154
|
+
}, []);
|
|
155
|
+
|
|
156
|
+
return [isPending, startTransition];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function useSearchParams() {
|
|
160
|
+
const params = new URLSearchParams(window.location.search);
|
|
161
|
+
return {
|
|
162
|
+
get: (key) => params.get(key),
|
|
163
|
+
getAll: (key) => params.getAll(key),
|
|
164
|
+
has: (key) => params.has(key),
|
|
165
|
+
entries: () => params.entries(),
|
|
166
|
+
keys: () => params.keys(),
|
|
167
|
+
values: () => params.values(),
|
|
168
|
+
forEach: (fn) => params.forEach(fn),
|
|
169
|
+
toString: () => params.toString(),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ─── Default 404 ─────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
function DefaultNotFound() {
|
|
176
|
+
return h('div', { style: { padding: '60px 24px', textAlign: 'center' } },
|
|
177
|
+
h('h1', { style: { fontSize: '72px', color: '#61dafb', margin: '0 0 8px' } }, '404'),
|
|
178
|
+
h('p', { style: { color: '#666', marginBottom: '24px' } }, 'This page could not be found.'),
|
|
179
|
+
h('button', {
|
|
180
|
+
class: 'btn btn-primary',
|
|
181
|
+
style: { padding: '10px 24px', cursor: 'pointer' },
|
|
182
|
+
onClick: () => navigate('/'),
|
|
183
|
+
}, 'Go Home')
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ─── Focus preservation ──────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
function captureFocus() {
|
|
190
|
+
const el = document.activeElement;
|
|
191
|
+
if (!el || el === document.body) return null;
|
|
192
|
+
if (!/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName || '')) return null;
|
|
193
|
+
return {
|
|
194
|
+
id: el.id || null,
|
|
195
|
+
name: el.getAttribute?.('name') || null,
|
|
196
|
+
placeholder: el.getAttribute?.('placeholder') || null,
|
|
197
|
+
tag: el.tagName,
|
|
198
|
+
start: el.selectionStart ?? null,
|
|
199
|
+
end: el.selectionEnd ?? null,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function restoreFocus(root, saved) {
|
|
204
|
+
if (!saved) return;
|
|
205
|
+
let el = null;
|
|
206
|
+
if (saved.id) el = root.querySelector(`#${CSS.escape(saved.id)}`);
|
|
207
|
+
if (!el && saved.name) el = root.querySelector(`[name="${CSS.escape(saved.name)}"]`);
|
|
208
|
+
if (!el && saved.placeholder) {
|
|
209
|
+
el = root.querySelector(`${saved.tag.toLowerCase()}[placeholder="${CSS.escape(saved.placeholder)}"]`);
|
|
210
|
+
}
|
|
211
|
+
if (el && typeof el.focus === 'function') {
|
|
212
|
+
el.focus({ preventScroll: true });
|
|
213
|
+
if (saved.start !== null && typeof el.setSelectionRange === 'function') {
|
|
214
|
+
try { el.setSelectionRange(saved.start, saved.end); } catch { /* non-text input */ }
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ─── Metadata ────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
function metaTitle(value) {
|
|
222
|
+
if (typeof value === 'string') return value;
|
|
223
|
+
if (value && typeof value === 'object') return value.absolute || value.default || '';
|
|
224
|
+
return '';
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function applyMetadata(layoutModules, pageModule) {
|
|
228
|
+
// Outer → inner; page metadata wins last.
|
|
229
|
+
const metas = [...layoutModules.map(m => m && m.metadata), pageModule && pageModule.metadata]
|
|
230
|
+
.filter(Boolean);
|
|
231
|
+
if (metas.length === 0) return;
|
|
232
|
+
|
|
233
|
+
const merged = Object.assign({}, ...metas);
|
|
234
|
+
|
|
235
|
+
const title = metaTitle(merged.title);
|
|
236
|
+
if (title) document.title = title;
|
|
237
|
+
|
|
238
|
+
if (merged.description) {
|
|
239
|
+
let tag = document.querySelector('meta[name="description"]');
|
|
240
|
+
if (!tag) {
|
|
241
|
+
tag = document.createElement('meta');
|
|
242
|
+
tag.setAttribute('name', 'description');
|
|
243
|
+
document.head.appendChild(tag);
|
|
244
|
+
}
|
|
245
|
+
tag.setAttribute('content', merged.description);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ─── Router object ───────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
export const router = {
|
|
252
|
+
layouts,
|
|
253
|
+
currentParams: {},
|
|
254
|
+
_lastPathname: null,
|
|
255
|
+
// Transition bookkeeping for loading.js. _epoch invalidates stale timers
|
|
256
|
+
// so a fast navigation can never be replaced by a slow one's spinner.
|
|
257
|
+
_epoch: 0,
|
|
258
|
+
_showingLoading: false,
|
|
259
|
+
_pendingAtStart: 0,
|
|
260
|
+
|
|
261
|
+
register(pathname, module) {
|
|
262
|
+
routes[pathname] = module;
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
registerLayout(pathname, module) {
|
|
266
|
+
layouts.push({ path: pathname, module });
|
|
267
|
+
},
|
|
268
|
+
|
|
269
|
+
registerLoading(pathname, module) {
|
|
270
|
+
loadings.push({ path: pathname, module });
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
registerError(pathname, module) {
|
|
274
|
+
errors.push({ path: pathname, module });
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
registerNotFound(module) {
|
|
278
|
+
notFoundModule = module;
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
async init() {
|
|
282
|
+
// Navigation API — globals so pages need no imports.
|
|
283
|
+
window.navigate = navigate;
|
|
284
|
+
window.useRouter = useRouter;
|
|
285
|
+
window.useParams = useParams;
|
|
286
|
+
window.useSearchParams = useSearchParams;
|
|
287
|
+
window.usePathname = usePathname;
|
|
288
|
+
window.useNavigation = useNavigation;
|
|
289
|
+
window.useTransition = useTransition;
|
|
290
|
+
window.Link = Link;
|
|
291
|
+
window.redirect = redirect;
|
|
292
|
+
window.notFound = notFound;
|
|
293
|
+
window.back = back;
|
|
294
|
+
window.forward = forward;
|
|
295
|
+
window.refresh = refresh;
|
|
296
|
+
window.__rerenderPage = () => this.render();
|
|
297
|
+
|
|
298
|
+
await this.loadRoutes();
|
|
299
|
+
|
|
300
|
+
window.addEventListener('popstate', () => this.render());
|
|
301
|
+
|
|
302
|
+
await this.render();
|
|
303
|
+
},
|
|
304
|
+
|
|
305
|
+
async loadRoutes() {
|
|
306
|
+
let manifest;
|
|
307
|
+
try {
|
|
308
|
+
const res = await fetch('/_framework/routes.json');
|
|
309
|
+
const data = await res.json();
|
|
310
|
+
manifest = Array.isArray(data) ? data : data.routes || [];
|
|
311
|
+
dev = Array.isArray(data) ? true : data.dev !== false;
|
|
312
|
+
window.__azizDev = dev;
|
|
313
|
+
} catch (err) {
|
|
314
|
+
console.error('❌ Failed to load route manifest:', err);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
for (const entry of manifest) {
|
|
319
|
+
const { pathname, filePath, type } = entry;
|
|
320
|
+
try {
|
|
321
|
+
// Cache-bust in development; allow HTTP caching in production
|
|
322
|
+
const url = dev ? filePath + '?t=' + Date.now() : filePath;
|
|
323
|
+
const mod = await import(url);
|
|
324
|
+
if (type === 'page') this.register(pathname, mod);
|
|
325
|
+
else if (type === 'layout') this.registerLayout(pathname, mod);
|
|
326
|
+
else if (type === 'loading') this.registerLoading(pathname, mod);
|
|
327
|
+
else if (type === 'error') this.registerError(pathname, mod);
|
|
328
|
+
else if (type === 'not-found') this.registerNotFound(mod);
|
|
329
|
+
} catch (err) {
|
|
330
|
+
console.error(`❌ Failed to load ${filePath}:`, err);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
|
|
335
|
+
// ── Route transition (drives loading.js) ─────────────────
|
|
336
|
+
//
|
|
337
|
+
// Every page module is imported up front at boot, so a navigation has no
|
|
338
|
+
// module-fetch window — data fetching is the only genuinely async part.
|
|
339
|
+
// We therefore show the nearest loading.js only if the route still has
|
|
340
|
+
// requests in flight after LOADING_DELAY, which avoids flashing a spinner
|
|
341
|
+
// on fast routes. Called *after* the page has mounted, because the page's
|
|
342
|
+
// effects are what start those requests.
|
|
343
|
+
|
|
344
|
+
_beginTransition(pathname, loadingComponent, pendingAtStart) {
|
|
345
|
+
const epoch = ++this._epoch;
|
|
346
|
+
this._finishTransition(this._epoch - 1);
|
|
347
|
+
this._showingLoading = false;
|
|
348
|
+
|
|
349
|
+
this._pendingAtStart = pendingAtStart;
|
|
350
|
+
if (!loadingComponent) return;
|
|
351
|
+
|
|
352
|
+
// NOTE: the page's own requests start in a microtask (useEffect queues
|
|
353
|
+
// with queueMicrotask), so they are NOT running yet at this point. We
|
|
354
|
+
// therefore always arm the timer and let it inspect reality at
|
|
355
|
+
// LOADING_DELAY rather than deciding synchronously.
|
|
356
|
+
this._transitionStop = onLoadState((pending) => {
|
|
357
|
+
if (epoch !== this._epoch || !this._showingLoading) return;
|
|
358
|
+
if (pending > this._pendingAtStart) return;
|
|
359
|
+
// Everything settled — put the real page back.
|
|
360
|
+
this._showingLoading = false;
|
|
361
|
+
this._finishTransition(epoch);
|
|
362
|
+
this._renderAt(pathname);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
this._transitionTimer = setTimeout(() => {
|
|
366
|
+
if (epoch !== this._epoch || this._showingLoading) return;
|
|
367
|
+
if (getPendingLoads() > this._pendingAtStart) {
|
|
368
|
+
this._showingLoading = true;
|
|
369
|
+
this._renderAt(pathname, { forceLoading: true });
|
|
370
|
+
}
|
|
371
|
+
}, LOADING_DELAY);
|
|
372
|
+
},
|
|
373
|
+
|
|
374
|
+
_finishTransition(epoch) {
|
|
375
|
+
if (epoch !== this._epoch) return;
|
|
376
|
+
clearTimeout(this._transitionTimer);
|
|
377
|
+
this._transitionTimer = null;
|
|
378
|
+
if (this._transitionStop) { this._transitionStop(); this._transitionStop = null; }
|
|
379
|
+
},
|
|
380
|
+
|
|
381
|
+
async render() {
|
|
382
|
+
return this._renderAt(normalizePath(window.location.pathname));
|
|
383
|
+
},
|
|
384
|
+
|
|
385
|
+
async _renderAt(pathname, { forceLoading = false } = {}) {
|
|
386
|
+
const root = document.getElementById('root');
|
|
387
|
+
if (!root) return;
|
|
388
|
+
|
|
389
|
+
const isNewRoute = this._lastPathname !== pathname;
|
|
390
|
+
if (isNewRoute) {
|
|
391
|
+
// Unmount previous page: run effect cleanups, drop state
|
|
392
|
+
disposeFibers();
|
|
393
|
+
this._lastPathname = pathname;
|
|
394
|
+
}
|
|
395
|
+
|
|
231
396
|
const scrollY = isNewRoute ? 0 : window.scrollY;
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
397
|
+
// Baseline before the page's effects kick off their requests.
|
|
398
|
+
const pendingAtStart = getPendingLoads();
|
|
399
|
+
|
|
400
|
+
const match = resolveRoute(routes, pathname);
|
|
401
|
+
this.currentParams = match ? match.params : {};
|
|
402
|
+
|
|
403
|
+
const layoutChain = collectLayouts(layouts, pathname);
|
|
404
|
+
const layoutModules = layoutChain.map(l => l.module);
|
|
405
|
+
const layoutComponents = layoutModules.map(m => m.default).filter(Boolean);
|
|
406
|
+
|
|
407
|
+
let pageModule = match ? match.value : notFoundModule;
|
|
408
|
+
let PageComponent = pageModule && pageModule.default;
|
|
409
|
+
|
|
410
|
+
if (!PageComponent) {
|
|
411
|
+
PageComponent = DefaultNotFound;
|
|
412
|
+
pageModule = null;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Show the route's loading.js instead of the page while data settles.
|
|
416
|
+
if (forceLoading) {
|
|
417
|
+
const loading = collectNearest(loadings, pathname);
|
|
418
|
+
const LoadingComponent = loading && loading.module.default;
|
|
419
|
+
if (LoadingComponent) {
|
|
420
|
+
const savedL = captureFocus();
|
|
421
|
+
mountPage(LoadingComponent, layoutComponents, root, 'loading:' + pathname);
|
|
422
|
+
restoreFocus(root, savedL);
|
|
423
|
+
if (isNewRoute) window.scrollTo(0, 0);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const boundary = collectNearest(errors, pathname);
|
|
429
|
+
const ErrorComponent = boundary && boundary.module.default;
|
|
430
|
+
const saved = captureFocus();
|
|
431
|
+
|
|
432
|
+
try {
|
|
433
|
+
mountPage(PageComponent, layoutComponents, root, 'page:' + pathname);
|
|
434
|
+
if (forceLoading) this._finishTransition(this._epoch);
|
|
435
|
+
} catch (err) {
|
|
436
|
+
// A page called notFound() — swap in the 404 screen.
|
|
437
|
+
if (err && err.message === NOT_FOUND_SIGNAL) {
|
|
438
|
+
disposeFibers();
|
|
439
|
+
const NotFound = (notFoundModule && notFoundModule.default) || DefaultNotFound;
|
|
440
|
+
mountPage(NotFound, layoutComponents, root, 'notfound:' + pathname);
|
|
441
|
+
applyMetadata(layoutModules, notFoundModule);
|
|
442
|
+
} else if (ErrorComponent) {
|
|
443
|
+
// A real render error — hand it to the nearest error boundary
|
|
444
|
+
// instead of white-screening the app. Next.js shape:
|
|
445
|
+
// error.js receives { error, reset }.
|
|
446
|
+
disposeFibers();
|
|
447
|
+
console.error('❌ Error while rendering', pathname, err);
|
|
448
|
+
mountPage(ErrorComponent, layoutComponents, root, 'error:' + pathname, {
|
|
449
|
+
error: err,
|
|
450
|
+
reset: () => { this._epoch++; this._showingLoading = false; this.render(); },
|
|
451
|
+
});
|
|
452
|
+
applyMetadata(layoutModules, null);
|
|
453
|
+
} else {
|
|
454
|
+
throw err;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
restoreFocus(root, saved);
|
|
459
|
+
|
|
460
|
+
if (isNewRoute) window.scrollTo(0, 0);
|
|
461
|
+
else if (scrollY) window.scrollTo(0, scrollY);
|
|
462
|
+
|
|
463
|
+
this._beginTransition(pathname, collectNearest(loadings, pathname), pendingAtStart);
|
|
464
|
+
},
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
export default router;
|