@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,328 @@
1
+ import type { InferSchemaType, SchemaLike, ValidationIssue } from "../form/schema";
2
+ import type {
3
+ CreateQueryOptions,
4
+ MutationOptions,
5
+ MutationResult,
6
+ QueryResult
7
+ } from "../query/types";
8
+
9
+ export type RPCErrorCode =
10
+ | "BAD_REQUEST"
11
+ | "UNAUTHORIZED"
12
+ | "FORBIDDEN"
13
+ | "NOT_FOUND"
14
+ | "METHOD_NOT_ALLOWED"
15
+ | "TIMEOUT"
16
+ | "CONFLICT"
17
+ | "UNPROCESSABLE_CONTENT"
18
+ | "INTERNAL_SERVER_ERROR";
19
+
20
+ export type ProcedureType = "query" | "mutation" | "subscription";
21
+
22
+ export type MiddlewareFn<TContext = any, TNewContext = TContext> = (opts: {
23
+ ctx: TContext;
24
+ req: Request;
25
+ signal: AbortSignal;
26
+ next: (opts?: { ctx?: TNewContext }) => Promise<any>;
27
+ }) => Promise<any>;
28
+
29
+ export interface ProcedureOptionsWithInput<
30
+ TSchema extends SchemaLike<any>,
31
+ TOutput = any,
32
+ TContext = any
33
+ > {
34
+ input: TSchema;
35
+ output?: SchemaLike<TOutput>;
36
+ resolve: (opts: {
37
+ input: InferSchemaType<TSchema>;
38
+ ctx: TContext;
39
+ req: Request;
40
+ signal: AbortSignal;
41
+ }) => Promise<TOutput> | TOutput | AsyncIterable<TOutput>;
42
+ }
43
+
44
+ export interface ProcedureOptionsWithoutInput<TOutput = any, TContext = any> {
45
+ input?: undefined;
46
+ output?: SchemaLike<TOutput>;
47
+ resolve: (opts: {
48
+ input: void;
49
+ ctx: TContext;
50
+ req: Request;
51
+ signal: AbortSignal;
52
+ }) => Promise<TOutput> | TOutput | AsyncIterable<TOutput>;
53
+ }
54
+
55
+ export type ProcedureOptions<TInput = any, TOutput = any, TContext = any> = {
56
+ input?: SchemaLike<TInput>;
57
+ output?: SchemaLike<TOutput>;
58
+ resolve: (opts: {
59
+ input: TInput;
60
+ ctx: TContext;
61
+ req: Request;
62
+ signal: AbortSignal;
63
+ }) => Promise<TOutput> | TOutput | AsyncIterable<TOutput>;
64
+ };
65
+
66
+ export interface Procedure<
67
+ TType extends ProcedureType = ProcedureType,
68
+ TInput = any,
69
+ TOutput = any,
70
+ TContext = any
71
+ > {
72
+ type: TType;
73
+ inputSchema?: SchemaLike<TInput>;
74
+ outputSchema?: SchemaLike<TOutput>;
75
+ middlewares: MiddlewareFn<any, any>[];
76
+ resolve: (opts: {
77
+ input: TInput;
78
+ ctx: TContext;
79
+ req: Request;
80
+ signal: AbortSignal;
81
+ }) => Promise<TOutput> | TOutput | AsyncIterable<TOutput>;
82
+ }
83
+
84
+ export type ProcedureRecord = Record<string, Procedure<any, any, any, any> | Router<any, any>>;
85
+
86
+ export interface Router<TRecord extends ProcedureRecord = ProcedureRecord, TContext = any> {
87
+ _def: {
88
+ record: TRecord;
89
+ middlewares: MiddlewareFn<any, any>[];
90
+ };
91
+ query<TName extends string, TSchema extends SchemaLike<any>, TOutput = any>(
92
+ name: TName,
93
+ options: ProcedureOptionsWithInput<TSchema, TOutput, TContext>
94
+ ): Router<
95
+ TRecord & { [K in TName]: Procedure<"query", InferSchemaType<TSchema>, TOutput, TContext> },
96
+ TContext
97
+ >;
98
+ query<TName extends string, TOutput = any>(
99
+ name: TName,
100
+ options: ProcedureOptionsWithoutInput<TOutput, TContext>
101
+ ): Router<TRecord & { [K in TName]: Procedure<"query", void, TOutput, TContext> }, TContext>;
102
+
103
+ mutation<TName extends string, TSchema extends SchemaLike<any>, TOutput = any>(
104
+ name: TName,
105
+ options: ProcedureOptionsWithInput<TSchema, TOutput, TContext>
106
+ ): Router<
107
+ TRecord & { [K in TName]: Procedure<"mutation", InferSchemaType<TSchema>, TOutput, TContext> },
108
+ TContext
109
+ >;
110
+ mutation<TName extends string, TOutput = any>(
111
+ name: TName,
112
+ options: ProcedureOptionsWithoutInput<TOutput, TContext>
113
+ ): Router<TRecord & { [K in TName]: Procedure<"mutation", void, TOutput, TContext> }, TContext>;
114
+
115
+ subscription<TName extends string, TSchema extends SchemaLike<any>, TOutput = any>(
116
+ name: TName,
117
+ options: ProcedureOptionsWithInput<TSchema, TOutput, TContext>
118
+ ): Router<
119
+ TRecord & {
120
+ [K in TName]: Procedure<"subscription", InferSchemaType<TSchema>, TOutput, TContext>;
121
+ },
122
+ TContext
123
+ >;
124
+ subscription<TName extends string, TOutput = any>(
125
+ name: TName,
126
+ options: ProcedureOptionsWithoutInput<TOutput, TContext>
127
+ ): Router<
128
+ TRecord & { [K in TName]: Procedure<"subscription", void, TOutput, TContext> },
129
+ TContext
130
+ >;
131
+
132
+ use<TNewContext = TContext>(
133
+ middleware: MiddlewareFn<TContext, TNewContext>
134
+ ): Router<TRecord, TNewContext>;
135
+
136
+ merge<TPrefix extends string, TSubRecord extends ProcedureRecord>(
137
+ prefix: TPrefix,
138
+ router: Router<TSubRecord, TContext>
139
+ ): Router<TRecord & { [K in TPrefix]: Router<TSubRecord, TContext> }, TContext>;
140
+ }
141
+
142
+ export interface CORSOptions {
143
+ origin?: string | string[] | ((origin: string) => boolean | string);
144
+ methods?: string[];
145
+ allowedHeaders?: string[];
146
+ exposedHeaders?: string[];
147
+ credentials?: boolean;
148
+ maxAge?: number;
149
+ }
150
+
151
+ export type DynamicHeadersFn = (
152
+ req: Request,
153
+ ctx: any
154
+ ) => Promise<Record<string, string>> | Record<string, string>;
155
+
156
+ export type HeadersConfig = Record<string, string> | DynamicHeadersFn;
157
+
158
+ export interface RouteRule {
159
+ /**
160
+ * Response headers to attach to all matching requests.
161
+ */
162
+ headers?: HeadersConfig;
163
+
164
+ /**
165
+ * Required request headers. If any are missing, automatically rejects with HTTP 401 Unauthorized.
166
+ */
167
+ requireHeaders?: string[];
168
+
169
+ /**
170
+ * Required query parameters. If missing on GET requests, rejects with HTTP 400 Bad Request.
171
+ */
172
+ requireParams?: string[];
173
+
174
+ /**
175
+ * Custom guard function for matching requests. Return false or throw RPCError to reject.
176
+ */
177
+ guard?: (opts: {
178
+ req: Request;
179
+ ctx: any;
180
+ path: string;
181
+ }) => Promise<boolean | void> | boolean | void;
182
+ }
183
+
184
+ export interface FetchHandlerOptions<
185
+ TRouter extends Router<any, any> = Router<any, any>,
186
+ TContext = any
187
+ > {
188
+ router: TRouter;
189
+ endpoint?: string;
190
+ createContext?: (opts: { req: Request; extra: any[] }) => Promise<TContext> | TContext;
191
+ onError?: (opts: { error: any; path?: string; req: Request }) => void;
192
+ cors?: boolean | CORSOptions;
193
+ securityHeaders?: boolean | Record<string, string>;
194
+
195
+ /**
196
+ * Declarative pattern rules matching URL paths or procedure names (e.g. "*", "/api/*", "admin.*").
197
+ * Supports wildcard matching with '*'.
198
+ */
199
+ rules?: Record<string, RouteRule> | Array<{ match: string } & RouteRule>;
200
+ }
201
+
202
+ export type ClientHeadersRule =
203
+ | Record<string, string>
204
+ | (() => Promise<Record<string, string>> | Record<string, string>);
205
+
206
+ export interface ClientOptions {
207
+ baseUrl: string;
208
+ fetch?: typeof fetch;
209
+ headers?:
210
+ | Record<string, string>
211
+ | Record<string, ClientHeadersRule>
212
+ | (() => Promise<Record<string, string>> | Record<string, string>);
213
+ }
214
+
215
+ export interface SubscriptionCallbacks<TData = any> {
216
+ onData: (data: TData) => void;
217
+ onError?: (error: any) => void;
218
+ onComplete?: () => void;
219
+ }
220
+
221
+ export interface SubscriptionHandle {
222
+ unsubscribe: () => void;
223
+ }
224
+
225
+ export type InferProcedureInput<P> =
226
+ P extends Procedure<any, infer TInput, any, any> ? TInput : void;
227
+
228
+ export type InferProcedureOutput<P> =
229
+ P extends Procedure<any, any, infer TOutput, any> ? TOutput : unknown;
230
+
231
+ export type ClientQueryMethod<P> =
232
+ InferProcedureInput<P> extends void | undefined
233
+ ? (
234
+ input?: void,
235
+ options?: { signal?: AbortSignal; headers?: Record<string, string> }
236
+ ) => Promise<InferProcedureOutput<P>>
237
+ : (
238
+ input: InferProcedureInput<P>,
239
+ options?: { signal?: AbortSignal; headers?: Record<string, string> }
240
+ ) => Promise<InferProcedureOutput<P>>;
241
+
242
+ export type ClientMutationMethod<P> =
243
+ InferProcedureInput<P> extends void | undefined
244
+ ? (
245
+ input?: void,
246
+ options?: { signal?: AbortSignal; headers?: Record<string, string> }
247
+ ) => Promise<InferProcedureOutput<P>>
248
+ : (
249
+ input: InferProcedureInput<P>,
250
+ options?: { signal?: AbortSignal; headers?: Record<string, string> }
251
+ ) => Promise<InferProcedureOutput<P>>;
252
+
253
+ export type ClientSubscriptionMethod<P> =
254
+ InferProcedureInput<P> extends void | undefined
255
+ ? (
256
+ input: void | undefined,
257
+ callbacks: SubscriptionCallbacks<InferProcedureOutput<P>>
258
+ ) => SubscriptionHandle
259
+ : (
260
+ input: InferProcedureInput<P>,
261
+ callbacks: SubscriptionCallbacks<InferProcedureOutput<P>>
262
+ ) => SubscriptionHandle;
263
+
264
+ export type ClientProcedure<P> =
265
+ P extends Procedure<"query", infer TInput, infer TOutput, any>
266
+ ? {
267
+ query: ClientQueryMethod<P>;
268
+ createQuery: (
269
+ input?: TInput | (() => TInput),
270
+ extra?: Partial<CreateQueryOptions<TOutput, any, TOutput, any>>
271
+ ) => QueryResult<TOutput, any>;
272
+ useQuery: (
273
+ input?: TInput | (() => TInput),
274
+ extra?: Partial<CreateQueryOptions<TOutput, any, TOutput, any>>
275
+ ) => QueryResult<TOutput, any>;
276
+ queryKey: (input?: TInput) => unknown[];
277
+ queryOptions: (
278
+ input?: TInput,
279
+ extra?: Partial<CreateQueryOptions<TOutput, any, TOutput, any>>
280
+ ) => CreateQueryOptions<TOutput, any, TOutput, any>;
281
+ }
282
+ : P extends Procedure<"mutation", infer TInput, infer TOutput, any>
283
+ ? {
284
+ mutate: ClientMutationMethod<P>;
285
+ createMutation: (
286
+ extra?: Partial<MutationOptions<TOutput, any, TInput, any>>
287
+ ) => MutationResult<TOutput, any, TInput, any>;
288
+ useMutation: (
289
+ extra?: Partial<MutationOptions<TOutput, any, TInput, any>>
290
+ ) => MutationResult<TOutput, any, TInput, any>;
291
+ mutationKey: () => unknown[];
292
+ mutationOptions: (
293
+ extra?: Partial<MutationOptions<TOutput, any, TInput, any>>
294
+ ) => MutationOptions<TOutput, any, TInput, any>;
295
+ }
296
+ : P extends Procedure<"subscription", any, any, any>
297
+ ? {
298
+ subscribe: ClientSubscriptionMethod<P>;
299
+ }
300
+ : never;
301
+
302
+ export type ClientRouter<R> =
303
+ R extends Router<infer TRecord, any>
304
+ ? {
305
+ [K in keyof TRecord]: TRecord[K] extends Procedure<any, any, any, any>
306
+ ? ClientProcedure<TRecord[K]>
307
+ : TRecord[K] extends Router<any, any>
308
+ ? ClientRouter<TRecord[K]>
309
+ : never;
310
+ }
311
+ : never;
312
+
313
+ export interface RPCResponseSuccess<T = any> {
314
+ result: {
315
+ data: T;
316
+ };
317
+ }
318
+
319
+ export interface RPCResponseError {
320
+ error: {
321
+ code: RPCErrorCode;
322
+ message: string;
323
+ issues?: ValidationIssue[];
324
+ data?: any;
325
+ };
326
+ }
327
+
328
+ export type RPCResponse<T = any> = RPCResponseSuccess<T> | RPCResponseError;
@@ -0,0 +1,21 @@
1
+ import { type Child, insertChild } from "../dom/render";
2
+ import { createRoot, onCleanup } from "../signals";
3
+
4
+ /**
5
+ * Hydrate server-rendered HTML by attaching fine-grained signal subscriptions and event listeners
6
+ */
7
+ export function hydrate(fn: () => Child, container: HTMLElement | DocumentFragment): () => void {
8
+ return createRoot((dispose) => {
9
+ // Adopt / mount fine-grained reactive tree into container
10
+ container.textContent = "";
11
+ const node = insertChild(container, fn());
12
+
13
+ onCleanup(() => {
14
+ if (node && node instanceof Node && node.parentNode === container) {
15
+ container.removeChild(node);
16
+ }
17
+ });
18
+
19
+ return dispose;
20
+ });
21
+ }
@@ -0,0 +1,10 @@
1
+ export { hydrate } from "./hydrate";
2
+
3
+ export {
4
+ renderToStream,
5
+ type StreamOptions
6
+ } from "./render-to-stream";
7
+ export {
8
+ renderChildToString,
9
+ renderToString
10
+ } from "./render-to-string";
@@ -0,0 +1,31 @@
1
+ import type { Child } from "../dom/render";
2
+ import { renderToString } from "./render-to-string";
3
+
4
+ export interface StreamOptions {
5
+ onChunk?: (chunk: string) => void;
6
+ onError?: (error: any) => void;
7
+ }
8
+
9
+ /**
10
+ * Stream HTML chunks progressively using standard Web Streams (ReadableStream)
11
+ */
12
+ export function renderToStream(
13
+ fn: () => Child,
14
+ options?: StreamOptions
15
+ ): ReadableStream<Uint8Array> {
16
+ const encoder = new TextEncoder();
17
+
18
+ return new ReadableStream<Uint8Array>({
19
+ start(controller) {
20
+ try {
21
+ const initialHtml = renderToString(fn);
22
+ options?.onChunk?.(initialHtml);
23
+ controller.enqueue(encoder.encode(initialHtml));
24
+ controller.close();
25
+ } catch (err) {
26
+ options?.onError?.(err);
27
+ controller.error(err);
28
+ }
29
+ }
30
+ });
31
+ }
@@ -0,0 +1,125 @@
1
+ import type { Child } from "../dom/render";
2
+ import { createRoot } from "../signals";
3
+
4
+ const VOID_ELEMENTS = new Set([
5
+ "area",
6
+ "base",
7
+ "br",
8
+ "col",
9
+ "embed",
10
+ "hr",
11
+ "img",
12
+ "input",
13
+ "link",
14
+ "meta",
15
+ "param",
16
+ "source",
17
+ "track",
18
+ "wbr"
19
+ ]);
20
+
21
+ function escapeHtml(str: string): string {
22
+ return str
23
+ .replace(/&/g, "&amp;")
24
+ .replace(/</g, "&lt;")
25
+ .replace(/>/g, "&gt;")
26
+ .replace(/"/g, "&quot;")
27
+ .replace(/'/g, "&#39;");
28
+ }
29
+
30
+ export function renderChildToString(child: Child): string {
31
+ if (child === null || child === undefined || child === false || child === true) {
32
+ return "";
33
+ }
34
+
35
+ if (typeof child === "function") {
36
+ return renderChildToString((child as () => any)());
37
+ }
38
+
39
+ if (Array.isArray(child)) {
40
+ return child.map((c) => renderChildToString(c)).join("");
41
+ }
42
+
43
+ if (typeof child === "object" && "nodeType" in child) {
44
+ if ((child as any).nodeType === 11 && (child as any).childNodes) {
45
+ return (child as any).childNodes.map((c: any) => renderChildToString(c)).join("");
46
+ }
47
+ return (child as any).outerHTML || (child as any).textContent || "";
48
+ }
49
+
50
+ return escapeHtml(String(child));
51
+ }
52
+
53
+ /**
54
+ * Synchronously render a Visage component tree to an HTML string
55
+ */
56
+ export function renderToString(fn: () => Child): string {
57
+ return createRoot(() => {
58
+ const result = fn();
59
+ return renderNodeToString(result);
60
+ });
61
+ }
62
+
63
+ function renderNodeToString(node: any): string {
64
+ if (node === null || node === undefined || node === false || node === true) {
65
+ return "";
66
+ }
67
+
68
+ if (typeof node === "function") {
69
+ return renderNodeToString(node());
70
+ }
71
+
72
+ if (Array.isArray(node)) {
73
+ return node.map((n) => renderNodeToString(n)).join("");
74
+ }
75
+
76
+ if (typeof node === "object" && node !== null) {
77
+ if (node.nodeType === 11) {
78
+ return (node.childNodes || []).map((c: any) => renderNodeToString(c)).join("");
79
+ }
80
+
81
+ if ("tagName" in node) {
82
+ const tag = String(node.tagName).toLowerCase();
83
+ const isVoid = VOID_ELEMENTS.has(tag);
84
+
85
+ let attrStr = "";
86
+ if (node.attributes instanceof Map) {
87
+ for (const [k, v] of node.attributes) {
88
+ attrStr += ` ${k}="${escapeHtml(String(v))}"`;
89
+ }
90
+ }
91
+
92
+ if (
93
+ node.style &&
94
+ typeof node.style === "object" &&
95
+ !(node.attributes instanceof Map && node.attributes.has("style"))
96
+ ) {
97
+ const styleEntries = Object.entries(node.style).filter(
98
+ ([_, v]) => typeof v === "string" && v.length > 0
99
+ );
100
+ if (styleEntries.length > 0) {
101
+ const styleStr = styleEntries
102
+ .map(
103
+ ([k, v]) =>
104
+ `${k.startsWith("--") ? k : k.replace(/([A-Z])/g, "-$1").toLowerCase()}: ${v}`
105
+ )
106
+ .join("; ");
107
+ attrStr += ` style="${escapeHtml(styleStr)}"`;
108
+ }
109
+ }
110
+
111
+ if (isVoid) {
112
+ return `<${tag}${attrStr} />`;
113
+ }
114
+
115
+ const inner = (node.childNodes || []).map((c: any) => renderNodeToString(c)).join("");
116
+ return `<${tag}${attrStr}>${inner}</${tag}>`;
117
+ }
118
+
119
+ if ("textContent" in node) {
120
+ return escapeHtml(node.textContent || "");
121
+ }
122
+ }
123
+
124
+ return escapeHtml(String(node));
125
+ }