@netrojs/fnetro 0.2.20 → 0.2.21

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/client.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  // ─────────────────────────────────────────────────────────────────────────────
2
2
  // FNetro · client.ts
3
- // SolidJS hydration · SPA routing · client middleware · SEO sync · prefetch
3
+ // SolidJS hydration · @solidjs/router SPA routing · client middleware · SEO
4
4
  // ─────────────────────────────────────────────────────────────────────────────
5
5
 
6
- import { createSignal, createMemo, createComponent } from 'solid-js'
6
+ import { createSignal, createComponent, lazy, Suspense } from 'solid-js'
7
7
  import { hydrate } from 'solid-js/web'
8
+ import { Router, Route } from '@solidjs/router'
8
9
  import {
9
10
  resolveRoutes, compilePath, matchPath,
10
11
  SPA_HEADER, STATE_KEY, PARAMS_KEY, SEO_KEY,
@@ -30,20 +31,7 @@ function findRoute(pathname: string) {
30
31
  }
31
32
 
32
33
  // ══════════════════════════════════════════════════════════════════════════════
33
- // § 2 Navigation state signal
34
- // ══════════════════════════════════════════════════════════════════════════════
35
-
36
- interface NavState {
37
- path: string
38
- data: Record<string, unknown>
39
- params: Record<string, string>
40
- }
41
-
42
- // Populated by createAppRoot(); exposed so navigate() can update it.
43
- let _setNav: ((s: NavState) => void) | null = null
44
-
45
- // ══════════════════════════════════════════════════════════════════════════════
46
- // § 3 Client middleware
34
+ // § 2 Client middleware
47
35
  // ══════════════════════════════════════════════════════════════════════════════
48
36
 
49
37
  const _mw: ClientMiddleware[] = []
@@ -76,7 +64,7 @@ async function runMiddleware(url: string, done: () => Promise<void>): Promise<vo
76
64
  }
77
65
 
78
66
  // ══════════════════════════════════════════════════════════════════════════════
79
- // § 4 SEO — client-side <head> sync
67
+ // § 3 SEO — client-side <head> sync
80
68
  // ══════════════════════════════════════════════════════════════════════════════
81
69
 
82
70
  function setMeta(selector: string, attr: string, val: string | undefined): void {
@@ -91,7 +79,7 @@ function setMeta(selector: string, attr: string, val: string | undefined): void
91
79
  el.setAttribute(attr, val)
92
80
  }
93
81
 
