@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.
Files changed (138) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +110 -2
  3. package/dist/bundle.dev.js +4875 -0
  4. package/dist/index.js +9633 -0
  5. package/dist/src/api/index.js +4002 -0
  6. package/dist/src/charts/index.js +2717 -0
  7. package/dist/src/devtools/index.js +4677 -0
  8. package/dist/src/dom/index.js +1550 -0
  9. package/dist/src/form/index.js +1415 -0
  10. package/dist/src/index.js +11826 -0
  11. package/dist/src/jsx-runtime/index.js +1630 -0
  12. package/dist/src/query/index.js +2422 -0
  13. package/dist/src/router/index.js +1856 -0
  14. package/dist/src/rpc/index.js +3484 -0
  15. package/dist/src/server/index.js +1173 -0
  16. package/dist/src/signals/index.js +1077 -0
  17. package/dist/src/style/index.js +2340 -0
  18. package/dist/src/template/index.js +1149 -0
  19. package/dist/src/ui/index.js +4155 -0
  20. package/dist/src/virtual/index.js +1955 -0
  21. package/dist/src/vite/index.js +984 -0
  22. package/dist/vite/index.js +707 -0
  23. package/package.json +140 -4
  24. package/src/api/client.ts +371 -0
  25. package/src/api/file-router.ts +183 -0
  26. package/src/api/handler.ts +326 -0
  27. package/src/api/index.ts +71 -0
  28. package/src/api/middleware.ts +54 -0
  29. package/src/api/pragma.ts +155 -0
  30. package/src/api/procedure.ts +172 -0
  31. package/src/api/types.ts +249 -0
  32. package/src/charts/Area.tsx +111 -0
  33. package/src/charts/Axis.tsx +126 -0
  34. package/src/charts/Bar.tsx +86 -0
  35. package/src/charts/CartesianGrid.tsx +72 -0
  36. package/src/charts/Chart.tsx +269 -0
  37. package/src/charts/Cursor.tsx +42 -0
  38. package/src/charts/Legend.tsx +56 -0
  39. package/src/charts/Line.tsx +106 -0
  40. package/src/charts/Pie.tsx +88 -0
  41. package/src/charts/Tooltip.tsx +127 -0
  42. package/src/charts/context.ts +30 -0
  43. package/src/charts/index.ts +14 -0
  44. package/src/charts/path.ts +155 -0
  45. package/src/charts/scale.ts +133 -0
  46. package/src/charts/types.ts +50 -0
  47. package/src/devtools/DomTab.tsx +134 -0
  48. package/src/devtools/GraphInspector.tsx +196 -0
  49. package/src/devtools/GraphTab.tsx +315 -0
  50. package/src/devtools/QueryTab.tsx +200 -0
  51. package/src/devtools/RpcTab.tsx +190 -0
  52. package/src/devtools/SignalsTab.tsx +129 -0
  53. package/src/devtools/component.tsx +243 -0
  54. package/src/devtools/highlighter.ts +134 -0
  55. package/src/devtools/hub.ts +175 -0
  56. package/src/devtools/index.ts +20 -0
  57. package/src/devtools/styles.ts +313 -0
  58. package/src/devtools/types.ts +45 -0
  59. package/src/dom/async.ts +232 -0
  60. package/src/dom/control-flow.ts +353 -0
  61. package/src/dom/index.ts +32 -0
  62. package/src/dom/lazy.ts +82 -0
  63. package/src/dom/render.ts +286 -0
  64. package/src/dom/resource.ts +120 -0
  65. package/src/form/form.ts +218 -0
  66. package/src/form/index.ts +17 -0
  67. package/src/form/schema.ts +299 -0
  68. package/src/index.ts +413 -0
  69. package/src/jsx-dev-runtime/index.ts +1 -0
  70. package/src/jsx-runtime/index.ts +112 -0
  71. package/src/jsx-runtime/jsx.d.ts +278 -0
  72. package/src/query/create-mutation.ts +64 -0
  73. package/src/query/create-query.ts +426 -0
  74. package/src/query/focus-manager.ts +68 -0
  75. package/src/query/index.ts +63 -0
  76. package/src/query/mutation-cache.ts +138 -0
  77. package/src/query/online-manager.ts +67 -0
  78. package/src/query/query-cache.ts +369 -0
  79. package/src/query/query-client.ts +248 -0
  80. package/src/query/types.ts +221 -0
  81. package/src/query/utils.ts +132 -0
  82. package/src/router/index.ts +19 -0
  83. package/src/router/router.tsx +352 -0
  84. package/src/rpc/client.ts +317 -0
  85. package/src/rpc/error.ts +70 -0
  86. package/src/rpc/index.ts +52 -0
  87. package/src/rpc/procedure.ts +88 -0
  88. package/src/rpc/router.ts +105 -0
  89. package/src/rpc/server.ts +511 -0
  90. package/src/rpc/types.ts +328 -0
  91. package/src/server/hydrate.ts +21 -0
  92. package/src/server/index.ts +10 -0
  93. package/src/server/render-to-stream.ts +31 -0
  94. package/src/server/render-to-string.ts +125 -0
  95. package/src/signals/computed.ts +217 -0
  96. package/src/signals/context.ts +89 -0
  97. package/src/signals/effect.ts +161 -0
  98. package/src/signals/index.ts +57 -0
  99. package/src/signals/scope.ts +132 -0
  100. package/src/signals/signal.ts +203 -0
  101. package/src/signals/store.ts +163 -0
  102. package/src/signals/system.ts +124 -0
  103. package/src/signals/types.ts +64 -0
  104. package/src/style/component.tsx +122 -0
  105. package/src/style/create-style.ts +229 -0
  106. package/src/style/cx.ts +75 -0
  107. package/src/style/hash.ts +19 -0
  108. package/src/style/index.ts +8 -0
  109. package/src/style/layout.tsx +406 -0
  110. package/src/style/manager.ts +87 -0
  111. package/src/style/parser.ts +257 -0
  112. package/src/style/scale.ts +237 -0
  113. package/src/template/index.ts +4 -0
  114. package/src/template/template.ts +126 -0
  115. package/src/ui/accordion.tsx +286 -0
  116. package/src/ui/command-dialog.tsx +49 -0
  117. package/src/ui/command.tsx +400 -0
  118. package/src/ui/data-table.tsx +322 -0
  119. package/src/ui/dialog.tsx +362 -0
  120. package/src/ui/dropdown-menu.tsx +381 -0
  121. package/src/ui/icon.tsx +69 -0
  122. package/src/ui/index.ts +138 -0
  123. package/src/ui/layout.tsx +13 -0
  124. package/src/ui/popover.tsx +249 -0
  125. package/src/ui/switch.tsx +114 -0
  126. package/src/ui/tabs.tsx +297 -0
  127. package/src/ui/toast.tsx +280 -0
  128. package/src/ui/tooltip.tsx +262 -0
  129. package/src/ui/utils.ts +110 -0
  130. package/src/virtual/index.ts +12 -0
  131. package/src/virtual/virtual-list.tsx +185 -0
  132. package/src/virtual/virtualizer.ts +274 -0
  133. package/src/vite/hmr.ts +38 -0
  134. package/src/vite/index.ts +3 -0
  135. package/src/vite/lucide.ts +124 -0
  136. package/src/vite/plugin.ts +102 -0
  137. package/src/vite/rpc-middleware.ts +82 -0
  138. package/src/vite/types.ts +65 -0
