@netrojs/fnetro 0.2.20 → 0.3.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/core.ts CHANGED
@@ -1,135 +1,41 @@
1
1
  // ─────────────────────────────────────────────────────────────────────────────
2
2
  // FNetro · core.ts
3
- // Shared types · route builders · path matching · SEO · constants
4
- // Reactivity: consumers use solid-js primitives directly
3
+ // Route builders · path matching · route resolution · async-loader detection
5
4
  // ─────────────────────────────────────────────────────────────────────────────
6
5
 
7
- import type { Context, MiddlewareHandler, Hono } from 'hono'
8
- import type { Component, JSX } from 'solid-js'
9
-
10
- // ══════════════════════════════════════════════════════════════════════════════
11
- // § 1 Primitive aliases
12
- // ══════════════════════════════════════════════════════════════════════════════
13
-
14
- export type HonoMiddleware = MiddlewareHandler
15
- export type LoaderCtx = Context
16
-
17
- // ══════════════════════════════════════════════════════════════════════════════
18
- // § 2 SEO / head metadata
19
- // ══════════════════════════════════════════════════════════════════════════════
20
-
21
- export interface SEOMeta {
22
- // Basic
23
- title?: string
24
- description?: string
25
- keywords?: string
26
- author?: string
27
- robots?: string
28
- canonical?: string
29
- themeColor?: string
30
- // Open Graph
31
- ogTitle?: string
32
- ogDescription?: string
33
- ogImage?: string
34
- ogImageAlt?: string
35
- ogImageWidth?: string
36
- ogImageHeight?: string
37
- ogUrl?: string
38
- ogType?: string
39
- ogSiteName?: string
40
- ogLocale?: string
41
- // Twitter / X
42
- twitterCard?: 'summary' | 'summary_large_image' | 'app' | 'player'
43
- twitterSite?: string
44
- twitterCreator?: string
45
- twitterTitle?: string
46
- twitterDescription?: string
47
- twitterImage?: string
48
- twitterImageAlt?: string
49
- // Structured data (JSON-LD)
50
- jsonLd?: Record<string, unknown> | Record<string, unknown>[]
51
- // Arbitrary extra <meta> tags
52
- extra?: Array<{ name?: string; property?: string; httpEquiv?: string; content: string }>
53
- }
54
-
55
- // ══════════════════════════════════════════════════════════════════════════════
56
- // § 3 Component prop shapes
57
- // ══════════════════════════════════════════════════════════════════════════════
58
-
59
- export type PageProps<TData extends object = {}> = TData & {
60
- url: string
61
- params: Record<string, string>
62
- }
63
-
64
- export interface LayoutProps {
65
- children: JSX.Element
66
- url: string
67
- params: Record<string, string>
68
- }
69
-
70
- // ══════════════════════════════════════════════════════════════════════════════
71
- // § 4 Route definitions
72
- // ══════════════════════════════════════════════════════════════════════════════
73
-
74
- export interface PageDef<TData extends object = {}> {
75
- readonly __type: 'page'
76
- path: string
77
- middleware?: HonoMiddleware[]
78
- loader?: (c: LoaderCtx) => TData | Promise<TData>
79
- seo?: SEOMeta | ((data: TData, params: Record<string, string>) => SEOMeta)
80
- layout?: LayoutDef | false
81
- Page: Component<PageProps<TData>>
82
- }
83
-
84
- export interface GroupDef {
85
- readonly __type: 'group'
86
- prefix: string
87
- layout?: LayoutDef | false
88
- middleware?: HonoMiddleware[]
89
- routes: Route[]
90
- }
91
-
92
- export interface LayoutDef {
93
- readonly __type: 'layout'
94
- Component: Component<LayoutProps>
95
- }
96
-
97
- export interface ApiRouteDef {
98
- readonly __type: 'api'
99
- path: string
100
- register: (app: Hono, globalMiddleware: HonoMiddleware[]) => void
101
- }
102
-
103
- export type Route = PageDef<any> | GroupDef | ApiRouteDef
104
-
105
- // ══════════════════════════════════════════════════════════════════════════════
106
- // § 5 App config
107
- // ══════════════════════════════════════════════════════════════════════════════
108
-
109
- export interface AppConfig {
110
- layout?: LayoutDef
111
- seo?: SEOMeta
112
- middleware?: HonoMiddleware[]
113
- routes: Route[]
114
- notFound?: Component
115
- htmlAttrs?: Record<string, string>
116
- head?: string
6
+ import type { Component } from 'vue'
7
+ import type {
8
+ PageDef, GroupDef, LayoutDef, ApiRouteDef, Route,
9
+ ResolvedRoute, CompiledPath, HonoMiddleware, AsyncLoader,
10
+ } from './types'
11
+
12
+ // ── Async-loader detection ────────────────────────────────────────────────────
13
+ //
14
+ // A Vue component (SFC compiled by vite-plugin-vue) always carries one or more
15
+ // of these brand properties. A plain () => import('./Page.vue') factory has
16
+ // none of them, so checking for their absence is sufficient for real-world use.
17
+
18
+ const VUE_BRANDS = ['__name', '__file', '__vccOpts', 'setup', 'render', 'data', 'components'] as const
19
+
20
+ /**
21
+ * Returns true when `c` is an async factory function (i.e. `() => import(...)`)
22
+ * rather than a resolved Vue component object.
23
+ *
24
+ * Used by both server.ts (to resolve the import before SSR) and client.ts
25
+ * (to wrap with defineAsyncComponent for lazy hydration).
26
+ */
27
+ export function isAsyncLoader(c: unknown): c is AsyncLoader {
28
+ if (typeof c !== 'function') return false
29
+ const f = c as unknown as Record<string, unknown>
30
+ for (const brand of VUE_BRANDS) {
31
+ if (brand in f) return false
32
+ }
33
+ return true
117
34
  }
118
35
 
119
- // ══════════════════════════════════════════════════════════════════════════════
120
- // § 6 Client middleware
121
- // ══════════════════════════════════════════════════════════════════════════════
122
-
123
- export type ClientMiddleware = (
124
- url: string,
125
- next: () => Promise<void>,
126
- ) => Promise<void>
36
+ // ── Builder functions ─────────────────────────────────────────────────────────
127
37
 
128
- // ══════════════════════════════════════════════════════════════════════════════
129
- // § 7 Builder functions
130
- // ══════════════════════════════════════════════════════════════════════════════
131
-
132
- export function definePage<TData extends object = {}>(
38
+ export function definePage<TData extends object = Record<string, never>>(
133
39
  def: Omit<PageDef<TData>, '__type'>,
134
40
  ): PageDef<TData> {
135
41
  return { __type: 'page', ...def }
@@ -139,8 +45,9 @@ export function defineGroup(def: Omit<GroupDef, '__type'>): GroupDef {
139
45
  return { __type: 'group', ...def }
140
46
  }
141
47
 
142
- export function defineLayout(Component: Component<LayoutProps>): LayoutDef {
143
- return { __type: 'layout', Component }
48
+ /** Wrap a Vue layout component (must render <slot />) as a FNetro layout. */
49
+ export function defineLayout(component: Component): LayoutDef {
50
+ return { __type: 'layout', component }
144
51
  }
145
52
 
146
53
  export function defineApiRoute(
@@ -150,17 +57,42 @@ export function defineApiRoute(
150
57
  return { __type: 'api', path, register }
151
58
  }
152
59
 
153
- // ══════════════════════════════════════════════════════════════════════════════
154
- // § 8 Internal route resolution
155
- // ══════════════════════════════════════════════════════════════════════════════
60
+ // ── Path matching (FNetro [param] syntax → RegExp) ────────────────────────────
61
+
62
+ export function compilePath(path: string): CompiledPath {
63
+ const keys: string[] = []
64
+ const src = path
65
+ .replace(/\[\.\.\.([^\]]+)\]/g, (_, k: string) => { keys.push(k); return '(.*)' })
66
+ .replace(/\[([^\]]+)\]/g, (_, k: string) => { keys.push(k); return '([^/]+)' })
67
+ .replace(/\*/g, '(.*)')
68
+ return { re: new RegExp(`^${src}$`), keys }
69
+ }
70
+
71
+ export function matchPath(
72
+ cp: CompiledPath,
73
+ pathname: string,
74
+ ): Record<string, string> | null {
75
+ const m = pathname.match(cp.re)
76
+ if (!m) return null
77
+ const params: Record<string, string> = {}
78
+ cp.keys.forEach((k, i) => { params[k] = decodeURIComponent(m[i + 1] ?? '') })
79
+ return params
80
+ }
156
81
 
157
- export interface ResolvedRoute {
158
- fullPath: string
159
- page: PageDef<any>
160
- layout: LayoutDef | false | undefined
161
- middleware: HonoMiddleware[]
82
+ /**
83
+ * Convert FNetro `[param]` syntax to Vue Router `:param` syntax.
84
+ *
85
+ * `/posts/[slug]` → `/posts/:slug`
86
+ * `/files/[...path]` → `/files/:path(.*)*`
87
+ */
88
+ export function toVueRouterPath(fnetroPath: string): string {
89
+ return fnetroPath
90
+ .replace(/\[\.\.\.([^\]]+)\]/g, ':$1(.*)*')
91
+ .replace(/\[([^\]]+)\]/g, ':$1')
162
92
  }
163
93
 
94
+ // ── Route resolution ──────────────────────────────────────────────────────────
95
+
164
96
  export function resolveRoutes(
165
97
  routes: Route[],
166
98
  options: {
@@ -179,7 +111,11 @@ export function resolveRoutes(
179
111
  const prefix = (options.prefix ?? '') + route.prefix
180
112
  const mw = [...(options.middleware ?? []), ...(route.middleware ?? [])]
181
113
  const layout = route.layout !== undefined ? route.layout : options.layout
182
- const sub = resolveRoutes(route.routes, { prefix, middleware: mw, layout })
114
+ const sub = resolveRoutes(route.routes, {
115
+ prefix,
116
+ middleware: mw,
117
+ ...(layout !== undefined && { layout }),
118
+ })
183
119
  pages.push(...sub.pages)
184
120
  apis.push(...sub.apis)
185
121
  } else {
@@ -195,42 +131,5 @@ export function resolveRoutes(
195
131
  return { pages, apis }
196
132
  }
197
133
 
198
- // ══════════════════════════════════════════════════════════════════════════════
199
- // § 9 Path matching (used by both server + client)
200
- // ══════════════════════════════════════════════════════════════════════════════
201
-
202
- export interface CompiledPath {
203
- re: RegExp
204
- keys: string[]
205
- }
206
-
207
- export function compilePath(path: string): CompiledPath {
208
- const keys: string[] = []
209
- const src = path
210
- .replace(/\[\.\.\.([^\]]+)\]/g, (_, k: string) => { keys.push(k); return '(.*)' })
211
- .replace(/\[([^\]]+)\]/g, (_, k: string) => { keys.push(k); return '([^/]+)' })
212
- .replace(/\*/g, '(.*)')
213
- return { re: new RegExp(`^${src}$`), keys }
214
- }
215
-
216
- export function matchPath(
217
- compiled: CompiledPath,
218
- pathname: string,
219
- ): Record<string, string> | null {
220
- const m = pathname.match(compiled.re)
221
- if (!m) return null
222
- const params: Record<string, string> = {}
223
- compiled.keys.forEach((k, i) => {
224
- params[k] = decodeURIComponent(m[i + 1] ?? '')
225
- })
226
- return params
227
- }
228
-
229
- // ══════════════════════════════════════════════════════════════════════════════
230
- // § 10 Shared constants
231
- // ══════════════════════════════════════════════════════════════════════════════
232
-
233
- export const SPA_HEADER = 'x-fnetro-spa'
234
- export const STATE_KEY = '__FNETRO_STATE__'
235
- export const PARAMS_KEY = '__FNETRO_PARAMS__'
236
- export const SEO_KEY = '__FNETRO_SEO__'
134
+ // Re-export all types so `import from '@netrojs/fnetro'` (root export) works
135
+ export * from './types'
package/dist/client.d.ts CHANGED
@@ -1,5 +1,6 @@
1
+ import { Component } from 'vue';
1
2
  import { Hono, MiddlewareHandler, Context } from 'hono';
2
- import { Component, JSX } from 'solid-js';
3
+ export { RouterLink, RouterView, useRoute, useRouter } from 'vue-router';
3
4
 
4
5
  type HonoMiddleware = MiddlewareHandler;
5
6
  type LoaderCtx = Context;
@@ -15,44 +16,33 @@ interface SEOMeta {
15
16
  ogDescription?: string;
16
17
  ogImage?: string;
17
18
  ogImageAlt?: string;
18
- ogImageWidth?: string;
19
- ogImageHeight?: string;
20
19
  ogUrl?: string;
21
20
  ogType?: string;
22
21
  ogSiteName?: string;
23
- ogLocale?: string;
24
22
  twitterCard?: 'summary' | 'summary_large_image' | 'app' | 'player';
25
23
  twitterSite?: string;
26
- twitterCreator?: string;
27
24
  twitterTitle?: string;
28
25
  twitterDescription?: string;
29
26
  twitterImage?: string;
30
- twitterImageAlt?: string;
27
+ /** Structured data injected as <script type="application/ld+json">. */
31
28
  jsonLd?: Record<string, unknown> | Record<string, unknown>[];
32
- extra?: Array<{
33
- name?: string;
34
- property?: string;
35
- httpEquiv?: string;
36
- content: string;
37
- }>;
38
29
  }
39
- type PageProps<TData extends object = {}> = TData & {
40
- url: string;
41
- params: Record<string, string>;
42
- };
43
- interface LayoutProps {
44
- children: JSX.Element;
45
- url: string;
46
- params: Record<string, string>;
47
- }
48
- interface PageDef<TData extends object = {}> {
30
+ type AsyncLoader = () => Promise<{
31
+ default: Component;
32
+ } | Component>;
33
+ interface PageDef<TData extends object = Record<string, never>> {
49
34
  readonly __type: 'page';
50
35
  path: string;
51
36
  middleware?: HonoMiddleware[];
52
37
  loader?: (c: LoaderCtx) => TData | Promise<TData>;
53
38
  seo?: SEOMeta | ((data: TData, params: Record<string, string>) => SEOMeta);
39
+ /** Override or disable the app-level layout for this route. */
54
40
  layout?: LayoutDef | false;
55
- Page: Component<PageProps<TData>>;
41
+ /**
42
+ * The Vue component to render for this route.
43
+ * Use () => import('./Page.vue') for automatic code splitting.
44
+ */
45
+ component: Component | AsyncLoader;
56
46
  }
57
47
  interface GroupDef {
58
48
  readonly __type: 'group';
@@ -63,7 +53,8 @@ interface GroupDef {
63
53
  }
64
54
  interface LayoutDef {
65
55
  readonly __type: 'layout';
66
- Component: Component<LayoutProps>;
56
+ /** Vue layout component — must contain <slot /> for page content. */
57
+ component: Component;
67
58
  }
68
59
  interface ApiRouteDef {
69
60
  readonly __type: 'api';
@@ -78,38 +69,67 @@ interface AppConfig {
78
69
  routes: Route[];
79
70
  notFound?: Component;
80
71
  htmlAttrs?: Record<string, string>;
72
+ /** Extra HTML injected into <head> (e.g. font preloads). */
81
73
  head?: string;
82
74
  }
83
- type ClientMiddleware = (url: string, next: () => Promise<void>) => Promise<void>;
84
- declare function definePage<TData extends object = {}>(def: Omit<PageDef<TData>, '__type'>): PageDef<TData>;
85
- declare function defineGroup(def: Omit<GroupDef, '__type'>): GroupDef;
86
- declare function defineLayout(Component: Component<LayoutProps>): LayoutDef;
87
- declare function defineApiRoute(path: string, register: ApiRouteDef['register']): ApiRouteDef;
88
75
  interface ResolvedRoute {
89
76
  fullPath: string;
90
77
  page: PageDef<any>;
91
78
  layout: LayoutDef | false | undefined;
92
79
  middleware: HonoMiddleware[];
93
80
  }
94
- declare function resolveRoutes(routes: Route[], options?: {
95
- prefix?: string;
96
- middleware?: HonoMiddleware[];
97
- layout?: LayoutDef | false;
98
- }): {
99
- pages: ResolvedRoute[];
100
- apis: ApiRouteDef[];
101
- };
102
81
  interface CompiledPath {
103
82
  re: RegExp;
104
83
  keys: string[];
105
84
  }
106
- declare function compilePath(path: string): CompiledPath;
107
- declare function matchPath(compiled: CompiledPath, pathname: string): Record<string, string> | null;
85
+ type ClientMiddleware = (url: string, next: () => Promise<void>) => Promise<void>;
86
+ /** Custom request header that identifies an SPA navigation (JSON payload). */
108
87
  declare const SPA_HEADER = "x-fnetro-spa";
88
+ /** window key for SSR-injected per-page loader data. */
109
89
  declare const STATE_KEY = "__FNETRO_STATE__";
90
+ /** window key for SSR-injected URL params. */
110
91
  declare const PARAMS_KEY = "__FNETRO_PARAMS__";
92
+ /** window key for SSR-injected SEO meta. */
111
93
  declare const SEO_KEY = "__FNETRO_SEO__";
94
+ /**
95
+ * Vue provide/inject key for the reactive page-data object.
96
+ * Symbol.for() ensures the same reference across module instances (SSR safe).
97
+ */
98
+ declare const DATA_KEY: unique symbol;
99
+
100
+ /**
101
+ * Returns true when `c` is an async factory function (i.e. `() => import(...)`)
102
+ * rather than a resolved Vue component object.
103
+ *
104
+ * Used by both server.ts (to resolve the import before SSR) and client.ts
105
+ * (to wrap with defineAsyncComponent for lazy hydration).
106
+ */
107
+ declare function isAsyncLoader(c: unknown): c is AsyncLoader;
108
+ declare function definePage<TData extends object = Record<string, never>>(def: Omit<PageDef<TData>, '__type'>): PageDef<TData>;
109
+ declare function defineGroup(def: Omit<GroupDef, '__type'>): GroupDef;
110
+ /** Wrap a Vue layout component (must render <slot />) as a FNetro layout. */
111
+ declare function defineLayout(component: Component): LayoutDef;
112
+ declare function defineApiRoute(path: string, register: ApiRouteDef['register']): ApiRouteDef;
113
+ declare function compilePath(path: string): CompiledPath;
114
+ declare function matchPath(cp: CompiledPath, pathname: string): Record<string, string> | null;
115
+ /**
116
+ * Convert FNetro `[param]` syntax to Vue Router `:param` syntax.
117
+ *
118
+ * `/posts/[slug]` → `/posts/:slug`
119
+ * `/files/[...path]` → `/files/:path(.*)*`
120
+ */
121
+ declare function toVueRouterPath(fnetroPath: string): string;
122
+ declare function resolveRoutes(routes: Route[], options?: {
123
+ prefix?: string;
124
+ middleware?: HonoMiddleware[];
125
+ layout?: LayoutDef | false;
126
+ }): {
127
+ pages: ResolvedRoute[];
128
+ apis: ApiRouteDef[];
129
+ };
112
130
 
131
+ declare function syncSEO(seo: SEOMeta): void;
132
+ declare function prefetch(url: string): void;
113
133
  /**
114
134
  * Register a client-side navigation middleware.
115
135
  * Must be called **before** `boot()`.
@@ -118,23 +138,25 @@ declare const SEO_KEY = "__FNETRO_SEO__";
118
138
  * useClientMiddleware(async (url, next) => {
119
139
  * if (!isLoggedIn() && url.startsWith('/dashboard')) {
120
140
  * await navigate('/login')
121
- * return // cancel original navigation
141
+ * return
122
142
  * }
123
143
  * await next()
124
144
  * })
125
145
  */
126
146
  declare function useClientMiddleware(mw: ClientMiddleware): void;
127
- interface NavigateOptions {
128
- replace?: boolean;
129
- scroll?: boolean;
130
- }
131
- 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;
147
+ /**
148
+ * Access the current page's loader data inside any Vue component.
149
+ * The returned object is reactive — it updates automatically on navigation.
150
+ *
151
+ * @example
152
+ * const data = usePageData<{ title: string; posts: Post[] }>()
153
+ * // data.title is typed and reactive
154
+ */
155
+ declare function usePageData<T extends Record<string, unknown> = Record<string, unknown>>(): T;
134
156
  interface BootOptions extends AppConfig {
135
- /** Enable hover-based prefetching. @default true */
157
+ /** Warm fetch cache on link hover. @default true */
136
158
  prefetchOnHover?: boolean;
137
159
  }
138
160
  declare function boot(options: BootOptions): Promise<void>;
139
161
 
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 };
162
+ export { type ApiRouteDef, type AppConfig, type AsyncLoader, type BootOptions, type ClientMiddleware, type CompiledPath, DATA_KEY, type GroupDef, type HonoMiddleware, type LayoutDef, type LoaderCtx, PARAMS_KEY, type PageDef, type ResolvedRoute, type Route, type SEOMeta, SEO_KEY, SPA_HEADER, STATE_KEY, boot, compilePath, defineApiRoute, defineGroup, defineLayout, definePage, isAsyncLoader, matchPath, prefetch, resolveRoutes, syncSEO, toVueRouterPath, useClientMiddleware, usePageData };