@epoch32/visage 0.0.0-stage → 1.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 (135) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +110 -2
  3. package/dist/src/api/index.js +4002 -0
  4. package/dist/src/charts/index.js +2717 -0
  5. package/dist/src/devtools/index.js +4677 -0
  6. package/dist/src/dom/index.js +1550 -0
  7. package/dist/src/form/index.js +1415 -0
  8. package/dist/src/index.js +11826 -0
  9. package/dist/src/jsx-runtime/index.js +1630 -0
  10. package/dist/src/query/index.js +2422 -0
  11. package/dist/src/router/index.js +1856 -0
  12. package/dist/src/rpc/index.js +3484 -0
  13. package/dist/src/server/index.js +1173 -0
  14. package/dist/src/signals/index.js +1077 -0
  15. package/dist/src/style/index.js +2340 -0
  16. package/dist/src/template/index.js +1149 -0
  17. package/dist/src/ui/index.js +4155 -0
  18. package/dist/src/virtual/index.js +1955 -0
  19. package/dist/src/vite/index.js +984 -0
  20. package/package.json +140 -4
  21. package/src/api/client.ts +371 -0
  22. package/src/api/file-router.ts +183 -0
  23. package/src/api/handler.ts +326 -0
  24. package/src/api/index.ts +71 -0
  25. package/src/api/middleware.ts +54 -0
  26. package/src/api/pragma.ts +155 -0
  27. package/src/api/procedure.ts +172 -0
  28. package/src/api/types.ts +249 -0
  29. package/src/charts/Area.tsx +111 -0
  30. package/src/charts/Axis.tsx +126 -0
  31. package/src/charts/Bar.tsx +86 -0
  32. package/src/charts/CartesianGrid.tsx +72 -0
  33. package/src/charts/Chart.tsx +269 -0
  34. package/src/charts/Cursor.tsx +42 -0
  35. package/src/charts/Legend.tsx +56 -0
  36. package/src/charts/Line.tsx +106 -0
  37. package/src/charts/Pie.tsx +88 -0
  38. package/src/charts/Tooltip.tsx +127 -0
  39. package/src/charts/context.ts +30 -0
  40. package/src/charts/index.ts +14 -0
  41. package/src/charts/path.ts +155 -0
  42. package/src/charts/scale.ts +133 -0
  43. package/src/charts/types.ts +50 -0
  44. package/src/devtools/DomTab.tsx +134 -0
  45. package/src/devtools/GraphInspector.tsx +196 -0
  46. package/src/devtools/GraphTab.tsx +315 -0
  47. package/src/devtools/QueryTab.tsx +200 -0
  48. package/src/devtools/RpcTab.tsx +190 -0
  49. package/src/devtools/SignalsTab.tsx +129 -0
  50. package/src/devtools/component.tsx +243 -0
  51. package/src/devtools/highlighter.ts +134 -0
  52. package/src/devtools/hub.ts +175 -0
  53. package/src/devtools/index.ts +20 -0
  54. package/src/devtools/styles.ts +313 -0
  55. package/src/devtools/types.ts +45 -0
  56. package/src/dom/async.ts +232 -0
  57. package/src/dom/control-flow.ts +353 -0
  58. package/src/dom/index.ts +32 -0
  59. package/src/dom/lazy.ts +82 -0
  60. package/src/dom/render.ts +286 -0
  61. package/src/dom/resource.ts +120 -0
  62. package/src/form/form.ts +218 -0
  63. package/src/form/index.ts +17 -0
  64. package/src/form/schema.ts +299 -0
  65. package/src/index.ts +413 -0
  66. package/src/jsx-dev-runtime/index.ts +1 -0
  67. package/src/jsx-runtime/index.ts +112 -0
  68. package/src/jsx-runtime/jsx.d.ts +278 -0
  69. package/src/query/create-mutation.ts +64 -0
  70. package/src/query/create-query.ts +426 -0
  71. package/src/query/focus-manager.ts +68 -0
  72. package/src/query/index.ts +63 -0
  73. package/src/query/mutation-cache.ts +138 -0
  74. package/src/query/online-manager.ts +67 -0
  75. package/src/query/query-cache.ts +369 -0
  76. package/src/query/query-client.ts +248 -0
  77. package/src/query/types.ts +221 -0
  78. package/src/query/utils.ts +132 -0
  79. package/src/router/index.ts +19 -0
  80. package/src/router/router.tsx +352 -0
  81. package/src/rpc/client.ts +317 -0
  82. package/src/rpc/error.ts +70 -0
  83. package/src/rpc/index.ts +52 -0
  84. package/src/rpc/procedure.ts +88 -0
  85. package/src/rpc/router.ts +105 -0
  86. package/src/rpc/server.ts +511 -0
  87. package/src/rpc/types.ts +328 -0
  88. package/src/server/hydrate.ts +21 -0
  89. package/src/server/index.ts +10 -0
  90. package/src/server/render-to-stream.ts +31 -0
  91. package/src/server/render-to-string.ts +125 -0
  92. package/src/signals/computed.ts +217 -0
  93. package/src/signals/context.ts +89 -0
  94. package/src/signals/effect.ts +161 -0
  95. package/src/signals/index.ts +57 -0
  96. package/src/signals/scope.ts +132 -0
  97. package/src/signals/signal.ts +203 -0
  98. package/src/signals/store.ts +163 -0
  99. package/src/signals/system.ts +124 -0
  100. package/src/signals/types.ts +64 -0
  101. package/src/style/component.tsx +122 -0
  102. package/src/style/create-style.ts +229 -0
  103. package/src/style/cx.ts +75 -0
  104. package/src/style/hash.ts +19 -0
  105. package/src/style/index.ts +8 -0
  106. package/src/style/layout.tsx +406 -0
  107. package/src/style/manager.ts +87 -0
  108. package/src/style/parser.ts +257 -0
  109. package/src/style/scale.ts +237 -0
  110. package/src/template/index.ts +4 -0
  111. package/src/template/template.ts +126 -0
  112. package/src/ui/accordion.tsx +286 -0
  113. package/src/ui/command-dialog.tsx +49 -0
  114. package/src/ui/command.tsx +400 -0
  115. package/src/ui/data-table.tsx +322 -0
  116. package/src/ui/dialog.tsx +362 -0
  117. package/src/ui/dropdown-menu.tsx +381 -0
  118. package/src/ui/icon.tsx +69 -0
  119. package/src/ui/index.ts +138 -0
  120. package/src/ui/layout.tsx +13 -0
  121. package/src/ui/popover.tsx +249 -0
  122. package/src/ui/switch.tsx +114 -0
  123. package/src/ui/tabs.tsx +297 -0
  124. package/src/ui/toast.tsx +280 -0
  125. package/src/ui/tooltip.tsx +262 -0
  126. package/src/ui/utils.ts +110 -0
  127. package/src/virtual/index.ts +12 -0
  128. package/src/virtual/virtual-list.tsx +185 -0
  129. package/src/virtual/virtualizer.ts +274 -0
  130. package/src/vite/hmr.ts +38 -0
  131. package/src/vite/index.ts +3 -0
  132. package/src/vite/lucide.ts +124 -0
  133. package/src/vite/plugin.ts +102 -0
  134. package/src/vite/rpc-middleware.ts +82 -0
  135. package/src/vite/types.ts +65 -0