package/package.json CHANGED
@@ -1,6 +1,142 @@
1
1
  {
2
2
  "name": "@epoch32/visage",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.0.0",
4
+ "description": "Lightning-fast, zero-VDOM, fine-grained reactive UI library with full TypeScript support",
5
+ "type": "module",
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./src/index.ts",
11
+ "import": "./src/index.ts"
12
+ },
13
+ "./signals": {
14
+ "types": "./src/signals/index.ts",
15
+ "import": "./src/signals/index.ts"
16
+ },
17
+ "./dom": {
18
+ "types": "./src/dom/index.ts",
19
+ "import": "./src/dom/index.ts"
20
+ },
21
+ "./router": {
22
+ "types": "./src/router/index.ts",
23
+ "import": "./src/router/index.ts"
24
+ },
25
+ "./template": {
26
+ "types": "./src/template/index.ts",
27
+ "import": "./src/template/index.ts"
28
+ },
29
+ "./server": {
30
+ "types": "./src/server/index.ts",
31
+ "import": "./src/server/index.ts"
32
+ },
33
+ "./form": {
34
+ "types": "./src/form/index.ts",
35
+ "import": "./src/form/index.ts"
36
+ },
37
+ "./virtual": {
38
+ "types": "./src/virtual/index.ts",
39
+ "import": "./src/virtual/index.ts"
40
+ },
41
+ "./query": {
42
+ "types": "./src/query/index.ts",
43
+ "import": "./src/query/index.ts"
44
+ },
45
+ "./rpc": {
46
+ "types": "./src/rpc/index.ts",
47
+ "import": "./src/rpc/index.ts"
48
+ },
49
+ "./api": {
50
+ "types": "./src/api/index.ts",
51
+ "import": "./src/api/index.ts"
52
+ },
53
+ "./style": {
54
+ "types": "./src/style/index.ts",
55
+ "import": "./src/style/index.ts"
56
+ },
57
+ "./devtools": {
58
+ "types": "./src/devtools/index.ts",
59
+ "import": "./src/devtools/index.ts"
60
+ },
61
+ "./vite": {
62
+ "types": "./src/vite/index.ts",
63
+ "import": "./src/vite/index.ts"
64
+ },
65
+ "./ui": {
66
+ "types": "./src/ui/index.ts",
67
+ "import": "./src/ui/index.ts"
68
+ },
69
+ "./charts": {
70
+ "types": "./src/charts/index.ts",
71
+ "import": "./src/charts/index.ts"
72
+ },
73
+ "./jsx-runtime": {
74
+ "types": "./src/jsx-runtime/index.ts",
75
+ "import": "./src/jsx-runtime/index.ts"
76
+ },
77
+ "./jsx-dev-runtime": {
78
+ "types": "./src/jsx-runtime/index.ts",
79
+ "import": "./src/jsx-runtime/index.ts"
80
+ }
81
+ },
82
+ "sideEffects": false,
83
+ "repository": {
84
+ "type": "git",
85
+ "url": "git+https://github.com/Epoch32/visage.git"
86
+ },
87
+ "bugs": {
88
+ "url": "https://github.com/Epoch32/visage/issues"
89
+ },
90
+ "homepage": "https://github.com/Epoch32/visage#readme",
91
+ "author": "Epoch32",
92
+ "publishConfig": {
93
+ "access": "public"
94
+ },
95
+ "files": [
96
+ "dist",
97
+ "src",
98
+ "README.md",
99
+ "LICENSE",
100
+ "package.json"
101
+ ],
102
+ "scripts": {
103
+ "build": "bun build src/index.ts src/signals/index.ts src/dom/index.ts src/template/index.ts src/api/index.ts src/rpc/index.ts src/query/index.ts src/style/index.ts src/devtools/index.ts src/ui/index.ts src/virtual/index.ts src/form/index.ts src/server/index.ts src/router/index.ts src/vite/index.ts src/jsx-runtime/index.ts src/charts/index.ts --outdir dist/",
104
+ "test": "bun test",
105
+ "test:watch": "bun test --watch",
106
+ "test:coverage": "bun test --coverage",
107
+ "test:vitest": "vitest run",
108
+ "typecheck": "bun x tsc --noEmit",
109
+ "check": "bun x @biomejs/biome check --write .",
110
+ "check:ci": "bun x @biomejs/biome check .",
111
+ "lint": "bun x @biomejs/biome lint .",
112
+ "format": "bun x @biomejs/biome format --write .",
113
+ "verify": "bun test && bun run build",
114
+ "prepublishOnly": "bun run verify",
115
+ "example:survey": "bun examples/survey/server.ts",
116
+ "example:ui": "vite examples/ui-showcase",
117
+ "example:charts": "vite examples/charts-showcase",
118
+ "example:auth": "vite examples/enterprise-auth",
119
+ "example:api": "bun examples/api-showcase/server.ts"
120
+ },
121
+ "devDependencies": {
122
+ "@biomejs/biome": "^2.5.15",
123
+ "csstype": "^3.2.3",
124
+ "lucide": "^1.54.0",
125
+ "typescript": "^7.0.2",
126
+ "vitest": "^5.0.3"
127
+ },
128
+ "keywords": [
129
+ "reactive",
130
+ "signals",
131
+ "no-vdom",
132
+ "jsx",
133
+ "ssr",
134
+ "form",
135
+ "validation",
136
+ "router",
137
+ "template",
138
+ "typescript",
139
+ "ui"
140
+ ],
141
+ "license": "MPL-2.0"
142
+ }
@@ -0,0 +1,371 @@
1
+ /**
2
+ * Visage File-Based API Typed Client SDK (visage/api)
3
+ */
4
+ import { createMutation } from "../query/create-mutation";
5
+ import { createQuery } from "../query/create-query";
6
+ import { RPCError } from "../rpc/error";
7
+ import { matchesRulePattern } from "../rpc/server";
8
+ import type {
9
+ ApiCallOptions,
10
+ ApiClient,
11
+ ApiClientOptions,
12
+ ApiSubscriptionCallbacks,
13
+ ApiSubscriptionHandle
14
+ } from "./types";
15
+
16
+ async function resolveHeaders(
17
+ headers?: ApiClientOptions["headers"],
18
+ fullPath = ""
19
+ ): Promise<Record<string, string>> {
20
+ if (!headers) return {};
21
+ if (typeof headers === "function") {
22
+ return await headers();
23
+ }
24
+
25
+ const result: Record<string, string> = {};
26
+ for (const [key, val] of Object.entries(headers)) {
27
+ if (typeof val === "object" || typeof val === "function") {
28
+ if (matchesRulePattern(key, fullPath, fullPath)) {
29
+ const resolved = typeof val === "function" ? await val() : val;
30
+ if (resolved && typeof resolved === "object") {
31
+ Object.assign(result, resolved);
32
+ }
33
+ }
34
+ } else if (typeof val === "string") {
35
+ result[key] = val;
36
+ }
37
+ }
38
+ return result;
39
+ }
40
+
41
+ function buildUrl(baseUrl: string, pathSegments: string[], queryParams?: unknown): string {
42
+ const cleanBase = baseUrl.replace(/\/+$/, "");
43
+ const cleanPath = pathSegments.filter(Boolean).join("/");
44
+ let url = cleanPath ? `${cleanBase}/${cleanPath}` : cleanBase;
45
+
46
+ if (queryParams !== undefined && queryParams !== null) {
47
+ if (typeof queryParams === "object") {
48
+ const params = new URLSearchParams();
49
+ for (const [k, v] of Object.entries(queryParams as Record<string, any>)) {
50
+ if (v !== undefined && v !== null) {
51
+ if (typeof v === "object") {
52
+ params.set(k, JSON.stringify(v));
53
+ } else {
54
+ params.set(k, String(v));
55
+ }
56
+ }
57
+ }
58
+ const qs = params.toString();
59
+ if (qs) {
60
+ url += `${url.includes("?") ? "&" : "?"}${qs}`;
61
+ }
62
+ } else {
63
+ url += `${url.includes("?") ? "&" : "?"}input=${encodeURIComponent(String(queryParams))}`;
64
+ }
65
+ }
66
+
67
+ return url;
68
+ }
69
+
70
+ function createApiProxy(options: ApiClientOptions, path: string[] = []): any {
71
+ const handlerFn = (...args: any[]) => {
72
+ if (args.length > 0) {
73
+ return createApiProxy(options, [...path, String(args[0])]);
74
+ }
75
+ return createApiProxy(options, path);
76
+ };
77
+
78
+ return new Proxy(handlerFn, {
79
+ get(_target, prop: string) {
80
+ if (prop === "then") return undefined;
81
+
82
+ const fullPath = path.join("/");
83
+
84
+ // 1. GET Method
85
+ if (prop === "get") {
86
+ return async (input?: unknown, callOpts?: ApiCallOptions) => {
87
+ const fetchFn = options.fetch ?? globalThis.fetch;
88
+ const url = buildUrl(options.baseUrl, path, input);
89
+ const dynamicHeaders = await resolveHeaders(options.headers, fullPath);
90
+ const headers = {
91
+ Accept: "application/json",
92
+ ...dynamicHeaders,
93
+ ...callOpts?.headers
94
+ };
95
+
96
+ const devtools =
97
+ typeof window !== "undefined" ? (window as any).__VISAGE_DEVTOOLS__ : null;
98
+ const logId = devtools?.logRpcCall({
99
+ id: `api_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
100
+ path: fullPath,
101
+ type: "query",
102
+ input
103
+ });
104
+
105
+ try {
106
+ const res = await fetchFn(url, {
107
+ method: "GET",
108
+ headers,
109
+ signal: callOpts?.signal
110
+ });
111
+
112
+ const contentType = res.headers.get("Content-Type") || "";
113
+ if (contentType.includes("application/json")) {
114
+ const body = await res.json();
115
+ if (body && typeof body === "object" && "ok" in body) {
116
+ if (!body.ok) {
117
+ devtools?.updateRpcCall(logId, { status: "error", error: body.error });
118
+ throw new RPCError({
119
+ code: body.error?.code || "INTERNAL_SERVER_ERROR",
120
+ message: body.error?.message || "Request failed",
121
+ issues: body.error?.issues
122
+ });
123
+ }
124
+ devtools?.updateRpcCall(logId, { status: "success", response: body.data });
125
+ return body.data;
126
+ }
127
+ if (!res.ok) {
128
+ throw new Error(`Request failed with status ${res.status}`);
129
+ }
130
+ devtools?.updateRpcCall(logId, { status: "success", response: body });
131
+ return body;
132
+ }
133
+
134
+ if (!res.ok) {
135
+ throw new Error(`Request failed with status ${res.status}`);
136
+ }
137
+ const text = await res.text();
138
+ devtools?.updateRpcCall(logId, { status: "success", response: text });
139
+ return text;
140
+ } catch (err) {
141
+ devtools?.updateRpcCall(logId, { status: "error", error: err });
142
+ throw err;
143
+ }
144
+ };
145
+ }
146
+
147
+ // 2. Mutations: POST, PUT, PATCH, DELETE
148
+ if (
149
+ prop === "post" ||
150
+ prop === "put" ||
151
+ prop === "patch" ||
152
+ prop === "delete" ||
153
+ prop === "del"
154
+ ) {
155
+ const httpMethod =
156
+ prop === "del" ? "DELETE" : (prop.toUpperCase() as "POST" | "PUT" | "PATCH" | "DELETE");
157
+
158
+ return async (body?: unknown, callOpts?: ApiCallOptions) => {
159
+ const fetchFn = options.fetch ?? globalThis.fetch;
160
+ const url = buildUrl(options.baseUrl, path);
161
+ const dynamicHeaders = await resolveHeaders(options.headers, fullPath);
162
+ const headers: Record<string, string> = {
163
+ "Content-Type": "application/json",
164
+ Accept: "application/json",
165
+ ...dynamicHeaders,
166
+ ...callOpts?.headers
167
+ };
168
+
169
+ const devtools =
170
+ typeof window !== "undefined" ? (window as any).__VISAGE_DEVTOOLS__ : null;
171
+ const logId = devtools?.logRpcCall({
172
+ id: `api_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
173
+ path: fullPath,
174
+ type: "mutation",
175
+ input: body
176
+ });
177
+
178
+ try {
179
+ const res = await fetchFn(url, {
180
+ method: httpMethod,
181
+ headers,
182
+ body: body !== undefined ? JSON.stringify(body) : undefined,
183
+ signal: callOpts?.signal
184
+ });
185
+
186
+ const contentType = res.headers.get("Content-Type") || "";
187
+ if (contentType.includes("application/json")) {
188
+ const resBody = await res.json();
189
+ if (resBody && typeof resBody === "object" && "ok" in resBody) {
190
+ if (!resBody.ok) {
191
+ devtools?.updateRpcCall(logId, { status: "error", error: resBody.error });
192
+ throw new RPCError({
193
+ code: resBody.error?.code || "INTERNAL_SERVER_ERROR",
194
+ message: resBody.error?.message || "Request failed",
195
+ issues: resBody.error?.issues
196
+ });
197
+ }
198
+ devtools?.updateRpcCall(logId, { status: "success", response: resBody.data });
199
+ return resBody.data;
200
+ }
201
+ if (!res.ok) {
202
+ throw new Error(`Request failed with status ${res.status}`);
203
+ }
204
+ devtools?.updateRpcCall(logId, { status: "success", response: resBody });
205
+ return resBody;
206
+ }
207
+
208
+ if (!res.ok) {
209
+ throw new Error(`Request failed with status ${res.status}`);
210
+ }
211
+ const text = await res.text();
212
+ devtools?.updateRpcCall(logId, { status: "success", response: text });
213
+ return text;
214
+ } catch (err) {
215
+ devtools?.updateRpcCall(logId, { status: "error", error: err });
216
+ throw err;
217
+ }
218
+ };
219
+ }
220
+
221
+ // 3. SSE Subscriptions
222
+ if (prop === "subscribe") {
223
+ return (input: unknown, callbacks: ApiSubscriptionCallbacks): ApiSubscriptionHandle => {
224
+ const fetchFn = options.fetch ?? globalThis.fetch;
225
+ const url = buildUrl(options.baseUrl, path, input);
226
+ const controller = new AbortController();
227
+
228
+ const devtools =
229
+ typeof window !== "undefined" ? (window as any).__VISAGE_DEVTOOLS__ : null;
230
+ const logId = devtools?.logRpcCall({
231
+ id: `api_sse_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
232
+ path: fullPath,
233
+ type: "subscription",
234
+ input
235
+ });
236
+
237
+ (async () => {
238
+ try {
239
+ const dynamicHeaders = await resolveHeaders(options.headers, fullPath);
240
+ const headers = {
241
+ Accept: "text/event-stream",
242
+ ...dynamicHeaders
243
+ };
244
+
245
+ const res = await fetchFn(url, {
246
+ method: "GET",
247
+ headers,
248
+ signal: controller.signal
249
+ });
250
+
251
+ if (!res.ok || !res.body) {
252
+ throw new Error(`SSE stream failed with status ${res.status}`);
253
+ }
254
+
255
+ const reader = res.body.getReader();
256
+ const decoder = new TextDecoder();
257
+ let buffer = "";
258
+
259
+ while (true) {
260
+ const { done, value } = await reader.read();
261
+ if (done) break;
262
+
263
+ buffer += decoder.decode(value, { stream: true });
264
+ const lines = buffer.split("\n\n");
265
+ buffer = lines.pop() ?? "";
266
+
267
+ for (const line of lines) {
268
+ const trimmed = line.trim();
269
+ if (trimmed.startsWith("data:")) {
270
+ const jsonStr = trimmed.slice(5).trim();
271
+ try {
272
+ const parsed = JSON.parse(jsonStr);
273
+ devtools?.logRpcEvent(logId, parsed);
274
+ callbacks.onData(parsed);
275
+ } catch (parseErr) {
276
+ devtools?.updateRpcCall(logId, { status: "error", error: parseErr });
277
+ callbacks.onError?.(parseErr);
278
+ }
279
+ } else if (trimmed.startsWith("event: error")) {
280
+ devtools?.updateRpcCall(logId, { status: "error" });
281
+ callbacks.onError?.(new Error("Server SSE error"));
282
+ }
283
+ }
284
+ }
285
+
286
+ callbacks.onComplete?.();
287
+ } catch (err: any) {
288
+ if (!controller.signal.aborted) {
289
+ callbacks.onError?.(err);
290
+ }
291
+ }
292
+ })();
293
+
294
+ return {
295
+ unsubscribe: () => {
296
+ controller.abort();
297
+ }
298
+ };
299
+ };
300
+ }
301
+
302
+ // 4. Query Signals Integration
303
+ if (prop === "queryKey") {
304
+ return (input?: unknown) => {
305
+ const resolved = typeof input === "function" ? (input as () => unknown)() : input;
306
+ const key: unknown[] = [...path];
307
+ if (resolved !== undefined) key.push(resolved);
308
+ return key;
309
+ };
310
+ }
311
+
312
+ if (prop === "queryOptions") {
313
+ return (input?: unknown, extra?: Record<string, unknown>) => {
314
+ const resolved = typeof input === "function" ? (input as () => unknown)() : input;
315
+ const key: unknown[] = [...path];
316
+ if (resolved !== undefined) key.push(resolved);
317
+ return {
318
+ queryKey: key,
319
+ queryFn: ({ signal }: { signal: AbortSignal }) => {
320
+ const proxy = createApiProxy(options, path);
321
+ return proxy.get(resolved, { signal });
322
+ },
323
+ ...extra
324
+ };
325
+ };
326
+ }
327
+
328
+ if (prop === "createQuery" || prop === "useQuery") {
329
+ return (input?: unknown, extra?: Record<string, unknown>) => {
330
+ const proxy = createApiProxy(options, path);
331
+ return createQuery(() => proxy.queryOptions(input, extra));
332
+ };
333
+ }
334
+
335
+ // 5. Mutation Signals Integration
336
+ if (prop === "mutationKey") {
337
+ return () => [...path];
338
+ }
339
+
340
+ if (prop === "mutationOptions") {
341
+ return (extra?: Record<string, unknown>) => {
342
+ return {
343
+ mutationKey: [...path],
344
+ mutationFn: (body: unknown) => {
345
+ const proxy = createApiProxy(options, path);
346
+ return proxy.post(body);
347
+ },
348
+ ...extra
349
+ };
350
+ };
351
+ }
352
+
353
+ if (prop === "createMutation" || prop === "useMutation") {
354
+ return (extra?: Record<string, unknown>) => {
355
+ const proxy = createApiProxy(options, path);
356
+ return createMutation(proxy.mutationOptions(extra));
357
+ };
358
+ }
359
+
360
+ // 6. Nested Route Segment
361
+ return createApiProxy(options, [...path, prop]);
362
+ }
363
+ });
364
+ }
365
+
366
+ /**
367
+ * Create a type-safe API client for file-based API routes
368
+ */
369
+ export function createApiClient<TRoutes = any>(options: ApiClientOptions): ApiClient<TRoutes> {
370
+ return createApiProxy(options, []) as ApiClient<TRoutes>;
371
+ }
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Visage File-Based Router & Middleware Discovery
3
+ */
4
+ import { parsePragma, shouldApplyMiddleware } from "./pragma";
5
+ import type { ApiProcedure, FileRouteModule, MiddlewareDefinition, RouteMatch } from "./types";
6
+
7
+ export interface FileRouteEntry {
8
+ filePath: string;
9
+ module: FileRouteModule;
10
+ sourceCode?: string;
11
+ }
12
+
13
+ /**
14
+ * Compile a route path pattern into a regex and parameter list
15
+ * e.g. `/admin/users/:id` or `/admin/files/*slug`
16
+ */
17
+ export function compileRoutePattern(path: string): { pattern: RegExp; paramNames: string[] } {
18
+ const paramNames: string[] = [];
19
+ const normalized = path.replace(/\/+$/, "") || "/";
20
+
21
+ const regexStr = normalized
22
+ .replace(/:([a-zA-Z0-9_]+)/g, (_, name) => {
23
+ paramNames.push(name);
24
+ return "([^/]+)";
25
+ })
26
+ .replace(/\*([a-zA-Z0-9_]+)/g, (_, name) => {
27
+ paramNames.push(name);
28
+ return "(.*)";
29
+ });
30
+
31
+ const pattern = new RegExp(`^${regexStr}(?:/)?$`);
32
+ return { pattern, paramNames };
33
+ }
34
+
35
+ /**
36
+ * Normalize a file path to an API URL route path and directory
37
+ */
38
+ export function normalizeFilePath(rawPath: string): {
39
+ isMiddleware: boolean;
40
+ routePath: string;
41
+ directory: string;
42
+ } {
43
+ // Strip relative dots and common root folder prefixes
44
+ const cleaned = rawPath
45
+ .replace(/^\.\//, "")
46
+ .replace(/\.(ts|tsx|js|jsx)$/, "")
47
+ .replace(/^(src\/)?(routes|api)\//, "");
48
+
49
+ const parts = cleaned.split("/").filter(Boolean);
50
+ const fileName = parts.pop() || "";
51
+ const isMiddleware = fileName === "_middleware";
52
+
53
+ const dirParts = [...parts];
54
+ const directory = `/${dirParts.join("/")}`.replace(/\/+$/, "") || "/";
55
+
56
+ if (isMiddleware) {
57
+ return {
58
+ isMiddleware: true,
59
+ routePath: directory,
60
+ directory
61
+ };
62
+ }
63
+
64
+ // Handle index files
65
+ if (fileName !== "index") {
66
+ // Transform [id] -> :id and [...slug] -> *slug
67
+ const transformedFile = fileName
68
+ .replace(/\[\.\.\.([a-zA-Z0-9_]+)\]/g, "*$1")
69
+ .replace(/\[([a-zA-Z0-9_]+)\]/g, ":$1");
70
+ parts.push(transformedFile);
71
+ }
72
+
73
+ const routePath = `/${parts.join("/")}`.replace(/\/+$/, "") || "/";
74
+
75
+ return {
76
+ isMiddleware: false,
77
+ routePath,
78
+ directory
79
+ };
80
+ }
81
+
82
+ /**
83
+ * Build a file-based routing manifest with hierarchical middleware cascades
84
+ */
85
+ export function buildFileRouter(
86
+ files: Record<string, FileRouteModule | { default?: any; [key: string]: any }>,
87
+ sourceMap: Record<string, string> = {}
88
+ ): RouteMatch[] {
89
+ const middlewaresByDir: MiddlewareDefinition[] = [];
90
+ const rawRoutes: Array<{
91
+ filePath: string;
92
+ routePath: string;
93
+ directory: string;
94
+ module: FileRouteModule;
95
+ }> = [];
96
+
97
+ // 1. Separate middlewares and route handlers
98
+ for (const [filePath, mod] of Object.entries(files)) {
99
+ const { isMiddleware, routePath, directory } = normalizeFilePath(filePath);
100
+ const sourceCode = sourceMap[filePath] || "";
101
+ const pragma = mod.pragma || parsePragma(sourceCode);
102
+
103
+ if (isMiddleware) {
104
+ const mwFn = typeof mod === "function" ? (mod as any) : mod.middleware || mod.default;
105
+
106
+ if (typeof mwFn === "function") {
107
+ middlewaresByDir.push({
108
+ fn: mwFn,
109
+ pragma,
110
+ filePath,
111
+ directory
112
+ });
113
+ }
114
+ } else {
115
+ rawRoutes.push({
116
+ filePath,
117
+ routePath,
118
+ directory,
119
+ module: mod as FileRouteModule
120
+ });
121
+ }
122
+ }
123
+
124
+ // 2. Sort middlewares by directory depth & priority
125
+ middlewaresByDir.sort((a, b) => {
126
+ const dirA = a.directory || "/";
127
+ const dirB = b.directory || "/";
128
+ const depthA = dirA.split("/").filter(Boolean).length;
129
+ const depthB = dirB.split("/").filter(Boolean).length;
130
+ if (depthA !== depthB) return depthA - depthB;
131
+ return (b.pragma.priority ?? 0) - (a.pragma.priority ?? 0);
132
+ });
133
+
134
+ // 3. Assemble routes with applicable cascading middlewares
135
+ const routes: RouteMatch[] = [];
136
+
137
+ for (const raw of rawRoutes) {
138
+ const applicableMiddlewares = middlewaresByDir.filter((mw) =>
139
+ shouldApplyMiddleware(mw.pragma, raw.routePath, raw.directory, mw.directory || "/")
140
+ );
141
+
142
+ const { pattern, paramNames } = compileRoutePattern(raw.routePath);
143
+ const procedures: Record<string, ApiProcedure> = {};
144
+
145
+ // Extract procedures or standard export
146
+ const mod = raw.module;
147
+ if (mod.default && typeof mod.default === "object" && "type" in mod.default) {
148
+ procedures.default = mod.default as ApiProcedure;
149
+ }
150
+
151
+ for (const [key, val] of Object.entries(mod)) {
152
+ if (val && typeof val === "object" && "type" in val && "options" in val) {
153
+ procedures[key] = val as ApiProcedure;
154
+ }
155
+ }
156
+
157
+ const handler =
158
+ typeof mod.default === "function" && !("type" in mod.default)
159
+ ? (mod.default as (req: Request, ctx: any) => Promise<Response> | Response)
160
+ : undefined;
161
+
162
+ routes.push({
163
+ pattern,
164
+ paramNames,
165
+ routePath: raw.routePath,
166
+ filePath: raw.filePath,
167
+ directory: raw.directory,
168
+ procedures,
169
+ middlewares: applicableMiddlewares,
170
+ handler
171
+ });
172
+ }
173
+
174
+ // 4. Sort routes: static paths before parameterized paths
175
+ routes.sort((a, b) => {
176
+ const isParamA = a.routePath.includes(":") || a.routePath.includes("*");
177
+ const isParamB = b.routePath.includes(":") || b.routePath.includes("*");
178
+ if (isParamA !== isParamB) return isParamA ? 1 : -1;
179
+ return b.routePath.length - a.routePath.length;
180
+ });
181
+
182
+ return routes;
183
+ }