@netrojs/fnetro 0.1.6 → 0.2.1

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/dist/client.js CHANGED
@@ -1,419 +1,8 @@
1
1
  // client.ts
2
- import { render } from "hono/jsx/dom";
3
- import { jsx } from "hono/jsx";
4
- import {
5
- useState,
6
- useEffect,
7
- useMemo,
8
- useRef as useHonoRef,
9
- useSyncExternalStore
10
- } from "hono/jsx";
2
+ import { createSignal, createMemo, createComponent } from "solid-js";
3
+ import { hydrate } from "solid-js/web";
11
4
 
12
5
  // core.ts
13
- var RAW = /* @__PURE__ */ Symbol("raw");
14
- var IS_REACTIVE = /* @__PURE__ */ Symbol("isReactive");
15
- var IS_READONLY = /* @__PURE__ */ Symbol("isReadonly");
16
- var IS_REF = /* @__PURE__ */ Symbol("isRef");
17
- var MARK_RAW = /* @__PURE__ */ Symbol("markRaw");
18
- var targetMap = /* @__PURE__ */ new WeakMap();
19
- var activeEffect = null;
20
- var shouldTrack = true;
21
- var trackStack = [];
22
- function pauseTracking() {
23
- trackStack.push(shouldTrack);
24
- shouldTrack = false;
25
- }
26
- function resetTracking() {
27
- shouldTrack = trackStack.pop() ?? true;
28
- }
29
- function track(target, key) {
30
- if (!shouldTrack || !activeEffect) return;
31
- let depsMap = targetMap.get(target);
32
- if (!depsMap) targetMap.set(target, depsMap = /* @__PURE__ */ new Map());
33
- let dep = depsMap.get(key);
34
- if (!dep) depsMap.set(key, dep = /* @__PURE__ */ new Set());
35
- trackEffect(activeEffect, dep);
36
- }
37
- function trackEffect(effect2, dep) {
38
- if (!dep.has(effect2)) {
39
- dep.add(effect2);
40
- effect2.deps.push(dep);
41
- }
42
- }
43
- function trigger(target, key, newVal, oldVal) {
44
- const depsMap = targetMap.get(target);
45
- if (!depsMap) return;
46
- const effects = [];
47
- const computedEffects = [];
48
- depsMap.get(key)?.forEach((e) => {
49
- if (e !== activeEffect) {
50
- e.computed ? computedEffects.push(e) : effects.push(e);
51
- }
52
- });
53
- [...computedEffects, ...effects].forEach((e) => {
54
- if (e.active) e.scheduler ? e.scheduler() : e.run();
55
- });
56
- }
57
- var ReactiveEffect = class {
58
- constructor(fn, scheduler, scope) {
59
- this.fn = fn;
60
- this.scheduler = scheduler;
61
- this.scope = scope;
62
- scope?.effects.push(this);
63
- }
64
- deps = [];
65
- active = true;
66
- cleanup;
67
- computed = false;
68
- run() {
69
- if (!this.active) return this.fn();
70
- const prevEffect = activeEffect;
71
- const prevShouldTrack = shouldTrack;
72
- shouldTrack = true;
73
- activeEffect = this;
74
- this.cleanup?.();
75
- this.cleanup = void 0;
76
- this.deps.length = 0;
77
- try {
78
- const result = this.fn();
79
- if (typeof result === "function") this.cleanup = result;
80
- return result;
81
- } finally {
82
- activeEffect = prevEffect;
83
- shouldTrack = prevShouldTrack;
84
- }
85
- }
86
- stop() {
87
- if (this.active) {
88
- cleanupEffect(this);
89
- this.active = false;
90
- }
91
- }
92
- };
93
- function cleanupEffect(e) {
94
- e.deps.forEach((dep) => dep.delete(e));
95
- e.deps.length = 0;
96
- }
97
- var activeScope;
98
- var EffectScope = class {
99
- effects = [];
100
- cleanups = [];
101
- active = true;
102
- run(fn) {
103
- const prev = activeScope;
104
- activeScope = this;
105
- try {
106
- return fn();
107
- } finally {
108
- activeScope = prev;
109
- }
110
- }
111
- stop() {
112
- if (this.active) {
113
- this.effects.forEach((e) => e.stop());
114
- this.cleanups.forEach((fn) => fn());
115
- this.active = false;
116
- }
117
- }
118
- onCleanup(fn) {
119
- this.cleanups.push(fn);
120
- }
121
- };
122
- function effectScope() {
123
- return new EffectScope();
124
- }
125
- function effect(fn) {
126
- const e = new ReactiveEffect(fn, void 0, activeScope);
127
- e.run();
128
- return () => e.stop();
129
- }
130
- function watchEffect(fn, opts) {
131
- const e = new ReactiveEffect(fn, void 0, activeScope);
132
- e.run();
133
- return () => e.stop();
134
- }
135
- var refTarget = /* @__PURE__ */ Symbol("refTarget");
136
- var RefImpl = class {
137
- constructor(value, shallow = false) {
138
- this.shallow = shallow;
139
- this._value = shallow ? value : toReactive(value);
140
- }
141
- [IS_REF] = true;
142
- _value;
143
- _subscribers = /* @__PURE__ */ new Set();
144
- get value() {
145
- track(this, refTarget);
146
- this._subscribers.forEach((fn) => {
147
- });
148
- return this._value;
149
- }
150
- set value(next) {
151
- const newVal = this.shallow ? next : toReactive(next);
152
- if (!hasChanged(newVal, this._value)) return;
153
- this._value = newVal;
154
- trigger(this, refTarget, newVal, this._value);
155
- this._subscribers.forEach((fn) => fn());
156
- }
157
- /** Subscribe for useSyncExternalStore */
158
- subscribe(fn) {
159
- this._subscribers.add(fn);
160
- return () => this._subscribers.delete(fn);
161
- }
162
- peek() {
163
- return this._value;
164
- }
165
- };
166
- function ref(value) {
167
- return isRef(value) ? value : new RefImpl(value);
168
- }
169
- function shallowRef(value) {
170
- return new RefImpl(value, true);
171
- }
172
- function triggerRef(r) {
173
- if (r instanceof RefImpl) {
174
- trigger(r, refTarget);
175
- r._subscribers.forEach((fn) => fn());
176
- }
177
- }
178
- function isRef(r) {
179
- return !!r && typeof r === "object" && r[IS_REF] === true;
180
- }
181
- function unref(r) {
182
- return isRef(r) ? r.value : r;
183
- }
184
- function toRef(obj, key) {
185
- const r = new RefImpl(void 0, false);
186
- Object.defineProperty(r, "value", {
187
- get() {
188
- track(r, refTarget);
189
- return obj[key];
190
- },
191
- set(v) {
192
- obj[key] = v;
193
- trigger(r, refTarget, v, obj[key]);
194
- }
195
- });
196
- return r;
197
- }
198
- function toRefs(obj) {
199
- const result = {};
200
- for (const key in obj) result[key] = toRef(obj, key);
201
- return result;
202
- }
203
- var ComputedRefImpl = class {
204
- constructor(getter, setter) {
205
- this.setter = setter;
206
- this.effect = new ReactiveEffect(getter, () => {
207
- if (!this._dirty) {
208
- this._dirty = true;
209
- trigger(this, refTarget);
210
- this._subscribers.forEach((fn) => fn());
211
- }
212
- }, activeScope);
213
- this.effect.computed = true;
214
- }
215
- [IS_REF] = true;
216
- effect;
217
- _value;
218
- _dirty = true;
219
- _subscribers = /* @__PURE__ */ new Set();
220
- get value() {
221
- track(this, refTarget);
222
- if (this._dirty) {
223
- this._dirty = false;
224
- this._value = this.effect.run();
225
- }
226
- return this._value;
227
- }
228
- set value(v) {
229
- this.setter?.(v);
230
- }
231
- subscribe(fn) {
232
- this._subscribers.add(fn);
233
- return () => this._subscribers.delete(fn);
234
- }
235
- peek() {
236
- return this._value;
237
- }
238
- };
239
- function computed(arg) {
240
- if (typeof arg === "function") {
241
- return new ComputedRefImpl(arg);
242
- }
243
- return new ComputedRefImpl(arg.get, arg.set);
244
- }
245
- var reactiveMap = /* @__PURE__ */ new WeakMap();
246
- var readonlyMap = /* @__PURE__ */ new WeakMap();
247
- var shallowReactiveMap = /* @__PURE__ */ new WeakMap();
248
- function toReactive(value) {
249
- return value !== null && typeof value === "object" ? reactive(value) : value;
250
- }
251
- var arrayInstrumentations = {};
252
- ["includes", "indexOf", "lastIndexOf"].forEach((method) => {
253
- arrayInstrumentations[method] = function(...args) {
254
- const arr = toRaw(this);
255
- for (let i = 0; i < this.length; i++) track(arr, i);
256
- let res = arr[method](...args);
257
- if (res === -1 || res === false) res = arr[method](...args.map(toRaw));
258
- return res;
259
- };
260
- });
261
- ["push", "pop", "shift", "unshift", "splice"].forEach((method) => {
262
- arrayInstrumentations[method] = function(...args) {
263
- pauseTracking();
264
- const res = toRaw(this)[method].apply(this, args);
265
- resetTracking();
266
- return res;
267
- };
268
- });
269
- function createHandler(shallow = false, readonly2 = false) {
270
- return {
271
- get(target, key, receiver) {
272
- if (key === RAW) return target;
273
- if (key === IS_REACTIVE) return !readonly2;
274
- if (key === IS_READONLY) return readonly2;
275
- if (key === MARK_RAW) return target[MARK_RAW];
276
- const isArray = Array.isArray(target);
277
- if (!readonly2 && isArray && hasOwn(arrayInstrumentations, key)) {
278
- return Reflect.get(arrayInstrumentations, key, receiver);
279
- }
280
- const res = Reflect.get(target, key, receiver);
281
- if (typeof key === "symbol" || key === "__proto__") return res;
282
- if (!readonly2) track(target, key);
283
- if (shallow) return res;
284
- if (isRef(res)) return isArray ? res : res.value;
285
- return res !== null && typeof res === "object" && !res[MARK_RAW] ? readonly2 ? readonlyProxy(res) : reactive(res) : res;
286
- },
287
- set(target, key, value, receiver) {
288
- if (readonly2) {
289
- console.warn(`[fnetro] Cannot set "${String(key)}" on readonly object`);
290
- return true;
291
- }
292
- const oldVal = target[key];
293
- const result = Reflect.set(target, key, value, receiver);
294
- if (hasChanged(value, oldVal)) trigger(target, key, value, oldVal);
295
- return result;
296
- },
297
- deleteProperty(target, key) {
298
- if (readonly2) return true;
299
- const hadKey = hasOwn(target, key);
300
- const result = Reflect.deleteProperty(target, key);
301
- if (hadKey && result) trigger(target, key);
302
- return result;
303
- },
304
- has(target, key) {
305
- const res = Reflect.has(target, key);
306
- track(target, key);
307
- return res;
308
- },
309
- ownKeys(target) {
310
- track(target, Array.isArray(target) ? "length" : "__iterate__");
311
- return Reflect.ownKeys(target);
312
- }
313
- };
314
- }
315
- function reactive(target) {
316
- if (isReadonly(target)) return target;
317
- if (target[MARK_RAW]) return target;
318
- if (reactiveMap.has(target)) return reactiveMap.get(target);
319
- const proxy = new Proxy(target, createHandler());
320
- reactiveMap.set(target, proxy);
321
- return proxy;
322
- }
323
- function shallowReactive(target) {
324
- if (shallowReactiveMap.has(target)) return shallowReactiveMap.get(target);
325
- const proxy = new Proxy(target, createHandler(true));
326
- shallowReactiveMap.set(target, proxy);
327
- return proxy;
328
- }
329
- function readonlyProxy(target) {
330
- if (readonlyMap.has(target)) return readonlyMap.get(target);
331
- const proxy = new Proxy(target, createHandler(false, true));
332
- readonlyMap.set(target, proxy);
333
- return proxy;
334
- }
335
- function readonly(target) {
336
- return readonlyProxy(target);
337
- }
338
- function markRaw(value) {
339
- ;
340
- value[MARK_RAW] = true;
341
- return value;
342
- }
343
- function toRaw(observed) {
344
- const raw = observed?.[RAW];
345
- return raw ? toRaw(raw) : observed;
346
- }
347
- function isReactive(value) {
348
- if (isReadonly(value)) return isReactive(value[RAW]);
349
- return !!(value && value[IS_REACTIVE]);
350
- }
351
- function isReadonly(value) {
352
- return !!(value && value[IS_READONLY]);
353
- }
354
- function traverse(value, seen = /* @__PURE__ */ new Set()) {
355
- if (!value || typeof value !== "object" || seen.has(value)) return value;
356
- seen.add(value);
357
- if (isRef(value)) {
358
- traverse(value.value, seen);
359
- return value;
360
- }
361
- if (Array.isArray(value)) {
362
- value.forEach((v) => traverse(v, seen));
363
- return value;
364
- }
365
- for (const key in value) traverse(value[key], seen);
366
- return value;
367
- }
368
- function normalizeSource(src) {
369
- if (Array.isArray(src)) return () => src.map((s) => isRef(s) ? s.value : s());
370
- if (isRef(src)) return () => src.value;
371
- return src;
372
- }
373
- function watch(source, cb, opts = {}) {
374
- const getter = opts.deep ? () => traverse(normalizeSource(source)()) : normalizeSource(source);
375
- let oldVal = void 0;
376
- let cleanupFn;
377
- const cleanup = (fn) => {
378
- cleanupFn = fn;
379
- };
380
- const job = () => {
381
- if (!effect2.active) return;
382
- cleanupFn?.();
383
- cleanupFn = void 0;
384
- const newVal = effect2.run();
385
- if (opts.deep || hasChanged(newVal, oldVal)) {
386
- cb(newVal, oldVal, cleanup);
387
- oldVal = newVal;
388
- }
389
- if (opts.once) effect2.stop();
390
- };
391
- const effect2 = new ReactiveEffect(getter, job, activeScope);
392
- if (opts.immediate) {
393
- cleanupFn?.();
394
- cleanupFn = void 0;
395
- const val = effect2.run();
396
- cb(val, oldVal, cleanup);
397
- oldVal = val;
398
- } else {
399
- oldVal = effect2.run();
400
- }
401
- return () => effect2.stop();
402
- }
403
- var __hooks = {
404
- useValue: (r) => isRef(r) ? r.value : r(),
405
- useLocalRef: (init) => ref(init),
406
- useLocalReactive: (init) => reactive(init)
407
- };
408
- function use(source) {
409
- return __hooks.useValue(source);
410
- }
411
- function useLocalRef(init) {
412
- return __hooks.useLocalRef(init);
413
- }
414
- function useLocalReactive(init) {
415
- return __hooks.useLocalReactive(init);
416
- }
417
6
  function definePage(def) {
418
7
  return { __type: "page", ...def };
419
8
  }