@@ -0,0 +1,511 @@
1
+ import { ERROR_CODE_TO_STATUS, RPCError } from "./error";
2
+ import { executeProcedure } from "./procedure";
3
+ import type { CORSOptions, FetchHandlerOptions, Procedure, Router, RPCResponse } from "./types";
4
+
5
+ function getCORSHeaders(cors: boolean | CORSOptions | undefined, req: Request): Headers {
6
+ const headers = new Headers();
7
+ if (!cors) return headers;
8
+
9
+ const opts: CORSOptions = typeof cors === "object" ? cors : {};
10
+ const originHeader = req.headers.get("origin") ?? "*";
11
+
12
+ let allowOrigin = "*";
13
+ if (typeof opts.origin === "function") {
14
+ const res = opts.origin(originHeader);
15
+ if (typeof res === "string") allowOrigin = res;
16
+ else if (res) allowOrigin = originHeader;
17
+ } else if (typeof opts.origin === "string") {
18
+ allowOrigin = opts.origin;
19
+ } else if (Array.isArray(opts.origin)) {
20
+ if (opts.origin.includes(originHeader)) allowOrigin = originHeader;
21
+ } else {
22
+ allowOrigin = originHeader || "*";
23
+ }
24
+
25
+ headers.set("Access-Control-Allow-Origin", allowOrigin);
26
+ headers.set(
27
+ "Access-Control-Allow-Methods",
28
+ (opts.methods ?? ["GET", "POST", "OPTIONS", "HEAD"]).join(", ")
29
+ );
30
+ headers.set(
31
+ "Access-Control-Allow-Headers",
32
+ (opts.allowedHeaders ?? ["Content-Type", "Authorization", "X-Requested-With"]).join(", ")
33
+ );
34
+
35
+ if (opts.exposedHeaders) {
36
+ headers.set("Access-Control-Expose-Headers", opts.exposedHeaders.join(", "));
37
+ }
38
+ if (opts.credentials) {
39
+ headers.set("Access-Control-Allow-Credentials", "true");
40
+ }
41
+ if (opts.maxAge !== undefined) {
42
+ headers.set("Access-Control-Max-Age", opts.maxAge.toString());
43
+ }
44
+
45
+ return headers;
46
+ }
47
+
48
+ function resolveProcedureFromPath(
49
+ router: Router<any, any>,
50
+ path: string
51
+ ): Procedure<any, any, any, any> {
52
+ const parts = path.split(/[./]/).filter(Boolean);
53
+ let current: any = router._def.record;
54
+
55
+ for (let i = 0; i < parts.length; i++) {
56
+ const part = parts[i]!;
57
+ if (!current || typeof current !== "object") {
58
+ throw new RPCError({
59
+ code: "NOT_FOUND",
60
+ message: `Procedure '${path}' not found`
61
+ });
62
+ }
63
+
64
+ if (i === parts.length - 1) {
65
+ if (part in current && "type" in current[part]) {
66
+ return current[part];
67
+ }
68
+ throw new RPCError({
69
+ code: "NOT_FOUND",
70
+ message: `Procedure '${path}' not found`
71
+ });
72
+ }
73
+
74
+ if (part in current && "_def" in current[part]) {
75
+ current = current[part]._def.record;
76
+ } else if (part in current && typeof current[part] === "object") {
77
+ current = current[part];
78
+ } else {
79
+ throw new RPCError({
80
+ code: "NOT_FOUND",
81
+ message: `Procedure '${path}' not found`
82
+ });
83
+ }
84
+ }
85
+
86
+ throw new RPCError({
87
+ code: "NOT_FOUND",
88
+ message: `Procedure '${path}' not found`
89
+ });
90
+ }
91
+
92
+ export function matchesRulePattern(
93
+ pattern: string,
94
+ urlPath: string,
95
+ procedurePath: string
96
+ ): boolean {
97
+ if (pattern === "*") return true;
98
+
99
+ const cleanUrl = urlPath.replace(/^\/+|\/+$/g, "");
100
+ const cleanPattern = pattern.replace(/^\/+|\/+$/g, "");
101
+ const cleanProc = procedurePath.replace(/^\/+|\/+$/g, "");
102
+
103
+ if (
104
+ cleanUrl === cleanPattern ||
105
+ cleanProc === cleanPattern ||
106
+ cleanProc === cleanPattern.replace(/\//g, ".")
107
+ ) {
108
+ return true;
109
+ }
110
+
111
+ if (cleanPattern.endsWith("*")) {
112
+ const prefix = cleanPattern.slice(0, -1);
113
+ if (cleanUrl.startsWith(prefix) || cleanProc.startsWith(prefix)) {
114
+ return true;
115
+ }
116
+ const dotPrefix = prefix.replace(/\//g, ".");
117
+ if (cleanProc.startsWith(dotPrefix)) {
118
+ return true;
119
+ }
120
+ }
121
+
122
+ try {
123
+ const regexStr = cleanPattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
124
+ const regex = new RegExp(`^${regexStr}$`);
125
+ return regex.test(cleanUrl) || regex.test(cleanProc);
126
+ } catch {
127
+ return false;
128
+ }
129
+ }
130
+
131
+ export function createFetchHandler<TRouter extends Router<any, any>, TContext = any>(
132
+ options: FetchHandlerOptions<TRouter, TContext>
133
+ ): (request: Request, ...extra: any[]) => Promise<Response> {
134
+ const {
135
+ router,
136
+ endpoint = "",
137
+ createContext,
138
+ onError,
139
+ cors = true,
140
+ securityHeaders = true,
141
+ rules
142
+ } = options;
143
+
144
+ return async (request: Request, ...extra: any[]): Promise<Response> => {
145
+ const corsHeaders = getCORSHeaders(cors, request);
146
+
147
+ if (request.method === "OPTIONS") {
148
+ return new Response(null, {
149
+ status: 204,
150
+ headers: corsHeaders
151
+ });
152
+ }
153
+
154
+ const baseHeaders = new Headers(corsHeaders);
155
+ if (securityHeaders) {
156
+ if (typeof securityHeaders === "object") {
157
+ for (const [k, v] of Object.entries(securityHeaders)) {
158
+ baseHeaders.set(k, v);
159
+ }
160
+ } else {
161
+ baseHeaders.set("X-Content-Type-Options", "nosniff");
162
+ }
163
+ }
164
+
165
+ const url = new URL(request.url);
166
+ let pathname = url.pathname;
167
+ if (endpoint && pathname.startsWith(endpoint)) {
168
+ pathname = pathname.slice(endpoint.length);
169
+ }
170
+ pathname = pathname.replace(/^\/+|\/+$/g, "");
171
+
172
+ const procedurePath = pathname || url.searchParams.get("procedure") || "";
173
+
174
+ let ctx: TContext = {} as TContext;
175
+ try {
176
+ if (createContext) {
177
+ ctx = await createContext({ req: request, extra });
178
+ }
179
+ } catch (err: any) {
180
+ const error =
181
+ err instanceof RPCError
182
+ ? err
183
+ : new RPCError({
184
+ code: "UNAUTHORIZED",
185
+ message: err.message ?? "Context initialization failed",
186
+ cause: err
187
+ });
188
+ onError?.({ error, req: request });
189
+ baseHeaders.set("Content-Type", "application/json");
190
+ return new Response(
191
+ JSON.stringify({
192
+ error: {
193
+ code: error.code,
194
+ message: error.message,
195
+ issues: error.issues
196
+ }
197
+ }),
198
+ {
199
+ status: error.status,
200
+ headers: baseHeaders
201
+ }
202
+ );
203
+ }
204
+
205
+ // Evaluate declarative pattern rules
206
+ const rulesList: Array<{ match: string } & import("./types").RouteRule> = [];
207
+ if (rules) {
208
+ if (Array.isArray(rules)) {
209
+ rulesList.push(...rules);
210
+ } else {
211
+ for (const [match, rule] of Object.entries(rules)) {
212
+ rulesList.push({ match, ...rule });
213
+ }
214
+ }
215
+ }
216
+
217
+ const matchingRules = rulesList.filter((r) =>
218
+ matchesRulePattern(r.match, url.pathname, procedurePath)
219
+ );
220
+
221
+ // Enforce required headers, query parameters, and custom guards from rules
222
+ for (const rule of matchingRules) {
223
+ if (rule.requireHeaders) {
224
+ for (const headerName of rule.requireHeaders) {
225
+ if (!request.headers.get(headerName)) {
226
+ const error = new RPCError({
227
+ code: "UNAUTHORIZED",
228
+ message: `Missing required header '${headerName}' for rule '${rule.match}'`
229
+ });
230
+ onError?.({ error, path: procedurePath, req: request });
231
+ baseHeaders.set("Content-Type", "application/json");
232
+ return new Response(
233
+ JSON.stringify({
234
+ error: {
235
+ code: error.code,
236
+ message: error.message
237
+ }
238
+ }),
239
+ {
240
+ status: 401,
241
+ headers: baseHeaders
242
+ }
243
+ );
244
+ }
245
+ }
246
+ }
247
+
248
+ if (rule.requireParams) {
249
+ for (const paramName of rule.requireParams) {
250
+ if (!url.searchParams.has(paramName)) {
251
+ const error = new RPCError({
252
+ code: "BAD_REQUEST",
253
+ message: `Missing required query parameter '${paramName}' for rule '${rule.match}'`
254
+ });
255
+ onError?.({ error, path: procedurePath, req: request });
256
+ baseHeaders.set("Content-Type", "application/json");
257
+ return new Response(
258
+ JSON.stringify({
259
+ error: {
260
+ code: error.code,
261
+ message: error.message
262
+ }
263
+ }),
264
+ {
265
+ status: 400,
266
+ headers: baseHeaders
267
+ }
268
+ );
269
+ }
270
+ }
271
+ }
272
+
273
+ if (rule.guard) {
274
+ try {
275
+ const guardResult = await rule.guard({ req: request, ctx, path: procedurePath });
276
+ if (guardResult === false) {
277
+ throw new RPCError({
278
+ code: "UNAUTHORIZED",
279
+ message: `Access denied by route rule guard for '${rule.match}'`
280
+ });
281
+ }
282
+ } catch (err: any) {
283
+ const error =
284
+ err instanceof RPCError
285
+ ? err
286
+ : new RPCError({
287
+ code: "UNAUTHORIZED",
288
+ message: err.message ?? "Unauthorized",
289
+ cause: err
290
+ });
291
+ onError?.({ error, path: procedurePath, req: request });
292
+ baseHeaders.set("Content-Type", "application/json");
293
+ return new Response(
294
+ JSON.stringify({
295
+ error: {
296
+ code: error.code,
297
+ message: error.message
298
+ }
299
+ }),
300
+ {
301
+ status: error.status,
302
+ headers: baseHeaders
303
+ }
304
+ );
305
+ }
306
+ }
307
+
308
+ // Inject custom response headers defined by matching rule
309
+ if (rule.headers) {
310
+ const resHeaders =
311
+ typeof rule.headers === "function" ? await rule.headers(request, ctx) : rule.headers;
312
+ for (const [k, v] of Object.entries(resHeaders)) {
313
+ baseHeaders.set(k, v);
314
+ }
315
+ }
316
+ }
317
+
318
+ // Check for batch execution in POST body
319
+ if (request.method === "POST" && (!pathname || pathname === "batch")) {
320
+ try {
321
+ const body = await request.json();
322
+ if (Array.isArray(body)) {
323
+ const batchResults = await Promise.all(
324
+ body.map(async (item: { id?: string | number; path: string; input?: any }) => {
325
+ try {
326
+ const proc = resolveProcedureFromPath(router, item.path);
327
+ const data = await executeProcedure(proc, {
328
+ rawInput: item.input,
329
+ ctx,
330
+ req: request,
331
+ signal: request.signal
332
+ });
333
+ return {
334
+ id: item.id,
335
+ result: { data }
336
+ };
337
+ } catch (err: any) {
338
+ const error =
339
+ err instanceof RPCError
340
+ ? err
341
+ : new RPCError({
342
+ code: "INTERNAL_SERVER_ERROR",
343
+ message: err.message ?? "Internal Server Error",
344
+ cause: err
345
+ });
346
+ onError?.({ error, path: item.path, req: request });
347
+ return {
348
+ id: item.id,
349
+ error: {
350
+ code: error.code,
351
+ message: error.message,
352
+ issues: error.issues
353
+ }
354
+ };
355
+ }
356
+ })
357
+ );
358
+
359
+ baseHeaders.set("Content-Type", "application/json");
360
+ return new Response(JSON.stringify(batchResults), {
361
+ status: 200,
362
+ headers: baseHeaders
363
+ });
364
+ }
365
+ } catch (err: any) {
366
+ const error = new RPCError({
367
+ code: "BAD_REQUEST",
368
+ message: "Malformed batch request body",
369
+ cause: err
370
+ });
371
+ baseHeaders.set("Content-Type", "application/json");
372
+ return new Response(
373
+ JSON.stringify({
374
+ error: {
375
+ code: error.code,
376
+ message: error.message
377
+ }
378
+ }),
379
+ {
380
+ status: 400,
381
+ headers: baseHeaders
382
+ }
383
+ );
384
+ }
385
+ }
386
+
387
+ try {
388
+ const procedure = resolveProcedureFromPath(router, procedurePath);
389
+
390
+ // Verify HTTP method constraints
391
+ if (procedure.type === "mutation" && request.method !== "POST") {
392
+ throw new RPCError({
393
+ code: "METHOD_NOT_ALLOWED",
394
+ message: "Mutations must use POST method"
395
+ });
396
+ }
397
+
398
+ // Parse input
399
+ let rawInput: unknown;
400
+ if (request.method === "GET") {
401
+ const inputParam = url.searchParams.get("input");
402
+ if (inputParam !== null) {
403
+ try {
404
+ rawInput = JSON.parse(inputParam);
405
+ } catch {
406
+ rawInput = inputParam;
407
+ }
408
+ }
409
+ } else if (request.method === "POST") {
410
+ try {
411
+ const body = await request.json();
412
+ rawInput = body && typeof body === "object" && "input" in body ? body.input : body;
413
+ } catch {
414
+ rawInput = undefined;
415
+ }
416
+ }
417
+
418
+ // Handle Subscription (Server-Sent Events)
419
+ if (procedure.type === "subscription") {
420
+ const iterable = (await executeProcedure(procedure, {
421
+ rawInput,
422
+ ctx,
423
+ req: request,
424
+ signal: request.signal
425
+ })) as AsyncIterable<any>;
426
+
427
+ if (!iterable || typeof (iterable as any)[Symbol.asyncIterator] !== "function") {
428
+ throw new RPCError({
429
+ code: "INTERNAL_SERVER_ERROR",
430
+ message: "Subscription procedure must return an AsyncIterable"
431
+ });
432
+ }
433
+
434
+ const encoder = new TextEncoder();
435
+ const stream = new ReadableStream({
436
+ async start(controller) {
437
+ try {
438
+ for await (const chunk of iterable) {
439
+ if (request.signal.aborted) break;
440
+ const message = `data: ${JSON.stringify(chunk)}\n\n`;
441
+ controller.enqueue(encoder.encode(message));
442
+ }
443
+ controller.close();
444
+ } catch (err: any) {
445
+ const errorMessage = `event: error\ndata: ${JSON.stringify({ message: err.message ?? "Stream error" })}\n\n`;
446
+ controller.enqueue(encoder.encode(errorMessage));
447
+ controller.close();
448
+ }
449
+ },
450
+ cancel() {
451
+ if (typeof (iterable as any).return === "function") {
452
+ (iterable as any).return();
453
+ }
454
+ }
455
+ });
456
+
457
+ baseHeaders.set("Content-Type", "text/event-stream; charset=utf-8");
458
+ baseHeaders.set("Cache-Control", "no-cache, no-transform");
459
+ baseHeaders.set("Connection", "keep-alive");
460
+
461
+ return new Response(stream, {
462
+ status: 200,
463
+ headers: baseHeaders
464
+ });
465
+ }
466
+
467
+ // Standard Query or Mutation execution
468
+ const data = await executeProcedure(procedure, {
469
+ rawInput,
470
+ ctx,
471
+ req: request,
472
+ signal: request.signal
473
+ });
474
+
475
+ const responsePayload: RPCResponse = {
476
+ result: { data }
477
+ };
478
+
479
+ baseHeaders.set("Content-Type", "application/json");
480
+ return new Response(JSON.stringify(responsePayload), {
481
+ status: 200,
482
+ headers: baseHeaders
483
+ });
484
+ } catch (err: any) {
485
+ const error =
486
+ err instanceof RPCError
487
+ ? err
488
+ : new RPCError({
489
+ code: "INTERNAL_SERVER_ERROR",
490
+ message: err.message ?? "Internal Server Error",
491
+ cause: err
492
+ });
493
+
494
+ onError?.({ error, path: procedurePath, req: request });
495
+
496
+ const responsePayload: RPCResponse = {
497
+ error: {
498
+ code: error.code,
499
+ message: error.message,
500
+ issues: error.issues
501
+ }
502
+ };
503
+
504
+ baseHeaders.set("Content-Type", "application/json");
505
+ return new Response(JSON.stringify(responsePayload), {
506
+ status: ERROR_CODE_TO_STATUS[error.code] ?? 500,
507
+ headers: baseHeaders
508
+ });
509
+ }
510
+ };
511
+ }