@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
package/package.json
CHANGED
|
@@ -1,6 +1,142 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@epoch32/visage",
|
|
3
|
-
"version": "0.0.0
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|