@@ -423,9 +12,6 @@ function defineGroup(def) {
423
12
  function defineLayout(Component) {
424
13
  return { __type: "layout", Component };
425
14
  }
426
- function defineMiddleware(handler) {
427
- return { __type: "middleware", handler };
428
- }
429
15
  function defineApiRoute(path, register) {
430
16
  return { __type: "api", path, register };
431
17
  }
@@ -443,118 +29,122 @@ function resolveRoutes(routes, options = {}) {
443
29
  pages.push(...sub.pages);
444
30
  apis.push(...sub.apis);
445
31
  } else {
446
- const fullPath = (options.prefix ?? "") + route.path;
447
- const layout = route.layout !== void 0 ? route.layout : options.layout;
448
- const middleware = [...options.middleware ?? [], ...route.middleware ?? []];
449
- pages.push({ fullPath, page: route, layout, middleware });
32
+ pages.push({
33
+ fullPath: (options.prefix ?? "") + route.path,
34
+ page: route,
35
+ layout: route.layout !== void 0 ? route.layout : options.layout,
36
+ middleware: [...options.middleware ?? [], ...route.middleware ?? []]
37
+ });
450
38
  }
451
39
  }
452
40
  return { pages, apis };
453
41
  }
454
- var SPA_HEADER = "x-fnetro-spa";
455
- var STATE_KEY = "__FNETRO_STATE__";
456
- var PARAMS_KEY = "__FNETRO_PARAMS__";
457
- function hasChanged(a, b) {
458
- return !Object.is(a, b);
459
- }
460
- function hasOwn(obj, key) {
461
- return Object.prototype.hasOwnProperty.call(obj, key);
462
- }
463
-
464
- // client.ts
465
- function clientUseValue(source) {
466
- if (isRef(source)) {
467
- return useSyncExternalStore(
468
- (notify) => source.subscribe(notify),
469
- () => source.peek?.() ?? source.value
470
- );
471
- }
472
- const c = useMemo(() => computed(source), [source]);
473
- return useSyncExternalStore(
474
- (notify) => c.subscribe(notify),
475
- () => c.peek?.() ?? c.value
476
- );
477
- }
478
- function clientUseLocalRef(init) {
479
- const stableRef = useHonoRef(null);
480
- if (stableRef.current === null) stableRef.current = ref(init);
481
- const r = stableRef.current;
482
- useSyncExternalStore(
483
- (notify) => r.subscribe(notify),
484
- () => r.peek?.() ?? r.value
485
- );
486
- return r;
487
- }
488
- function clientUseLocalReactive(init) {
489
- const stableRef = useHonoRef(null);
490
- if (stableRef.current === null) stableRef.current = reactive(init);
491
- const proxy = stableRef.current;
492
- const [tick, setTick] = useState(0);
493
- useEffect(() => {
494
- return watchEffect(() => {
495
- JSON.stringify(proxy);
496
- setTick((t) => t + 1);
497
- });
498
- }, []);
499
- return proxy;
500
- }
501
- Object.assign(__hooks, {
502
- useValue: clientUseValue,
503
- useLocalRef: clientUseLocalRef,
504
- useLocalReactive: clientUseLocalReactive
505
- });
506
- function compileRoute(r) {
42
+ function compilePath(path) {
507
43
  const keys = [];
508
- const src = r.fullPath.replace(/\[\.\.\.([^\]]+)\]/g, (_, k) => {
44
+ const src = path.replace(/\[\.\.\.([^\]]+)\]/g, (_, k) => {
509
45
  keys.push(k);
510
46
  return "(.*)";
511
47
  }).replace(/\[([^\]]+)\]/g, (_, k) => {
512
48
  keys.push(k);
513
49
  return "([^/]+)";
514
50
  }).replace(/\*/g, "(.*)");
515
- return { route: r, re: new RegExp(`^${src}$`), keys };
51
+ return { re: new RegExp(`^${src}$`), keys };
52
+ }
53
+ function matchPath(compiled, pathname) {
54
+ const m = pathname.match(compiled.re);
55
+ if (!m) return null;
56
+ const params = {};
57
+ compiled.keys.forEach((k, i) => {
58
+ params[k] = decodeURIComponent(m[i + 1] ?? "");
59
+ });
60
+ return params;
516
61
  }