94
- function syncSEO(seo: SEOMeta): void {
82
+ export function syncSEO(seo: SEOMeta): void {
95
83
  if (seo.title) document.title = seo.title
96
84
 
97
85
  setMeta('[name="description"]', 'content', seo.description)
@@ -124,7 +112,7 @@ function syncSEO(seo: SEOMeta): void {
124
112
  }
125
113
 
126
114
  // ══════════════════════════════════════════════════════════════════════════════
127
- // § 5 Prefetch cache
115
+ // § 4 Prefetch cache + SPA data fetching
128
116
  // ══════════════════════════════════════════════════════════════════════════════
129
117
 
130
118
  interface NavPayload {
@@ -136,7 +124,7 @@ interface NavPayload {
136
124
 
137
125
  const _cache = new Map<string, Promise<NavPayload>>()
138
126
 
139
- function fetchPayload(href: string): Promise<NavPayload> {
127
+ export function fetchPayload(href: string): Promise<NavPayload> {
140
128
  if (!_cache.has(href)) {
141
129
  _cache.set(
142
130
  href,
@@ -150,8 +138,81 @@ function fetchPayload(href: string): Promise<NavPayload> {
150
138
  return _cache.get(href)!
151
139
  }
152
140
 
141
+ /** Warm the prefetch cache for a URL on hover/focus/etc. */
142
+ export function prefetch(url: string): void {
143
+ try {
144
+ const u = new URL(url, location.origin)
145
+ if (u.origin !== location.origin || !findRoute(u.pathname)) return
146
+ fetchPayload(u.toString())
147
+ } catch { /* ignore invalid URLs */ }
148
+ }
149
+
150
+ // ══════════════════════════════════════════════════════════════════════════════
151
+ // § 5 Route components with data loading for @solidjs/router
152
+ // ══════════════════════════════════════════════════════════════════════════════
153
+
154
+ /**
155
+ * Creates a solid-router-compatible route component that:
156
+ * 1. On first render: uses server-injected state (no network request)
157
+ * 2. On SPA navigation: fetches data from the FNetro server handler
158
+ */
159
+ function makeRouteComponent(
160
+ route: ResolvedRoute,
161
+ appLayout: LayoutDef | undefined,
162
+ initialState: Record<string, unknown>,
163
+ initialParams: Record<string, string>,
164
+ initialSeo: SEOMeta,
165
+ prefetchOnHover: boolean,
166
+ ) {
167
+ // The component returned here is used as @solidjs/router's <Route component>
168
+ return function FNetroRouteComponent(routerProps: any) {
169
+ // routerProps.params comes from @solidjs/router's URL matching
170
+ const routeParams: Record<string, string> = routerProps.params ?? {}
171
+ const pathname: string = routerProps.location?.pathname ?? location.pathname
172
+
173
+ // Determine the data source:
174
+ // - If this matches the server's initial state key, use it directly (no fetch needed on first load)
175
+ // - Otherwise fetch from the server via the SPA JSON endpoint
176
+ const serverData = initialState[pathname] as Record<string, unknown> | undefined
177
+ const [data, setData] = createSignal<Record<string, unknown>>(serverData ?? {})
178
+ const [params, setParams] = createSignal<Record<string, string>>(serverData ? initialParams : routeParams)
179
+
180
+ // Load data if we don't have it yet from the server
181
+ if (!serverData) {
182
+ const url = new URL(pathname, location.origin).toString()
183
+ fetchPayload(url).then(payload => {
184
+ setData(payload.state ?? {})
185
+ setParams(payload.params ?? {})
186
+ syncSEO(payload.seo ?? {})
187
+ }).catch(err => {
188
+ console.error('[fnetro] Failed to load route data:', err)
189
+ })
190
+ } else {
191
+ // Sync SEO for the initial page from server-injected data
192
+ syncSEO(initialSeo)
193
+ }
194
+
195
+ // Render the page (and optional layout wrapper)
196
+ const layout = route.layout !== undefined ? route.layout : appLayout
197
+
198
+ const pageEl = () => createComponent(route.page.Page as any, {
199
+ ...data(),
200
+ url: pathname,
201
+ params: params(),
202
+ })
203
+
204
+ if (!layout) return pageEl()
205
+
206
+ return createComponent(layout.Component as any, {
207
+ url: pathname,
208
+ params: params(),
209
+ get children() { return pageEl() },
210
+ })
211
+ }
212
+ }
213
+
153
214
  // ══════════════════════════════════════════════════════════════════════════════
154
- // § 6 navigate / prefetch
215
+ // § 6 navigate / prefetch (convenience exports, wraps solid-router navigate)
155
216
  // ══════════════════════════════════════════════════════════════════════════════
156
217
 
157
218
  export interface NavigateOptions {
@@ -159,6 +220,10 @@ export interface NavigateOptions {
159
220
  scroll?: boolean
160
221
  }
161
222
 
223
+ /**
224
+ * Programmatic navigation — delegates to history API and triggers
225
+ * @solidjs/router's reactive location update.
226
+ */
162
227
  export async function navigate(to: string, opts: NavigateOptions = {}): Promise<void> {
163
228
  const u = new URL(to, location.origin)
164
229
  if (u.origin !== location.origin) { location.href = to; return }
@@ -166,14 +231,15 @@ export async function navigate(to: string, opts: NavigateOptions = {}): Promise<
166
231
 
167
232
  await runMiddleware(u.pathname, async () => {
168
233
  try {
234
+ // Prefetch/cache the payload so the route component can use it
169
235
  const payload = await fetchPayload(u.toString())
170
236
  history[opts.replace ? 'replaceState' : 'pushState'](
171
237
  { url: u.pathname }, '', u.pathname,
172
238
  )
173
239
  if (opts.scroll !== false) window.scrollTo(0, 0)
174
-
175
- _setNav?.({ path: u.pathname, data: payload.state ?? {}, params: payload.params ?? {} })
176
240
  syncSEO(payload.seo ?? {})
241
+ // Dispatch a popstate-like event so @solidjs/router's location signal updates
242
+ window.dispatchEvent(new PopStateEvent('popstate', { state: history.state }))
177
243
  } catch (err) {
178
244
  console.error('[fnetro] Navigation error:', err)
179
245
  location.href = to
@@ -181,79 +247,8 @@ export async function navigate(to: string, opts: NavigateOptions = {}): Promise<
181
247
  })
182
248
  }
183
249
 
184
- /** Warm the prefetch cache for a URL on hover/focus/etc. */
185
- export function prefetch(url: string): void {
186
- try {
187
- const u = new URL(url, location.origin)
188
- if (u.origin !== location.origin || !findRoute(u.pathname)) return
189
- fetchPayload(u.toString())
190
- } catch { /* ignore invalid URLs */ }
191
- }
192
-
193
- // ══════════════════════════════════════════════════════════════════════════════
194
- // § 7 DOM event intercepts
195
- // ══════════════════════════════════════════════════════════════════════════════
196
-
197
- function onLinkClick(e: MouseEvent): void {
198
- if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return
199
- const a = e.composedPath().find(
200
- (el): el is HTMLAnchorElement => el instanceof HTMLAnchorElement,
201
- )
202
- if (!a?.href) return
203
- if (a.target && a.target !== '_self') return
204
- if (a.hasAttribute('data-no-spa') || a.rel?.includes('external')) return
205
- const u = new URL(a.href)
206
- if (u.origin !== location.origin) return
207
- e.preventDefault()
208
- navigate(a.href)
209
- }
210
-
211
- function onLinkHover(e: MouseEvent): void {
212
- const a = e.composedPath().find(
213
- (el): el is HTMLAnchorElement => el instanceof HTMLAnchorElement,
214
- )
215
- if (a?.href) prefetch(a.href)
216
- }
217
-
218
- function onPopState(): void {
219
- navigate(location.href, { replace: true, scroll: false })
220
- }
221
-
222
- // ══════════════════════════════════════════════════════════════════════════════
223
- // § 8 App root component (created inside hydrate's reactive owner)
224
- // ══════════════════════════════════════════════════════════════════════════════
225
-
226
- function AppRoot(props: { initial: NavState; appLayout: LayoutDef | undefined }): any {
227
- const [nav, setNav] = createSignal<NavState>(props.initial)
228
- // Expose setter so navigate() can trigger re-renders
229
- _setNav = setNav
230
-
231
- const view = createMemo(() => {
232
- const { path, data, params } = nav()
233
- const m = findRoute(path)
234
-
235
- if (!m) {
236
- // No match client-side — shouldn't happen but handle gracefully
237
- return null as any
238
- }
239
-
240
- const layout = m.route.layout !== undefined ? m.route.layout : props.appLayout
241
- const pageEl = createComponent(m.route.page.Page as any, { ...data, url: path, params })
242
-
243
- if (!layout) return pageEl
244
-
245
- return createComponent(layout.Component as any, {
246
- url: path,
247
- params,
248
- get children() { return pageEl },
249
- })
250
- })
251
-
252
- return view
253
- }
254
-
255
250
  // ══════════════════════════════════════════════════════════════════════════════
256
- // § 9 boot()
251
+ // § 7 boot()
257
252
  // ══════════════════════════════════════════════════════════════════════════════
258
253
 
259
254
  export interface BootOptions extends AppConfig {
@@ -281,37 +276,50 @@ export async function boot(options: BootOptions): Promise<void> {
281
276
  const paramsMap = (window as any)[PARAMS_KEY] as Record<string, string> ?? {}
282
277
  const seoData = (window as any)[SEO_KEY] as SEOMeta ?? {}
283
278
 
284
- const initial: NavState = {
285
- path: pathname,
286
- data: stateMap[pathname] ?? {},
287
- params: paramsMap,
288
- }
289
-
290
279
  const container = document.getElementById('fnetro-app')
291
280
  if (!container) {
292
281
  console.error('[fnetro] #fnetro-app not found — aborting hydration')
293
282
  return
294
283
  }
295
284
 
296
- // Sync initial SEO (document.title etc.)
297
- syncSEO(seoData)
285
+ const prefetchOnHover = options.prefetchOnHover !== false
286
+
287
+ // Build @solidjs/router <Route> elements for each resolved page
288
+ const routeElements = pages.map(route =>
289
+ createComponent(Route, {
290
+ path: route.fullPath,
291
+ component: makeRouteComponent(
292
+ route,
293
+ _appLayout,
294
+ stateMap,
295
+ paramsMap,
296
+ seoData,
297
+ prefetchOnHover,
298
+ ),
299
+ }) as any
300
+ )
298
301
 
299
- // Hydrate the server-rendered HTML with SolidJS
302
+ // Hydrate with @solidjs/router wrapping all routes
300
303
  hydrate(
301
- () => createComponent(AppRoot as any, { initial, appLayout: _appLayout }) as any,
304
+ () => createComponent(Router as any, {
305
+ get children() { return routeElements },
306
+ }) as any,
302
307
  container,
303
308
  )
304
309
 
305
- // Wire up SPA navigation
306
- document.addEventListener('click', onLinkClick)
307
- if (options.prefetchOnHover !== false) {
308
- document.addEventListener('mouseover', onLinkHover)
310
+ // Hover prefetch
311
+ if (prefetchOnHover) {
312
+ document.addEventListener('mouseover', (e: MouseEvent) => {
313
+ const a = e.composedPath().find(
314
+ (el): el is HTMLAnchorElement => el instanceof HTMLAnchorElement,
315
+ )
316
+ if (a?.href) prefetch(a.href)
317
+ })
309
318
  }
310
- window.addEventListener('popstate', onPopState)
311
319
  }
312
320
 
313
321
  // ══════════════════════════════════════════════════════════════════════════════
314
- // § 10 Re-exports
322
+ // § 8 Re-exports
315
323
  // ══════════════════════════════════════════════════════════════════════════════
316
324
 
317
325
  export {
@@ -325,3 +333,6 @@ export type {
325
333
  PageProps, LayoutProps, SEOMeta, HonoMiddleware, LoaderCtx,
326
334
  ResolvedRoute, CompiledPath, ClientMiddleware,
327
335
  } from './core'
336
+
337
+ // Re-export solid-router primitives for convenience
338
+ export { useNavigate, useParams, useLocation, A, useSearchParams } from '@solidjs/router'
package/dist/client.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Hono, MiddlewareHandler, Context } from 'hono';
2
2
  import { Component, JSX } from 'solid-js';
3
+ export { A, useLocation, useNavigate, useParams, useSearchParams } from '@solidjs/router';
3
4
 
4
5
  type HonoMiddleware = MiddlewareHandler;
5
6
  type LoaderCtx = Context;
@@ -124,17 +125,29 @@ declare const SEO_KEY = "__FNETRO_SEO__";
124
125
  * })
125
126
  */
126
127
  declare function useClientMiddleware(mw: ClientMiddleware): void;
128
+ declare function syncSEO(seo: SEOMeta): void;
129
+ interface NavPayload {
130
+ state: Record<string, unknown>;
131
+ params: Record<string, string>;
132
+ seo: SEOMeta;
133
+ url: string;
134
+ }
135
+ declare function fetchPayload(href: string): Promise<NavPayload>;
136
+ /** Warm the prefetch cache for a URL on hover/focus/etc. */
137
+ declare function prefetch(url: string): void;
127
138
  interface NavigateOptions {
128
139
  replace?: boolean;
129
140
  scroll?: boolean;
130
141
  }
142
+ /**
143
+ * Programmatic navigation — delegates to history API and triggers
144
+ * @solidjs/router's reactive location update.
145
+ */
131
146
  declare function navigate(to: string, opts?: NavigateOptions): Promise<void>;
132
- /** Warm the prefetch cache for a URL on hover/focus/etc. */
133
- declare function prefetch(url: string): void;
134
147
  interface BootOptions extends AppConfig {
135
148
  /** Enable hover-based prefetching. @default true */
136
149
  prefetchOnHover?: boolean;
137
150
  }
138
151
  declare function boot(options: BootOptions): Promise<void>;
139
152
 
140
- export { type ApiRouteDef, type AppConfig, type BootOptions, type ClientMiddleware, type CompiledPath, type GroupDef, type HonoMiddleware, type LayoutDef, type LayoutProps, type LoaderCtx, type NavigateOptions, PARAMS_KEY, type PageDef, type PageProps, type ResolvedRoute, type Route, type SEOMeta, SEO_KEY, SPA_HEADER, STATE_KEY, boot, compilePath, defineApiRoute, defineGroup, defineLayout, definePage, matchPath, navigate, prefetch, resolveRoutes, useClientMiddleware };
153
+ export { type ApiRouteDef, type AppConfig, type BootOptions, type ClientMiddleware, type CompiledPath, type GroupDef, type HonoMiddleware, type LayoutDef, type LayoutProps, type LoaderCtx, type NavigateOptions, PARAMS_KEY, type PageDef, type PageProps, type ResolvedRoute, type Route, type SEOMeta, SEO_KEY, SPA_HEADER, STATE_KEY, boot, compilePath, defineApiRoute, defineGroup, defineLayout, definePage, fetchPayload, matchPath, navigate, prefetch, resolveRoutes, syncSEO, useClientMiddleware };
package/dist/client.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // client.ts
2
- import { createSignal, createMemo, createComponent } from "solid-js";
2
+ import { createSignal, createComponent } from "solid-js";
3
3
  import { hydrate } from "solid-js/web";
4
+ import { Router, Route } from "@solidjs/router";
4
5
 
5
6
  // core.ts
6
7
  function definePage(def) {
@@ -65,6 +66,7 @@ var PARAMS_KEY = "__FNETRO_PARAMS__";
65
66
  var SEO_KEY = "__FNETRO_SEO__";
66
67
 
67
68
  // client.ts
69
+ import { useNavigate, useParams, useLocation, A, useSearchParams } from "@solidjs/router";
68
70
  var _routes = [];
69
71
  var _appLayout;
70
72
  function findRoute(pathname) {
@@ -74,7 +76,6 @@ function findRoute(pathname) {
74
76
  }
75
77
  return null;
76
78
  }
77
- var _setNav = null;
78
79
  var _mw = [];
79
80
  function useClientMiddleware(mw) {
80
81
  _mw.push(mw);
@@ -146,6 +147,49 @@ function fetchPayload(href) {
146
147
  }
147
148
  return _cache.get(href);
148
149
  }
150
+ function prefetch(url) {
151
+ try {
152
+ const u = new URL(url, location.origin);
153
+ if (u.origin !== location.origin || !findRoute(u.pathname)) return;
154
+ fetchPayload(u.toString());
155
+ } catch {
156
+ }
157
+ }
158
+ function makeRouteComponent(route, appLayout, initialState, initialParams, initialSeo, prefetchOnHover) {
159
+ return function FNetroRouteComponent(routerProps) {
160
+ const routeParams = routerProps.params ?? {};
161
+ const pathname = routerProps.location?.pathname ?? location.pathname;
162
+ const serverData = initialState[pathname];
163
+ const [data, setData] = createSignal(serverData ?? {});
164
+ const [params, setParams] = createSignal(serverData ? initialParams : routeParams);
165
+ if (!serverData) {
166
+ const url = new URL(pathname, location.origin).toString();
167
+ fetchPayload(url).then((payload) => {
168
+ setData(payload.state ?? {});
169
+ setParams(payload.params ?? {});
170
+ syncSEO(payload.seo ?? {});
171
+ }).catch((err) => {
172
+ console.error("[fnetro] Failed to load route data:", err);
173
+ });
174
+ } else {
175
+ syncSEO(initialSeo);
176
+ }
177
+ const layout = route.layout !== void 0 ? route.layout : appLayout;
178
+ const pageEl = () => createComponent(route.page.Page, {
179
+ ...data(),
180
+ url: pathname,
181
+ params: params()
182
+ });
183
+ if (!layout) return pageEl();
184
+ return createComponent(layout.Component, {
185
+ url: pathname,
186
+ params: params(),
187
+ get children() {
188
+ return pageEl();
189
+ }
190
+ });
191
+ };
192
+ }
149
193
  async function navigate(to, opts = {}) {
150
194
  const u = new URL(to, location.origin);
151
195
  if (u.origin !== location.origin) {
@@ -165,66 +209,14 @@ async function navigate(to, opts = {}) {
165
209
  u.pathname
166
210
  );
167
211
  if (opts.scroll !== false) window.scrollTo(0, 0);
168
- _setNav?.({ path: u.pathname, data: payload.state ?? {}, params: payload.params ?? {} });
169
212
  syncSEO(payload.seo ?? {});
213
+ window.dispatchEvent(new PopStateEvent("popstate", { state: history.state }));
170
214
  } catch (err) {
171
215
  console.error("[fnetro] Navigation error:", err);
172
216
  location.href = to;
173
217
  }
174
218
  });
175
219
  }
176
- function prefetch(url) {
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
- }
183
- }
184
- function onLinkClick(e) {
185
- if (e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
186
- const a = e.composedPath().find(
187
- (el) => el instanceof HTMLAnchorElement
188
- );
189
- if (!a?.href) return;
190
- if (a.target && a.target !== "_self") return;
191
- if (a.hasAttribute("data-no-spa") || a.rel?.includes("external")) return;
192
- const u = new URL(a.href);
193
- if (u.origin !== location.origin) return;
194
- e.preventDefault();
195
- navigate(a.href);
196
- }
197
- function onLinkHover(e) {
198
- const a = e.composedPath().find(
199
- (el) => el instanceof HTMLAnchorElement
200
- );
201
- if (a?.href) prefetch(a.href);
202
- }
203
- function onPopState() {
204
- navigate(location.href, { replace: true, scroll: false });
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
- }
228
220
  async function boot(options) {
229
221
  const { pages } = resolveRoutes(options.routes, {
230
222
  layout: options.layout,
@@ -240,28 +232,44 @@ async function boot(options) {
240
232
  const stateMap = window[STATE_KEY] ?? {};
241
233
  const paramsMap = window[PARAMS_KEY] ?? {};
242
234
  const seoData = window[SEO_KEY] ?? {};
243
- const initial = {
244
- path: pathname,
245
- data: stateMap[pathname] ?? {},
246
- params: paramsMap
247
- };
248
235
  const container = document.getElementById("fnetro-app");
249
236
  if (!container) {
250
237
  console.error("[fnetro] #fnetro-app not found \u2014 aborting hydration");
251
238
  return;
252
239
  }
253
- syncSEO(seoData);
240
+ const prefetchOnHover = options.prefetchOnHover !== false;
241
+ const routeElements = pages.map(
242
+ (route) => createComponent(Route, {
243
+ path: route.fullPath,
244
+ component: makeRouteComponent(
245
+ route,
246
+ _appLayout,
247
+ stateMap,
248
+ paramsMap,
249
+ seoData,
250
+ prefetchOnHover
251
+ )
252
+ })
253
+ );
254
254
  hydrate(
255
- () => createComponent(AppRoot, { initial, appLayout: _appLayout }),
255
+ () => createComponent(Router, {
256
+ get children() {
257
+ return routeElements;
258
+ }
259
+ }),
256
260
  container
257
261
  );
258
- document.addEventListener("click", onLinkClick);
259
- if (options.prefetchOnHover !== false) {
260
- document.addEventListener("mouseover", onLinkHover);
262
+ if (prefetchOnHover) {
263
+ document.addEventListener("mouseover", (e) => {
264
+ const a = e.composedPath().find(
265
+ (el) => el instanceof HTMLAnchorElement
266
+ );
267
+ if (a?.href) prefetch(a.href);
268
+ });
261
269
  }
262
- window.addEventListener("popstate", onPopState);
263
270
  }
264
271
  export {
272
+ A,
265
273
  PARAMS_KEY,
266
274
  SEO_KEY,
267
275
  SPA_HEADER,
@@ -272,9 +280,15 @@ export {
272
280
  defineGroup,
273
281
  defineLayout,
274
282
  definePage,
283
+ fetchPayload,
275
284
  matchPath,
276
285
  navigate,
277
286
  prefetch,
278
287
  resolveRoutes,
279
- useClientMiddleware
288
+ syncSEO,
289
+ useClientMiddleware,
290
+ useLocation,
291
+ useNavigate,
292
+ useParams,
293
+ useSearchParams
280
294
  };
package/dist/server.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { Hono } from "hono";
3
3
  import { createComponent } from "solid-js";
4
4
  import { renderToStringAsync, generateHydrationScript } from "solid-js/web";
5
+ import { Router } from "@solidjs/router";
5
6
 
6
7
  // core.ts
7
8
  function definePage(def) {
@@ -179,13 +180,18 @@ async function renderPage(route, data, url, params, appLayout) {
179
180
  const layout = route.layout !== void 0 ? route.layout : appLayout;
180
181
  return renderToStringAsync(() => {
181
182
  const pageEl = createComponent(route.page.Page, { ...data, url, params });
182
- if (!layout) return pageEl;
183
- return createComponent(layout.Component, {
183
+ const content = layout ? createComponent(layout.Component, {
184
184
  url,
185
185
  params,
186
186
  get children() {
187
187
  return pageEl;
188
188
  }
189
+ }) : pageEl;
190
+ return createComponent(Router, {
191
+ url,
192
+ get children() {
193
+ return content;
194
+ }
189
195
  });
190
196
  });
191
197
  }
@@ -350,9 +356,10 @@ function fnetroVitePlugin(opts = {}) {
350
356
  name: "fnetro:jsx",
351
357
  enforce: "pre",
352
358
  // Sync config hook — must return Omit<UserConfig, 'plugins'> | null
353
- config(_cfg, _env) {
359
+ // Note: Vite 6+ deprecated `esbuild.jsx`; Vite 8 uses `oxc` instead.
360
+ config(_cfg, env) {
354
361
  return {
355
- esbuild: {
362
+ oxc: {
356
363
  jsx: "automatic",
357
364
  jsxImportSource: "solid-js"
358
365
  }
@@ -406,7 +413,7 @@ function fnetroVitePlugin(opts = {}) {
406
413
  format: "es",
407
414
  entryFileNames: "server.js"
408
415
  },
409
- external: (id) => NODE_BUILTINS.test(id) || id === "@hono/node-server" || id === "@hono/node-server/serve-static" || serverExternal.includes(id)
416
+ external: (id) => NODE_BUILTINS.test(id) || id === "@hono/node-server" || id === "@hono/node-server/serve-static" || id === "@solidjs/router" || id.startsWith("@solidjs/router/") || serverExternal.includes(id)
410
417
  }
411
418
  }
412
419
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@netrojs/fnetro",
3
- "version": "0.2.20",
3
+ "version": "0.2.21",
4
4
  "description": "Full-stack Hono framework — SolidJS SSR/SPA, SEO, middleware, route groups, API routes",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -74,6 +74,7 @@
74
74
  },
75
75
  "peerDependencies": {
76
76
  "solid-js": ">=1.9.11",
77
+ "@solidjs/router": ">=0.15.0",
77
78
  "hono": ">=4.0.0",
78
79
  "vite": ">=5.0.0",
79
80
  "vite-plugin-solid": ">=2.11.11"
@@ -88,6 +89,7 @@
88
89
  },
89
90
  "devDependencies": {
90
91
  "@hono/node-server": "^1.19.11",
92
+ "@solidjs/router": "^0.16.1",
91
93
  "@types/node": "^22.0.0",
92
94
  "hono": "^4.12.8",
93
95
  "rimraf": "^6.1.3",
package/server.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  import { Hono } from 'hono'
7
7
  import { createComponent } from 'solid-js'
8
8
  import { renderToStringAsync, generateHydrationScript } from 'solid-js/web'
9
+ import { Router } from '@solidjs/router'
9
10
  import {
10
11
  resolveRoutes, compilePath, matchPath,
11
12
  SPA_HEADER, STATE_KEY, PARAMS_KEY, SEO_KEY,
@@ -237,13 +238,20 @@ async function renderPage(
237
238
 
238
239
  return renderToStringAsync(() => {
239
240
  const pageEl = createComponent(route.page.Page as AnyComponent, { ...data, url, params })
240
- if (!layout) return pageEl as any
241
241
 
242
- return createComponent(layout.Component as AnyComponent, {
242
+ const content = layout
243
+ ? createComponent(layout.Component as AnyComponent, {
244
+ url,
245
+ params,
246
+ get children() { return pageEl },
247
+ })
248
+ : pageEl
249
+
250
+ // Wrap in Router so hydration keys match the client-side <Router>
251
+ return createComponent(Router as AnyComponent, {
243
252
  url,
244
- params,
245
- get children() { return pageEl },
246
- }) as any
253
+ get children() { return content },
254
+ })
247
255
  })
248
256
  }
249
257
 
@@ -256,7 +264,7 @@ async function renderFullPage(
256
264
  assets: ResolvedAssets,
257
265
  ): Promise<string> {
258
266
  const pageSEO = typeof route.page.seo === 'function'
259
- ? route.page.seo(data as any, params)
267
+ ? route.page.seo(data, params)
260
268
  : route.page.seo
261
269
  const seo = mergeSEO(config.seo, pageSEO)
262
270
  const title = seo.title ?? 'FNetro'
@@ -519,13 +527,15 @@ export function fnetroVitePlugin(opts: FNetroPluginOptions = {}): Plugin[] {
519
527
  enforce: 'pre',
520
528
 
521
529
  // Sync config hook — must return Omit<UserConfig, 'plugins'> | null
522
- config(_cfg: UserConfig, _env: ConfigEnv): Omit<UserConfig, 'plugins'> | null {
530
+ // Note: Vite 6+ deprecated `esbuild.jsx`; Vite 8 uses `oxc` instead.
531
+ config(_cfg: UserConfig, env: ConfigEnv): Omit<UserConfig, 'plugins'> | null {
532
+ // oxc is the new JSX transform pipeline in Vite 8+
523
533
  return {
524
- esbuild: {
534
+ oxc: {
525
535
  jsx: 'automatic',
526
536
  jsxImportSource: 'solid-js',
527
537
  },
528
- }
538
+ } as any
529
539
  },
530
540
 
531
541
  async buildStart() {
@@ -589,6 +599,8 @@ export function fnetroVitePlugin(opts: FNetroPluginOptions = {}): Plugin[] {
589
599
  NODE_BUILTINS.test(id) ||
590
600
  id === '@hono/node-server' ||
591
601
  id === '@hono/node-server/serve-static' ||
602
+ id === '@solidjs/router' ||
603
+ id.startsWith('@solidjs/router/') ||
592
604
  serverExternal.includes(id),
593
605
  },
594
606
  },