@epoch32/visage 0.0.0-stage → 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +373 -0
- package/README.md +110 -2
- package/dist/bundle.dev.js +4875 -0
- package/dist/index.js +9633 -0
- package/dist/src/api/index.js +4002 -0
- package/dist/src/charts/index.js +2717 -0
- package/dist/src/devtools/index.js +4677 -0
- package/dist/src/dom/index.js +1550 -0
- package/dist/src/form/index.js +1415 -0
- package/dist/src/index.js +11826 -0
- package/dist/src/jsx-runtime/index.js +1630 -0
- package/dist/src/query/index.js +2422 -0
- package/dist/src/router/index.js +1856 -0
- package/dist/src/rpc/index.js +3484 -0
- package/dist/src/server/index.js +1173 -0
- package/dist/src/signals/index.js +1077 -0
- package/dist/src/style/index.js +2340 -0
- package/dist/src/template/index.js +1149 -0
- package/dist/src/ui/index.js +4155 -0
- package/dist/src/virtual/index.js +1955 -0
- package/dist/src/vite/index.js +984 -0
- package/dist/vite/index.js +707 -0
- package/package.json +140 -4
- package/src/api/client.ts +371 -0
- package/src/api/file-router.ts +183 -0
- package/src/api/handler.ts +326 -0
- package/src/api/index.ts +71 -0
- package/src/api/middleware.ts +54 -0
- package/src/api/pragma.ts +155 -0
- package/src/api/procedure.ts +172 -0
- package/src/api/types.ts +249 -0
- package/src/charts/Area.tsx +111 -0
- package/src/charts/Axis.tsx +126 -0
- package/src/charts/Bar.tsx +86 -0
- package/src/charts/CartesianGrid.tsx +72 -0
- package/src/charts/Chart.tsx +269 -0
- package/src/charts/Cursor.tsx +42 -0
- package/src/charts/Legend.tsx +56 -0
- package/src/charts/Line.tsx +106 -0
- package/src/charts/Pie.tsx +88 -0
- package/src/charts/Tooltip.tsx +127 -0
- package/src/charts/context.ts +30 -0
- package/src/charts/index.ts +14 -0
- package/src/charts/path.ts +155 -0
- package/src/charts/scale.ts +133 -0
- package/src/charts/types.ts +50 -0
- package/src/devtools/DomTab.tsx +134 -0
- package/src/devtools/GraphInspector.tsx +196 -0
- package/src/devtools/GraphTab.tsx +315 -0
- package/src/devtools/QueryTab.tsx +200 -0
- package/src/devtools/RpcTab.tsx +190 -0
- package/src/devtools/SignalsTab.tsx +129 -0
- package/src/devtools/component.tsx +243 -0
- package/src/devtools/highlighter.ts +134 -0
- package/src/devtools/hub.ts +175 -0
- package/src/devtools/index.ts +20 -0
- package/src/devtools/styles.ts +313 -0
- package/src/devtools/types.ts +45 -0
- package/src/dom/async.ts +232 -0
- package/src/dom/control-flow.ts +353 -0
- package/src/dom/index.ts +32 -0
- package/src/dom/lazy.ts +82 -0
- package/src/dom/render.ts +286 -0
- package/src/dom/resource.ts +120 -0
- package/src/form/form.ts +218 -0
- package/src/form/index.ts +17 -0
- package/src/form/schema.ts +299 -0
- package/src/index.ts +413 -0
- package/src/jsx-dev-runtime/index.ts +1 -0
- package/src/jsx-runtime/index.ts +112 -0
- package/src/jsx-runtime/jsx.d.ts +278 -0
- package/src/query/create-mutation.ts +64 -0
- package/src/query/create-query.ts +426 -0
- package/src/query/focus-manager.ts +68 -0
- package/src/query/index.ts +63 -0
- package/src/query/mutation-cache.ts +138 -0
- package/src/query/online-manager.ts +67 -0
- package/src/query/query-cache.ts +369 -0
- package/src/query/query-client.ts +248 -0
- package/src/query/types.ts +221 -0
- package/src/query/utils.ts +132 -0
- package/src/router/index.ts +19 -0
- package/src/router/router.tsx +352 -0
- package/src/rpc/client.ts +317 -0
- package/src/rpc/error.ts +70 -0
- package/src/rpc/index.ts +52 -0
- package/src/rpc/procedure.ts +88 -0
- package/src/rpc/router.ts +105 -0
- package/src/rpc/server.ts +511 -0
- package/src/rpc/types.ts +328 -0
- package/src/server/hydrate.ts +21 -0
- package/src/server/index.ts +10 -0
- package/src/server/render-to-stream.ts +31 -0
- package/src/server/render-to-string.ts +125 -0
- package/src/signals/computed.ts +217 -0
- package/src/signals/context.ts +89 -0
- package/src/signals/effect.ts +161 -0
- package/src/signals/index.ts +57 -0
- package/src/signals/scope.ts +132 -0
- package/src/signals/signal.ts +203 -0
- package/src/signals/store.ts +163 -0
- package/src/signals/system.ts +124 -0
- package/src/signals/types.ts +64 -0
- package/src/style/component.tsx +122 -0
- package/src/style/create-style.ts +229 -0
- package/src/style/cx.ts +75 -0
- package/src/style/hash.ts +19 -0
- package/src/style/index.ts +8 -0
- package/src/style/layout.tsx +406 -0
- package/src/style/manager.ts +87 -0
- package/src/style/parser.ts +257 -0
- package/src/style/scale.ts +237 -0
- package/src/template/index.ts +4 -0
- package/src/template/template.ts +126 -0
- package/src/ui/accordion.tsx +286 -0
- package/src/ui/command-dialog.tsx +49 -0
- package/src/ui/command.tsx +400 -0
- package/src/ui/data-table.tsx +322 -0
- package/src/ui/dialog.tsx +362 -0
- package/src/ui/dropdown-menu.tsx +381 -0
- package/src/ui/icon.tsx +69 -0
- package/src/ui/index.ts +138 -0
- package/src/ui/layout.tsx +13 -0
- package/src/ui/popover.tsx +249 -0
- package/src/ui/switch.tsx +114 -0
- package/src/ui/tabs.tsx +297 -0
- package/src/ui/toast.tsx +280 -0
- package/src/ui/tooltip.tsx +262 -0
- package/src/ui/utils.ts +110 -0
- package/src/virtual/index.ts +12 -0
- package/src/virtual/virtual-list.tsx +185 -0
- package/src/virtual/virtualizer.ts +274 -0
- package/src/vite/hmr.ts +38 -0
- package/src/vite/index.ts +3 -0
- package/src/vite/lucide.ts +124 -0
- package/src/vite/plugin.ts +102 -0
- package/src/vite/rpc-middleware.ts +82 -0
- package/src/vite/types.ts +65 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { type Child, Show } from "../dom";
|
|
2
|
+
import {
|
|
3
|
+
computed,
|
|
4
|
+
createContext,
|
|
5
|
+
onCleanup,
|
|
6
|
+
type ReadonlySignal,
|
|
7
|
+
signal,
|
|
8
|
+
useContext,
|
|
9
|
+
type WritableSignal
|
|
10
|
+
} from "../signals";
|
|
11
|
+
|
|
12
|
+
export interface LocationState {
|
|
13
|
+
pathname: ReadonlySignal<string>;
|
|
14
|
+
search: ReadonlySignal<string>;
|
|
15
|
+
hash: ReadonlySignal<string>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface RouterContextValue {
|
|
19
|
+
location: LocationState;
|
|
20
|
+
navigate: (to: string, options?: { replace?: boolean }) => void;
|
|
21
|
+
params: ReadonlySignal<Record<string, string>>;
|
|
22
|
+
base: string;
|
|
23
|
+
matchedRoute: ReadonlySignal<MatchedRoute | null>;
|
|
24
|
+
routes: RouteDefinition[];
|
|
25
|
+
registerRoute: (route: RouteDefinition) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface MatchedRoute {
|
|
29
|
+
path: string;
|
|
30
|
+
params: Record<string, string>;
|
|
31
|
+
component?: (props?: any) => Child;
|
|
32
|
+
children?: Child;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface RouteDefinition {
|
|
36
|
+
path: string;
|
|
37
|
+
component?: (props?: any) => Child;
|
|
38
|
+
children?: Child;
|
|
39
|
+
matcher: RegExp;
|
|
40
|
+
paramNames: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const RouterContext = createContext<RouterContextValue | null>(null);
|
|
44
|
+
|
|
45
|
+
function compilePath(path: string): { matcher: RegExp; paramNames: string[] } {
|
|
46
|
+
const paramNames: string[] = [];
|
|
47
|
+
const normalized = path.replace(/\/+$/, "") || "/";
|
|
48
|
+
|
|
49
|
+
const regexStr = normalized
|
|
50
|
+
.replace(/:([a-zA-Z0-9_]+)/g, (_, name) => {
|
|
51
|
+
paramNames.push(name);
|
|
52
|
+
return "([^/]+)";
|
|
53
|
+
})
|
|
54
|
+
.replace(/\*/g, "(.*)");
|
|
55
|
+
|
|
56
|
+
const matcher = new RegExp(`^${regexStr}(?:/)?$`);
|
|
57
|
+
return { matcher, paramNames };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function matchRoute(routes: RouteDefinition[], pathname: string, base = ""): MatchedRoute | null {
|
|
61
|
+
const cleanPath =
|
|
62
|
+
base && pathname.startsWith(base) ? pathname.slice(base.length) || "/" : pathname;
|
|
63
|
+
|
|
64
|
+
for (const route of routes) {
|
|
65
|
+
const match = cleanPath.match(route.matcher);
|
|
66
|
+
if (match) {
|
|
67
|
+
const params: Record<string, string> = {};
|
|
68
|
+
const captures = match.slice(1);
|
|
69
|
+
route.paramNames.forEach((name, i) => {
|
|
70
|
+
params[name] = decodeURIComponent(captures[i] ?? "");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
path: route.path,
|
|
75
|
+
params,
|
|
76
|
+
component: route.component,
|
|
77
|
+
children: route.children
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface RouterProps {
|
|
86
|
+
base?: string;
|
|
87
|
+
mode?: "history" | "hash";
|
|
88
|
+
children: Child;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Root Router provider managing browser location state and URL synchronization
|
|
93
|
+
*/
|
|
94
|
+
export function Router(props: RouterProps): Child {
|
|
95
|
+
const base = props.base ?? "";
|
|
96
|
+
const mode = props.mode ?? "history";
|
|
97
|
+
|
|
98
|
+
const getPathname = () => {
|
|
99
|
+
if (typeof window === "undefined" || !window.location) return "/";
|
|
100
|
+
if (mode === "hash") {
|
|
101
|
+
const hash = window.location.hash.slice(1) || "/";
|
|
102
|
+
return hash.split("?")[0] || "/";
|
|
103
|
+
}
|
|
104
|
+
return window.location.pathname || "/";
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const getSearch = () => {
|
|
108
|
+
if (typeof window === "undefined" || !window.location) return "";
|
|
109
|
+
if (mode === "hash") {
|
|
110
|
+
const hash = window.location.hash.slice(1);
|
|
111
|
+
const queryIdx = hash.indexOf("?");
|
|
112
|
+
return queryIdx !== -1 ? hash.slice(queryIdx) : "";
|
|
113
|
+
}
|
|
114
|
+
return window.location.search || "";
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const getHash = () => {
|
|
118
|
+
if (typeof window === "undefined" || !window.location) return "";
|
|
119
|
+
return window.location.hash || "";
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const pathname: WritableSignal<string> = signal(getPathname());
|
|
123
|
+
const search: WritableSignal<string> = signal(getSearch());
|
|
124
|
+
const hash: WritableSignal<string> = signal(getHash());
|
|
125
|
+
|
|
126
|
+
const routes: RouteDefinition[] = [];
|
|
127
|
+
const registeredRoutes = signal<RouteDefinition[]>([]);
|
|
128
|
+
|
|
129
|
+
const registerRoute = (route: RouteDefinition) => {
|
|
130
|
+
routes.push(route);
|
|
131
|
+
registeredRoutes.set([...routes]);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const syncLocation = () => {
|
|
135
|
+
pathname.set(getPathname());
|
|
136
|
+
search.set(getSearch());
|
|
137
|
+
hash.set(getHash());
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
if (typeof window !== "undefined") {
|
|
141
|
+
const eventName = mode === "hash" ? "hashchange" : "popstate";
|
|
142
|
+
window.addEventListener(eventName, syncLocation);
|
|
143
|
+
onCleanup(() => window.removeEventListener(eventName, syncLocation));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const navigate = (to: string, options?: { replace?: boolean }) => {
|
|
147
|
+
if (typeof window === "undefined" || !window.history) {
|
|
148
|
+
pathname.set(to);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const target = base ? `${base.replace(/\/$/, "")}/${to.replace(/^\//, "")}` : to;
|
|
153
|
+
|
|
154
|
+
if (mode === "hash") {
|
|
155
|
+
if (options?.replace) {
|
|
156
|
+
window.location.replace(`#${to}`);
|
|
157
|
+
} else {
|
|
158
|
+
window.location.hash = `#${to}`;
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
if (options?.replace) {
|
|
162
|
+
window.history.replaceState(null, "", target);
|
|
163
|
+
} else {
|
|
164
|
+
window.history.pushState(null, "", target);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
syncLocation();
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const matchedRoute = computed(() => {
|
|
171
|
+
const currentPath = pathname();
|
|
172
|
+
const routeList = registeredRoutes();
|
|
173
|
+
return matchRoute(routeList, currentPath, base);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const params = computed(() => matchedRoute()?.params ?? {});
|
|
177
|
+
|
|
178
|
+
const routerValue: RouterContextValue = {
|
|
179
|
+
location: { pathname, search, hash },
|
|
180
|
+
navigate,
|
|
181
|
+
params,
|
|
182
|
+
base,
|
|
183
|
+
matchedRoute,
|
|
184
|
+
routes,
|
|
185
|
+
registerRoute
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
return RouterContext.Provider({
|
|
189
|
+
value: routerValue,
|
|
190
|
+
children: props.children
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export interface RouteProps {
|
|
195
|
+
path: string;
|
|
196
|
+
component?: (props: any) => Child;
|
|
197
|
+
children?: Child;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Route declaration registering URL path pattern and view component
|
|
202
|
+
*/
|
|
203
|
+
export function Route(props: RouteProps): Child {
|
|
204
|
+
const router = useContext(RouterContext);
|
|
205
|
+
if (!router) {
|
|
206
|
+
throw new Error("<Route> must be rendered inside a <Router>");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const { matcher, paramNames } = compilePath(props.path);
|
|
210
|
+
router.registerRoute({
|
|
211
|
+
path: props.path,
|
|
212
|
+
component: props.component,
|
|
213
|
+
children: props.children,
|
|
214
|
+
matcher,
|
|
215
|
+
paramNames
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export interface RoutesProps {
|
|
222
|
+
children: Child;
|
|
223
|
+
fallback?: Child;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Routes outlet matching the current URL to registered Route definitions
|
|
228
|
+
*/
|
|
229
|
+
export function Routes(props: RoutesProps): Child {
|
|
230
|
+
const router = useContext(RouterContext);
|
|
231
|
+
if (!router) {
|
|
232
|
+
throw new Error("<Routes> must be rendered inside a <Router>");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const _ =
|
|
236
|
+
typeof props.children === "function" ? (props.children as () => Child)() : props.children;
|
|
237
|
+
|
|
238
|
+
return Show({
|
|
239
|
+
when: () => router.matchedRoute() !== null,
|
|
240
|
+
fallback: props.fallback ?? (() => document.createTextNode("404 Not Found")),
|
|
241
|
+
children: () => {
|
|
242
|
+
const match = router.matchedRoute();
|
|
243
|
+
if (!match) return null;
|
|
244
|
+
|
|
245
|
+
if (match.component) {
|
|
246
|
+
return match.component({ params: match.params });
|
|
247
|
+
}
|
|
248
|
+
return match.children ?? null;
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export interface LinkProps {
|
|
254
|
+
href: string;
|
|
255
|
+
class?: string | (() => string);
|
|
256
|
+
activeClass?: string;
|
|
257
|
+
replace?: boolean;
|
|
258
|
+
children: Child;
|
|
259
|
+
[key: string]: any;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Accessible client-side navigation Link with active state highlighting
|
|
264
|
+
*/
|
|
265
|
+
export function Link(props: LinkProps): Child {
|
|
266
|
+
const router = useContext(RouterContext);
|
|
267
|
+
const { href, class: className, activeClass = "active", replace, children, ...rest } = props;
|
|
268
|
+
|
|
269
|
+
const handleClick = (e: MouseEvent) => {
|
|
270
|
+
if (e.ctrlKey || e.metaKey || e.altKey || e.shiftKey || e.button !== 0) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
e.preventDefault();
|
|
275
|
+
if (router) {
|
|
276
|
+
router.navigate(href, { replace });
|
|
277
|
+
} else if (typeof window !== "undefined") {
|
|
278
|
+
window.location.href = href;
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const isActive = computed(() => {
|
|
283
|
+
if (!router) return false;
|
|
284
|
+
const current = router.location.pathname();
|
|
285
|
+
return current === href || (href !== "/" && current.startsWith(href));
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const computedClass = computed(() => {
|
|
289
|
+
const baseCls = typeof className === "function" ? className() : (className ?? "");
|
|
290
|
+
if (activeClass && isActive()) {
|
|
291
|
+
return baseCls ? `${baseCls} ${activeClass}` : activeClass;
|
|
292
|
+
}
|
|
293
|
+
return baseCls;
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
return (
|
|
297
|
+
<a href={href} class={computedClass} onClick={handleClick} {...rest}>
|
|
298
|
+
{children}
|
|
299
|
+
</a>
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Access the active router location state signals
|
|
305
|
+
*/
|
|
306
|
+
export function useLocation(): LocationState {
|
|
307
|
+
const router = useContext(RouterContext);
|
|
308
|
+
if (!router) throw new Error("useLocation must be used within a <Router>");
|
|
309
|
+
return router.location;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Access the imperative navigate function
|
|
314
|
+
*/
|
|
315
|
+
export function useNavigate(): (to: string, options?: { replace?: boolean }) => void {
|
|
316
|
+
const router = useContext(RouterContext);
|
|
317
|
+
if (!router) throw new Error("useNavigate must be used within a <Router>");
|
|
318
|
+
return router.navigate;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Access reactive URL parameter signals
|
|
323
|
+
*/
|
|
324
|
+
export function useParams<T extends Record<string, string> = Record<string, string>>(): () => T {
|
|
325
|
+
const router = useContext(RouterContext);
|
|
326
|
+
if (!router) throw new Error("useParams must be used within a <Router>");
|
|
327
|
+
return () => router.params() as T;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Access and manipulate URL query search parameters reactively
|
|
332
|
+
*/
|
|
333
|
+
export function useSearchParams(): [
|
|
334
|
+
() => URLSearchParams,
|
|
335
|
+
(params: Record<string, string> | URLSearchParams) => void
|
|
336
|
+
] {
|
|
337
|
+
const router = useContext(RouterContext);
|
|
338
|
+
if (!router) throw new Error("useSearchParams must be used within a <Router>");
|
|
339
|
+
|
|
340
|
+
const params = computed(() => new URLSearchParams(router.location.search()));
|
|
341
|
+
|
|
342
|
+
const setSearchParams = (next: Record<string, string> | URLSearchParams) => {
|
|
343
|
+
const sp = next instanceof URLSearchParams ? next : new URLSearchParams(next);
|
|
344
|
+
const queryString = sp.toString();
|
|
345
|
+
const newUrl = `${router.location.pathname()}${queryString ? `?${queryString}` : ""}${router.location.hash()}`;
|
|
346
|
+
router.navigate(newUrl);
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
return [params, setSearchParams];
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export { lazy } from "../dom";
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { createMutation } from "../query/create-mutation";
|
|
2
|
+
import { createQuery } from "../query/create-query";
|
|
3
|
+
import { RPCError } from "./error";
|
|
4
|
+
import { matchesRulePattern } from "./server";
|
|
5
|
+
import type {
|
|
6
|
+
ClientOptions,
|
|
7
|
+
ClientRouter,
|
|
8
|
+
Router,
|
|
9
|
+
RPCResponse,
|
|
10
|
+
SubscriptionCallbacks,
|
|
11
|
+
SubscriptionHandle
|
|
12
|
+
} from "./types";
|
|
13
|
+
|
|
14
|
+
async function resolveHeaders(
|
|
15
|
+
headers?:
|
|
16
|
+
| Record<string, string>
|
|
17
|
+
| Record<string, any>
|
|
18
|
+
| (() => Promise<Record<string, string>> | Record<string, string>),
|
|
19
|
+
fullPath = ""
|
|
20
|
+
): Promise<Record<string, string>> {
|
|
21
|
+
if (!headers) return {};
|
|
22
|
+
if (typeof headers === "function") {
|
|
23
|
+
return await headers();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const result: Record<string, string> = {};
|
|
27
|
+
for (const [key, val] of Object.entries(headers)) {
|
|
28
|
+
if (typeof val === "object" || typeof val === "function") {
|
|
29
|
+
if (matchesRulePattern(key, fullPath, fullPath)) {
|
|
30
|
+
const resolved = typeof val === "function" ? await val() : val;
|
|
31
|
+
if (resolved && typeof resolved === "object") {
|
|
32
|
+
Object.assign(result, resolved);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
} else if (typeof val === "string") {
|
|
36
|
+
result[key] = val;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function createProxyPath(options: ClientOptions, path: string[] = []): any {
|
|
43
|
+
return new Proxy(
|
|
44
|
+
{},
|
|
45
|
+
{
|
|
46
|
+
get(_target, prop: string) {
|
|
47
|
+
if (prop === "then") return undefined;
|
|
48
|
+
|
|
49
|
+
if (prop === "query") {
|
|
50
|
+
return async (
|
|
51
|
+
input?: unknown,
|
|
52
|
+
callOpts?: { signal?: AbortSignal; headers?: Record<string, string> }
|
|
53
|
+
) => {
|
|
54
|
+
const fetchFn = options.fetch ?? globalThis.fetch;
|
|
55
|
+
const baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
56
|
+
const fullPath = path.join(".");
|
|
57
|
+
let url = `${baseUrl}/${fullPath}`;
|
|
58
|
+
|
|
59
|
+
if (input !== undefined) {
|
|
60
|
+
const serialized = JSON.stringify(input);
|
|
61
|
+
url += `${url.includes("?") ? "&" : "?"}input=${encodeURIComponent(serialized)}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const dynamicHeaders = await resolveHeaders(options.headers, fullPath);
|
|
65
|
+
const headers = {
|
|
66
|
+
Accept: "application/json",
|
|
67
|
+
...dynamicHeaders,
|
|
68
|
+
...callOpts?.headers
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const devtools =
|
|
72
|
+
typeof window !== "undefined" ? (window as any).__VISAGE_DEVTOOLS__ : null;
|
|
73
|
+
const logId = devtools?.logRpcCall({
|
|
74
|
+
id: `rpc_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
|
75
|
+
path: fullPath,
|
|
76
|
+
type: "query",
|
|
77
|
+
input
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const res = await fetchFn(url, {
|
|
82
|
+
method: "GET",
|
|
83
|
+
headers,
|
|
84
|
+
signal: callOpts?.signal
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const body: RPCResponse = await res.json();
|
|
88
|
+
if ("error" in body) {
|
|
89
|
+
devtools?.updateRpcCall(logId, { status: "error", error: body.error });
|
|
90
|
+
throw new RPCError({
|
|
91
|
+
code: body.error.code,
|
|
92
|
+
message: body.error.message,
|
|
93
|
+
issues: body.error.issues
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
devtools?.updateRpcCall(logId, { status: "success", response: body.result.data });
|
|
98
|
+
return body.result.data;
|
|
99
|
+
} catch (err) {
|
|
100
|
+
devtools?.updateRpcCall(logId, { status: "error", error: err });
|
|
101
|
+
throw err;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (prop === "mutate") {
|
|
107
|
+
return async (
|
|
108
|
+
input?: unknown,
|
|
109
|
+
callOpts?: { signal?: AbortSignal; headers?: Record<string, string> }
|
|
110
|
+
) => {
|
|
111
|
+
const fetchFn = options.fetch ?? globalThis.fetch;
|
|
112
|
+
const baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
113
|
+
const fullPath = path.join(".");
|
|
114
|
+
const url = `${baseUrl}/${fullPath}`;
|
|
115
|
+
|
|
116
|
+
const dynamicHeaders = await resolveHeaders(options.headers, fullPath);
|
|
117
|
+
const headers = {
|
|
118
|
+
"Content-Type": "application/json",
|
|
119
|
+
Accept: "application/json",
|
|
120
|
+
...dynamicHeaders,
|
|
121
|
+
...callOpts?.headers
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const devtools =
|
|
125
|
+
typeof window !== "undefined" ? (window as any).__VISAGE_DEVTOOLS__ : null;
|
|
126
|
+
const logId = devtools?.logRpcCall({
|
|
127
|
+
id: `rpc_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
|
128
|
+
path: fullPath,
|
|
129
|
+
type: "mutation",
|
|
130
|
+
input
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const res = await fetchFn(url, {
|
|
135
|
+
method: "POST",
|
|
136
|
+
headers,
|
|
137
|
+
body: JSON.stringify({ input }),
|
|
138
|
+
signal: callOpts?.signal
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const body: RPCResponse = await res.json();
|
|
142
|
+
if ("error" in body) {
|
|
143
|
+
devtools?.updateRpcCall(logId, { status: "error", error: body.error });
|
|
144
|
+
throw new RPCError({
|
|
145
|
+
code: body.error.code,
|
|
146
|
+
message: body.error.message,
|
|
147
|
+
issues: body.error.issues
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
devtools?.updateRpcCall(logId, { status: "success", response: body.result.data });
|
|
152
|
+
return body.result.data;
|
|
153
|
+
} catch (err) {
|
|
154
|
+
devtools?.updateRpcCall(logId, { status: "error", error: err });
|
|
155
|
+
throw err;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (prop === "subscribe") {
|
|
161
|
+
return (input: unknown, callbacks: SubscriptionCallbacks): SubscriptionHandle => {
|
|
162
|
+
const fetchFn = options.fetch ?? globalThis.fetch;
|
|
163
|
+
const baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
164
|
+
const fullPath = path.join(".");
|
|
165
|
+
let url = `${baseUrl}/${fullPath}`;
|
|
166
|
+
|
|
167
|
+
if (input !== undefined) {
|
|
168
|
+
const serialized = JSON.stringify(input);
|
|
169
|
+
url += `${url.includes("?") ? "&" : "?"}input=${encodeURIComponent(serialized)}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const controller = new AbortController();
|
|
173
|
+
const devtools =
|
|
174
|
+
typeof window !== "undefined" ? (window as any).__VISAGE_DEVTOOLS__ : null;
|
|
175
|
+
const logId = devtools?.logRpcCall({
|
|
176
|
+
id: `rpc_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
|
177
|
+
path: fullPath,
|
|
178
|
+
type: "subscription",
|
|
179
|
+
input
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
(async () => {
|
|
183
|
+
try {
|
|
184
|
+
const dynamicHeaders = await resolveHeaders(options.headers, fullPath);
|
|
185
|
+
const headers = {
|
|
186
|
+
Accept: "text/event-stream",
|
|
187
|
+
...dynamicHeaders
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const res = await fetchFn(url, {
|
|
191
|
+
method: "GET",
|
|
192
|
+
headers,
|
|
193
|
+
signal: controller.signal
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
if (!res.ok || !res.body) {
|
|
197
|
+
throw new Error(`Subscription connection failed with status ${res.status}`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const reader = res.body.getReader();
|
|
201
|
+
const decoder = new TextDecoder();
|
|
202
|
+
let buffer = "";
|
|
203
|
+
|
|
204
|
+
while (true) {
|
|
205
|
+
const { done, value } = await reader.read();
|
|
206
|
+
if (done) break;
|
|
207
|
+
|
|
208
|
+
buffer += decoder.decode(value, { stream: true });
|
|
209
|
+
const lines = buffer.split("\n\n");
|
|
210
|
+
buffer = lines.pop() ?? "";
|
|
211
|
+
|
|
212
|
+
for (const line of lines) {
|
|
213
|
+
const trimmed = line.trim();
|
|
214
|
+
if (trimmed.startsWith("data:")) {
|
|
215
|
+
const jsonStr = trimmed.slice(5).trim();
|
|
216
|
+
try {
|
|
217
|
+
const parsed = JSON.parse(jsonStr);
|
|
218
|
+
devtools?.logRpcEvent(logId, parsed);
|
|
219
|
+
callbacks.onData(parsed);
|
|
220
|
+
} catch (parseErr) {
|
|
221
|
+
devtools?.updateRpcCall(logId, { status: "error", error: parseErr });
|
|
222
|
+
callbacks.onError?.(parseErr);
|
|
223
|
+
}
|
|
224
|
+
} else if (trimmed.startsWith("event: error")) {
|
|
225
|
+
devtools?.updateRpcCall(logId, { status: "error" });
|
|
226
|
+
callbacks.onError?.(new Error("Server event stream error"));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
callbacks.onComplete?.();
|
|
232
|
+
} catch (err: any) {
|
|
233
|
+
if (!controller.signal.aborted) {
|
|
234
|
+
callbacks.onError?.(err);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
})();
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
unsubscribe: () => {
|
|
241
|
+
controller.abort();
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (prop === "queryKey") {
|
|
248
|
+
return (input?: unknown) => {
|
|
249
|
+
const resolvedInput = typeof input === "function" ? (input as () => unknown)() : input;
|
|
250
|
+
const key: unknown[] = [...path];
|
|
251
|
+
if (resolvedInput !== undefined) {
|
|
252
|
+
key.push(resolvedInput);
|
|
253
|
+
}
|
|
254
|
+
return key;
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (prop === "queryOptions") {
|
|
259
|
+
return (input?: unknown, extra?: Record<string, unknown>) => {
|
|
260
|
+
const resolvedInput = typeof input === "function" ? (input as () => unknown)() : input;
|
|
261
|
+
const key: unknown[] = [...path];
|
|
262
|
+
if (resolvedInput !== undefined) {
|
|
263
|
+
key.push(resolvedInput);
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
queryKey: key,
|
|
267
|
+
queryFn: ({ signal }: { signal: AbortSignal }) => {
|
|
268
|
+
const queryFn = createProxyPath(options, path).query;
|
|
269
|
+
return queryFn(resolvedInput, { signal });
|
|
270
|
+
},
|
|
271
|
+
...extra
|
|
272
|
+
};
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (prop === "createQuery" || prop === "useQuery") {
|
|
277
|
+
return (input?: unknown, extra?: Record<string, unknown>) => {
|
|
278
|
+
const proxy = createProxyPath(options, path);
|
|
279
|
+
return createQuery(() => proxy.queryOptions(input, extra));
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (prop === "mutationKey") {
|
|
284
|
+
return () => [...path];
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (prop === "mutationOptions") {
|
|
288
|
+
return (extra?: Record<string, unknown>) => {
|
|
289
|
+
return {
|
|
290
|
+
mutationKey: [...path],
|
|
291
|
+
mutationFn: (input: unknown) => {
|
|
292
|
+
const mutateFn = createProxyPath(options, path).mutate;
|
|
293
|
+
return mutateFn(input);
|
|
294
|
+
},
|
|
295
|
+
...extra
|
|
296
|
+
};
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (prop === "createMutation" || prop === "useMutation") {
|
|
301
|
+
return (extra?: Record<string, unknown>) => {
|
|
302
|
+
const proxy = createProxyPath(options, path);
|
|
303
|
+
return createMutation(proxy.mutationOptions(extra));
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return createProxyPath(options, [...path, prop]);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function createClient<TRouter extends Router<any, any>>(
|
|
314
|
+
options: ClientOptions
|
|
315
|
+
): ClientRouter<TRouter> {
|
|
316
|
+
return createProxyPath(options, []) as ClientRouter<TRouter>;
|
|
317
|
+
}
|