517
- function matchRoute(compiled2, pathname) {
518
- for (const c of compiled2) {
519
- const m = pathname.match(c.re);
520
- if (m) {
521
- const params = {};
522
- c.keys.forEach((k, i) => {
523
- params[k] = decodeURIComponent(m[i + 1]);
524
- });
525
- return { route: c.route, params };
526
- }
62
+ var SPA_HEADER = "x-fnetro-spa";
63
+ var STATE_KEY = "__FNETRO_STATE__";
64
+ var PARAMS_KEY = "__FNETRO_PARAMS__";
65
+ var SEO_KEY = "__FNETRO_SEO__";
66
+
67
+ // client.ts
68
+ var _routes = [];
69
+ var _appLayout;
70
+ function findRoute(pathname) {
71
+ for (const { route, cp } of _routes) {
72
+ const params = matchPath(cp, pathname);
73
+ if (params !== null) return { route, params };
527
74
  }
528
75
  return null;
529
76
  }
530
- var beforeNavListeners = [];
531
- var afterNavListeners = [];
532
- function onBeforeNavigate(fn) {
533
- beforeNavListeners.push(fn);
534
- return () => beforeNavListeners.splice(beforeNavListeners.indexOf(fn), 1);
535
- }
536
- function onAfterNavigate(fn) {
537
- afterNavListeners.push(fn);
538
- return () => afterNavListeners.splice(afterNavListeners.indexOf(fn), 1);
77
+ var _setNav = null;
78
+ var _mw = [];
79
+ function useClientMiddleware(mw) {
80
+ _mw.push(mw);
81
+ }
82
+ async function runMiddleware(url, done) {
83
+ const chain = [..._mw, async (_u, next) => {
84
+ await done();
85
+ await next();
86
+ }];
87
+ let i = 0;
88
+ const run = async () => {
89
+ const fn = chain[i++];
90
+ if (fn) await fn(url, run);
91
+ };
92
+ await run();
539
93
  }
540
- var compiled = [];
541
- var currentConfig;
542
- var currentLayout;
543
- var prefetchCache = /* @__PURE__ */ new Map();
544
- function fetchPage(url) {
545
- if (!prefetchCache.has(url)) {
546
- prefetchCache.set(url, fetch(url, {
547
- headers: { [SPA_HEADER]: "1" }
548
- }).then((r) => r.json()));
94
+ function setMeta(selector, attr, val) {
95
+ if (!val) {
96
+ document.querySelector(selector)?.remove();
97
+ return;
98
+ }
99
+ let el = document.querySelector(selector);
100
+ if (!el) {
101
+ el = document.createElement("meta");
102
+ const m = /\[(\w+[:-]?\w*)="([^"]+)"\]/.exec(selector);
103
+ if (m) el.setAttribute(m[1], m[2]);
104
+ document.head.appendChild(el);
105
+ }
106
+ el.setAttribute(attr, val);
107
+ }
108
+ function syncSEO(seo) {
109
+ if (seo.title) document.title = seo.title;
110
+ setMeta('[name="description"]', "content", seo.description);
111
+ setMeta('[name="keywords"]', "content", seo.keywords);
112
+ setMeta('[name="robots"]', "content", seo.robots);
113
+ setMeta('[name="theme-color"]', "content", seo.themeColor);
114
+ setMeta('[property="og:title"]', "content", seo.ogTitle);
115
+ setMeta('[property="og:description"]', "content", seo.ogDescription);
116
+ setMeta('[property="og:image"]', "content", seo.ogImage);
117
+ setMeta('[property="og:url"]', "content", seo.ogUrl);
118
+ setMeta('[property="og:type"]', "content", seo.ogType);
119
+ setMeta('[name="twitter:card"]', "content", seo.twitterCard);
120
+ setMeta('[name="twitter:title"]', "content", seo.twitterTitle);
121
+ setMeta('[name="twitter:description"]', "content", seo.twitterDescription);
122
+ setMeta('[name="twitter:image"]', "content", seo.twitterImage);
123
+ const canon = seo.canonical;
124
+ let linkEl = document.querySelector('link[rel="canonical"]');
125
+ if (canon) {
126
+ if (!linkEl) {
127
+ linkEl = document.createElement("link");
128
+ linkEl.rel = "canonical";
129
+ document.head.appendChild(linkEl);
130
+ }
131
+ linkEl.href = canon;
132
+ } else {
133
+ linkEl?.remove();
549
134
  }
550
- return prefetchCache.get(url);
551
135
  }
552
- async function renderPage(route, data, url, params) {
553
- const container = document.getElementById("fnetro-app");
554
- const pageNode = jsx(route.page.Page, { ...data, url, params });
555
- const layout = route.layout !== void 0 ? route.layout : currentLayout;
556
- const tree = layout ? jsx(layout.Component, { url, params, children: pageNode }) : pageNode;
557
- render(tree, container);
136
+ var _cache = /* @__PURE__ */ new Map();
137
+ function fetchPayload(href) {
138
+ if (!_cache.has(href)) {
139
+ _cache.set(
140
+ href,
141
+ fetch(href, { headers: { [SPA_HEADER]: "1" } }).then((r) => {
142
+ if (!r.ok) throw new Error(`${r.status} ${r.statusText}`);
143
+ return r.json();
144
+ })
145
+ );
146
+ }
147
+ return _cache.get(href);
558
148
  }
559
149
  async function navigate(to, opts = {}) {
560
150
  const u = new URL(to, location.origin);
@@ -562,35 +152,36 @@ async function navigate(to, opts = {}) {
562
152
  location.href = to;
563
153
  return;
564
154
  }
565
- for (const fn of beforeNavListeners) await fn(u.pathname);
566
- const match = matchRoute(compiled, u.pathname);
567
- if (!match) {
155
+ if (!findRoute(u.pathname)) {
568
156
  location.href = to;
569
157
  return;
570
158
  }
571
- try {
572
- const payload = await fetchPage(u.toString());
573
- const method = opts.replace ? "replaceState" : "pushState";
574
- history[method]({ url: u.pathname }, "", u.pathname);
575
- if (opts.scroll !== false) window.scrollTo(0, 0);
576
- await renderPage(match.route, payload.state ?? {}, u.pathname, payload.params ?? {});
577
- window[STATE_KEY] = {
578
- ...window[STATE_KEY],
579
- [u.pathname]: payload.state ?? {}
580
- };
581
- for (const fn of afterNavListeners) await fn(u.pathname);
582
- } catch (e) {
583
- console.error("[fnetro] Navigation failed:", e);
584
- location.href = to;
585
- }
159
+ await runMiddleware(u.pathname, async () => {
160
+ try {
161
+ const payload = await fetchPayload(u.toString());
162
+ history[opts.replace ? "replaceState" : "pushState"](
163
+ { url: u.pathname },
164
+ "",
165
+ u.pathname
166
+ );
167
+ if (opts.scroll !== false) window.scrollTo(0, 0);
168
+ _setNav?.({ path: u.pathname, data: payload.state ?? {}, params: payload.params ?? {} });
169
+ syncSEO(payload.seo ?? {});
170
+ } catch (err) {
171
+ console.error("[fnetro] Navigation error:", err);
172
+ location.href = to;
173
+ }
174
+ });
586
175
  }
587
176
  function prefetch(url) {
588
- const u = new URL(url, location.origin);
589
- if (u.origin !== location.origin) return;
590
- if (!matchRoute(compiled, u.pathname)) return;
591
- fetchPage(u.toString());
177
+ try {
178
+ const u = new URL(url, location.origin);
179
+ if (u.origin !== location.origin || !findRoute(u.pathname)) return;
180
+ fetchPayload(u.toString());
181
+ } catch {
182
+ }
592
183
  }
593
- function interceptClicks(e) {
184
+ function onLinkClick(e) {
594
185
  if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
595
186
  const a = e.composedPath().find(
596
187
  (el) => el instanceof HTMLAnchorElement
@@ -603,7 +194,7 @@ function interceptClicks(e) {
603
194
  e.preventDefault();
604
195
  navigate(a.href);
605
196
  }
606
- function interceptHover(e) {
197
+ function onLinkHover(e) {
607
198
  const a = e.composedPath().find(
608
199
  (el) => el instanceof HTMLAnchorElement
609
200
  );
@@ -612,62 +203,78 @@ function interceptHover(e) {
612
203
  function onPopState() {
613
204
  navigate(location.href, { replace: true, scroll: false });
614
205
  }
206
+ function AppRoot(props) {
207
+ const [nav, setNav] = createSignal(props.initial);
208
+ _setNav = setNav;
209
+ const view = createMemo(() => {
210
+ const { path, data, params } = nav();
211
+ const m = findRoute(path);
212
+ if (!m) {
213
+ return null;
214
+ }
215
+ const layout = m.route.layout !== void 0 ? m.route.layout : props.appLayout;
216
+ const pageEl = createComponent(m.route.page.Page, { ...data, url: path, params });
217
+ if (!layout) return pageEl;
218
+ return createComponent(layout.Component, {
219
+ url: path,
220
+ params,
221
+ get children() {
222
+ return pageEl;
223
+ }
224
+ });
225
+ });
226
+ return view;
227
+ }
615
228
  async function boot(options) {
616
229
  const { pages } = resolveRoutes(options.routes, {
617
230
  layout: options.layout,
618
231
  middleware: []
619
232
  });
620
- compiled = pages.map(compileRoute);
621
- currentConfig = options;
622
- currentLayout = options.layout;
233
+ _routes = pages.map((r) => ({ route: r, cp: compilePath(r.fullPath) }));
234
+ _appLayout = options.layout;
623
235
  const pathname = location.pathname;
624
- const match = matchRoute(compiled, pathname);
625
- if (!match) {
626
- console.warn(`[fnetro] No route matched "${pathname}" \u2014 not hydrating`);
236
+ if (!findRoute(pathname)) {
237
+ console.warn(`[fnetro] No route matched "${pathname}" \u2014 skipping hydration`);
627
238
  return;
628
239
  }
629
240
  const stateMap = window[STATE_KEY] ?? {};
630
241
  const paramsMap = window[PARAMS_KEY] ?? {};
631
- const data = stateMap[pathname] ?? {};
632
- await renderPage(match.route, data, pathname, paramsMap);
633
- document.addEventListener("click", interceptClicks);
242
+ const seoData = window[SEO_KEY] ?? {};
243
+ const initial = {
244
+ path: pathname,
245
+ data: stateMap[pathname] ?? {},
246
+ params: paramsMap
247
+ };
248
+ const container = document.getElementById("fnetro-app");
249
+ if (!container) {
250
+ console.error("[fnetro] #fnetro-app not found \u2014 aborting hydration");
251
+ return;
252
+ }
253
+ syncSEO(seoData);
254
+ hydrate(
255
+ () => createComponent(AppRoot, { initial, appLayout: _appLayout }),
256
+ container
257
+ );
258
+ document.addEventListener("click", onLinkClick);
634
259
  if (options.prefetchOnHover !== false) {
635
- document.addEventListener("mouseover", interceptHover);
260
+ document.addEventListener("mouseover", onLinkHover);
636
261
  }
637
262
  window.addEventListener("popstate", onPopState);
638
- for (const fn of afterNavListeners) await fn(pathname);
639
263
  }
640
264
  export {
265
+ PARAMS_KEY,
266
+ SEO_KEY,
267
+ SPA_HEADER,
268
+ STATE_KEY,
641
269
  boot,
642
- computed,
270
+ compilePath,
643
271
  defineApiRoute,
644
272
  defineGroup,
645
273
  defineLayout,
646
- defineMiddleware,
647
274
  definePage,
648
- effect,
649
- effectScope,
650
- isReactive,
651
- isReadonly,
652
- isRef,
653
- markRaw,
275
+ matchPath,
654
276
  navigate,
655
- onAfterNavigate,
656
- onBeforeNavigate,
657
277
  prefetch,
658
- reactive,
659
- readonly,
660
- ref,
661
- shallowReactive,
662
- shallowRef,
663
- toRaw,
664
- toRef,
665
- toRefs,
666
- triggerRef,
667
- unref,
668
- use,
669
- useLocalReactive,
670
- useLocalRef,
671
- watch,
672
- watchEffect
278
+ resolveRoutes,
279
+ useClientMiddleware
673
280
  };