@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,185 @@
1
+ import { For } from "../dom/control-flow";
2
+ import { computed, signal } from "../signals";
3
+ import { createVirtualizer } from "./virtualizer";
4
+
5
+ export interface VirtualListProps<T> {
6
+ each: T[] | (() => T[]);
7
+ itemHeight: number | ((index: number) => number);
8
+ height?: number | string;
9
+ width?: number | string;
10
+ overscan?: number;
11
+ class?: string;
12
+ style?: Record<string, any> | (() => Record<string, any>);
13
+ children: (item: T, index: () => number) => any;
14
+ }
15
+
16
+ export function VirtualList<T>(props: VirtualListProps<T>) {
17
+ const scrollElement = signal<HTMLElement | null>(null);
18
+
19
+ const getItems = (): T[] =>
20
+ typeof props.each === "function" ? (props.each as () => T[])() : props.each;
21
+ const count = computed(() => getItems().length);
22
+
23
+ const parsedHeight =
24
+ typeof props.height === "number"
25
+ ? props.height
26
+ : Number.parseInt(String(props.height || 400), 10) || 400;
27
+
28
+ const virtualizer = createVirtualizer({
29
+ count,
30
+ itemHeight: props.itemHeight,
31
+ overscan: props.overscan ?? 3,
32
+ initialSize: parsedHeight,
33
+ getScrollElement: () => scrollElement()
34
+ });
35
+
36
+ const containerHeight =
37
+ typeof props.height === "number" ? `${props.height}px` : (props.height ?? "400px");
38
+
39
+ const containerWidth =
40
+ typeof props.width === "number" ? `${props.width}px` : (props.width ?? "100%");
41
+
42
+ return (
43
+ <div
44
+ ref={(el: HTMLElement) => {
45
+ scrollElement.set(el);
46
+ }}
47
+ class={props.class}
48
+ style={() => {
49
+ const customStyle = typeof props.style === "function" ? props.style() : props.style || {};
50
+ return {
51
+ overflowY: "auto",
52
+ overflowX: "hidden",
53
+ position: "relative",
54
+ height: containerHeight,
55
+ width: containerWidth,
56
+ WebkitOverflowScrolling: "touch",
57
+ ...customStyle
58
+ };
59
+ }}
60
+ >
61
+ <div
62
+ style={() => ({
63
+ height: `${virtualizer.totalSize()}px`,
64
+ width: "100%",
65
+ position: "relative",
66
+ pointerEvents: "none"
67
+ })}
68
+ >
69
+ <For each={() => virtualizer.virtualItems()}>
70
+ {(vItem) => {
71
+ const itemData = computed(() => getItems()[vItem.index]);
72
+ return (
73
+ <div
74
+ style={() => ({
75
+ position: "absolute",
76
+ top: 0,
77
+ left: 0,
78
+ width: "100%",
79
+ height: `${vItem.size}px`,
80
+ transform: `translateY(${vItem.start}px)`,
81
+ pointerEvents: "auto",
82
+ willChange: "transform"
83
+ })}
84
+ >
85
+ {props.children(itemData() as T, () => vItem.index)}
86
+ </div>
87
+ );
88
+ }}
89
+ </For>
90
+ </div>
91
+ </div>
92
+ );
93
+ }
94
+
95
+ export interface VirtualGridProps<T> {
96
+ data: T[][] | (() => T[][]);
97
+ rowHeight: number;
98
+ columnWidth: number;
99
+ height?: number | string;
100
+ width?: number | string;
101
+ overscan?: number;
102
+ children: (item: T, rowIndex: number, colIndex: number) => any;
103
+ }
104
+
105
+ export function VirtualGrid<T>(props: VirtualGridProps<T>) {
106
+ const scrollElement = signal<HTMLElement | null>(null);
107
+ const getData = (): T[][] =>
108
+ typeof props.data === "function" ? (props.data as () => T[][])() : props.data;
109
+
110
+ const rowCount = computed(() => getData().length);
111
+ const colCount = computed(() => {
112
+ const d = getData();
113
+ return d[0] ? d[0].length : 0;
114
+ });
115
+
116
+ const rowVirtualizer = createVirtualizer({
117
+ count: rowCount,
118
+ itemHeight: props.rowHeight,
119
+ overscan: props.overscan ?? 3,
120
+ getScrollElement: () => scrollElement()
121
+ });
122
+
123
+ const colVirtualizer = createVirtualizer({
124
+ count: colCount,
125
+ itemHeight: props.columnWidth,
126
+ overscan: props.overscan ?? 3,
127
+ horizontal: true,
128
+ getScrollElement: () => scrollElement()
129
+ });
130
+
131
+ const containerHeight =
132
+ typeof props.height === "number" ? `${props.height}px` : (props.height ?? "400px");
133
+
134
+ const containerWidth =
135
+ typeof props.width === "number" ? `${props.width}px` : (props.width ?? "100%");
136
+
137
+ return (
138
+ <div
139
+ ref={(el: HTMLElement) => {
140
+ scrollElement.set(el);
141
+ }}
142
+ style={{
143
+ overflow: "auto",
144
+ position: "relative",
145
+ height: containerHeight,
146
+ width: containerWidth,
147
+ WebkitOverflowScrolling: "touch"
148
+ }}
149
+ >
150
+ <div
151
+ style={() => ({
152
+ height: `${rowVirtualizer.totalSize()}px`,
153
+ width: `${colVirtualizer.totalSize()}px`,
154
+ position: "relative"
155
+ })}
156
+ >
157
+ <For each={() => rowVirtualizer.virtualItems()}>
158
+ {(r) => (
159
+ <For each={() => colVirtualizer.virtualItems()}>
160
+ {(c) => {
161
+ const rowData = getData()[r.index];
162
+ const item = rowData ? rowData[c.index] : undefined;
163
+ return (
164
+ <div
165
+ style={() => ({
166
+ position: "absolute",
167
+ top: 0,
168
+ left: 0,
169
+ width: `${c.size}px`,
170
+ height: `${r.size}px`,
171
+ transform: `translate(${c.start}px, ${r.start}px)`,
172
+ willChange: "transform"
173
+ })}
174
+ >
175
+ {props.children(item as T, r.index, c.index)}
176
+ </div>
177
+ );
178
+ }}
179
+ </For>
180
+ )}
181
+ </For>
182
+ </div>
183
+ </div>
184
+ );
185
+ }
@@ -0,0 +1,274 @@
1
+ import { computed, effect, onCleanup, type ReadonlySignal, signal } from "../signals";
2
+
3
+ export interface VirtualItem {
4
+ index: number;
5
+ start: number;
6
+ size: number;
7
+ end: number;
8
+ key: number | string;
9
+ }
10
+
11
+ export interface VirtualizerOptions {
12
+ count: number | (() => number);
13
+ itemHeight: number | ((index: number) => number);
14
+ getScrollElement: () => HTMLElement | null;
15
+ overscan?: number;
16
+ horizontal?: boolean;
17
+ initialSize?: number;
18
+ estimateSize?: (index: number) => number;
19
+ }
20
+
21
+ export interface Virtualizer {
22
+ virtualItems: ReadonlySignal<VirtualItem[]>;
23
+ totalSize: ReadonlySignal<number>;
24
+ scrollOffset: ReadonlySignal<number>;
25
+ containerSize: ReadonlySignal<number>;
26
+ startIndex: ReadonlySignal<number>;
27
+ endIndex: ReadonlySignal<number>;
28
+ scrollToIndex: (index: number, options?: { align?: "start" | "center" | "end" | "auto" }) => void;
29
+ scrollToOffset: (offset: number) => void;
30
+ measureElement: (element: HTMLElement | null, index: number) => void;
31
+ }
32
+
33
+ export function createVirtualizer(options: VirtualizerOptions): Virtualizer {
34
+ const overscan = options.overscan ?? 3;
35
+ const isHorizontal = Boolean(options.horizontal);
36
+
37
+ const getCount = (): number =>
38
+ typeof options.count === "function"
39
+ ? (options.count as () => number)()
40
+ : (options.count as number);
41
+
42
+ const getItemHeight =
43
+ typeof options.itemHeight === "function"
44
+ ? options.itemHeight
45
+ : () => options.itemHeight as number;
46
+
47
+ const scrollOffset = signal(0);
48
+ const containerSize = signal(options.initialSize ?? 400);
49
+
50
+ // Dynamic measurement cache for variable height rows
51
+ const dynamicSizeCache = new Map<number, number>();
52
+
53
+ const getItemSize = (index: number): number => {
54
+ const cached = dynamicSizeCache.get(index);
55
+ if (cached !== undefined) return cached;
56
+ return getItemHeight(index);
57
+ };
58
+
59
+ // Compute prefix offsets for fast range calculation
60
+ const totalSize = computed(() => {
61
+ const count = getCount();
62
+ if (typeof options.itemHeight === "number" && dynamicSizeCache.size === 0) {
63
+ return count * options.itemHeight;
64
+ }
65
+ let total = 0;
66
+ for (let i = 0; i < count; i++) {
67
+ total += getItemSize(i);
68
+ }
69
+ return total;
70
+ });
71
+
72
+ const getOffsetForIndex = (targetIndex: number): number => {
73
+ if (typeof options.itemHeight === "number" && dynamicSizeCache.size === 0) {
74
+ return targetIndex * options.itemHeight;
75
+ }
76
+ let offset = 0;
77
+ for (let i = 0; i < targetIndex; i++) {
78
+ offset += getItemSize(i);
79
+ }
80
+ return offset;
81
+ };
82
+
83
+ const findIndexAtOffset = (offset: number): number => {
84
+ const count = getCount();
85
+ if (count === 0) return 0;
86
+
87
+ if (typeof options.itemHeight === "number" && dynamicSizeCache.size === 0) {
88
+ const idx = Math.floor(offset / options.itemHeight);
89
+ return Math.max(0, Math.min(count - 1, idx));
90
+ }
91
+
92
+ let low = 0;
93
+ let high = count - 1;
94
+
95
+ while (low <= high) {
96
+ const mid = (low + high) >> 1;
97
+ const midStart = getOffsetForIndex(mid);
98
+ const midEnd = midStart + getItemSize(mid);
99
+
100
+ if (offset < midStart) {
101
+ high = mid - 1;
102
+ } else if (offset >= midEnd) {
103
+ low = mid + 1;
104
+ } else {
105
+ return mid;
106
+ }
107
+ }
108
+
109
+ return Math.max(0, Math.min(count - 1, low));
110
+ };
111
+
112
+ const startIndex = computed(() => {
113
+ const offset = scrollOffset();
114
+ const rawStart = findIndexAtOffset(offset);
115
+ return Math.max(0, rawStart - overscan);
116
+ });
117
+
118
+ const endIndex = computed(() => {
119
+ const count = getCount();
120
+ if (count === 0) return 0;
121
+ const endOffset = scrollOffset() + (containerSize() || 500);
122
+ const rawEnd = findIndexAtOffset(endOffset);
123
+ return Math.min(count - 1, rawEnd + overscan);
124
+ });
125
+
126
+ const itemCache = new Map<number, VirtualItem>();
127
+
128
+ const virtualItems = computed(() => {
129
+ const count = getCount();
130
+ if (count === 0) return [];
131
+
132
+ const start = startIndex();
133
+ const end = endIndex();
134
+ const items: VirtualItem[] = [];
135
+
136
+ let currentStart = getOffsetForIndex(start);
137
+
138
+ for (let i = start; i <= end; i++) {
139
+ const size = getItemSize(i);
140
+ let item = itemCache.get(i);
141
+ if (!item || item.start !== currentStart || item.size !== size) {
142
+ item = {
143
+ index: i,
144
+ start: currentStart,
145
+ size,
146
+ end: currentStart + size,
147
+ key: i
148
+ };
149
+ itemCache.set(i, item);
150
+ }
151
+ items.push(item);
152
+ currentStart += size;
153
+ }
154
+
155
+ return items;
156
+ });
157
+
158
+ // Setup DOM event listeners and ResizeObserver
159
+ effect(() => {
160
+ const el = options.getScrollElement();
161
+ if (!el || typeof el.addEventListener !== "function") return;
162
+
163
+ const handleScroll = () => {
164
+ const offset = isHorizontal ? el.scrollLeft : el.scrollTop;
165
+ scrollOffset.set(offset);
166
+ };
167
+
168
+ const handleResize = () => {
169
+ const size = isHorizontal ? el.clientWidth : el.clientHeight;
170
+ if (size > 0) {
171
+ containerSize.set(size);
172
+ }
173
+ };
174
+
175
+ // Initial sync
176
+ handleResize();
177
+ handleScroll();
178
+
179
+ el.addEventListener("scroll", handleScroll, { passive: true });
180
+
181
+ let resizeObserver: any;
182
+ if (typeof ResizeObserver !== "undefined") {
183
+ resizeObserver = new ResizeObserver(() => {
184
+ handleResize();
185
+ });
186
+ resizeObserver.observe(el);
187
+ }
188
+
189
+ onCleanup(() => {
190
+ el.removeEventListener("scroll", handleScroll);
191
+ if (resizeObserver) {
192
+ resizeObserver.disconnect();
193
+ }
194
+ });
195
+ });
196
+
197
+ const scrollToIndex = (
198
+ index: number,
199
+ scrollOpts?: { align?: "start" | "center" | "end" | "auto" }
200
+ ) => {
201
+ const el = options.getScrollElement();
202
+ if (!el) return;
203
+
204
+ const align = scrollOpts?.align ?? "auto";
205
+ const itemStart = getOffsetForIndex(index);
206
+ const itemSize = getItemSize(index);
207
+ const itemEnd = itemStart + itemSize;
208
+ const currentScroll = scrollOffset();
209
+ const cSize = containerSize();
210
+
211
+ let targetOffset = itemStart;
212
+
213
+ if (align === "start") {
214
+ targetOffset = itemStart;
215
+ } else if (align === "end") {
216
+ targetOffset = itemEnd - cSize;
217
+ } else if (align === "center") {
218
+ targetOffset = itemStart - (cSize - itemSize) / 2;
219
+ } else {
220
+ // auto
221
+ if (itemStart < currentScroll) {
222
+ targetOffset = itemStart;
223
+ } else if (itemEnd > currentScroll + cSize) {
224
+ targetOffset = itemEnd - cSize;
225
+ } else {
226
+ return; // already fully in view
227
+ }
228
+ }
229
+
230
+ targetOffset = Math.max(0, targetOffset);
231
+ if (isHorizontal) {
232
+ el.scrollLeft = targetOffset;
233
+ } else {
234
+ el.scrollTop = targetOffset;
235
+ }
236
+ scrollOffset.set(targetOffset);
237
+ };
238
+
239
+ const scrollToOffset = (offset: number) => {
240
+ const el = options.getScrollElement();
241
+ if (el) {
242
+ if (isHorizontal) {
243
+ el.scrollLeft = offset;
244
+ } else {
245
+ el.scrollTop = offset;
246
+ }
247
+ }
248
+ scrollOffset.set(offset);
249
+ };
250
+
251
+ const measureElement = (element: HTMLElement | null, index: number) => {
252
+ if (!element) return;
253
+ const measuredSize = isHorizontal
254
+ ? element.getBoundingClientRect().width
255
+ : element.getBoundingClientRect().height;
256
+ if (measuredSize > 0 && dynamicSizeCache.get(index) !== measuredSize) {
257
+ dynamicSizeCache.set(index, measuredSize);
258
+ // Force recalculation by touching scrollOffset
259
+ scrollOffset.update((o) => o);
260
+ }
261
+ };
262
+
263
+ return {
264
+ virtualItems,
265
+ totalSize,
266
+ scrollOffset,
267
+ containerSize,
268
+ startIndex,
269
+ endIndex,
270
+ scrollToIndex,
271
+ scrollToOffset,
272
+ measureElement
273
+ };
274
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Transform JSX/TSX modules to attach Hot Module Replacement (HMR) acceptance
3
+ */
4
+ export function transformHmr(code: string, id: string): { code: string; map?: any } | null {
5
+ // Only target user code JSX/TSX files
6
+ if (!/\.[jt]sx$/.test(id) || id.includes("node_modules")) {
7
+ return null;
8
+ }
9
+
10
+ // Check if file exports any functions or components
11
+ const hasExports =
12
+ /\bexport\s+(?:default\s+)?(?:function|const|class)\s+([A-Z][A-Za-z0-9_]*)/.test(code) ||
13
+ /\bexport\s+\{\s*([A-Za-z0-9_,\s]+)\s*\}/.test(code);
14
+
15
+ if (!hasExports) {
16
+ return null;
17
+ }
18
+
19
+ // Append lightweight HMR boundary
20
+ const hmrFooter = `
21
+ /* @visage/vite-hmr */
22
+ if (import.meta.hot) {
23
+ import.meta.hot.accept((newMod) => {
24
+ if (newMod) {
25
+ // Re-trigger reactive components without wiping signal stores
26
+ if (typeof window !== "undefined" && window.__VISAGE_DEVTOOLS__) {
27
+ window.__VISAGE_DEVTOOLS__.refresh?.();
28
+ }
29
+ }
30
+ });
31
+ }
32
+ `;
33
+
34
+ return {
35
+ code: `${code}\n;${hmrFooter}`,
36
+ map: null
37
+ };
38
+ }
@@ -0,0 +1,3 @@
1
+ export { type VisageLucideOptions, visageLucide } from "./lucide";
2
+ export { default, visage } from "./plugin";
3
+ export type { VisagePluginOptions, VisageVitePlugin } from "./types";
@@ -0,0 +1,124 @@
1
+ import type { VisageVitePlugin } from "./types";
2
+
3
+ export interface VisageLucideOptions {
4
+ /**
5
+ * Module aliases to intercept and translate.
6
+ * Defaults to `["lucide-react"]`.
7
+ */
8
+ aliases?: string[];
9
+ }
10
+
11
+ interface ParsedImport {
12
+ raw: string;
13
+ importedName: string;
14
+ localName: string;
15
+ }
16
+
17
+ function parseNamedImports(importClause: string): ParsedImport[] {
18
+ return importClause
19
+ .split(",")
20
+ .map((s) => s.trim())
21
+ .filter(Boolean)
22
+ .map((item) => {
23
+ const parts = item.split(/\s+as\s+/).map((p) => p.trim());
24
+ const importedName = parts[0]!;
25
+ const localName = parts[1] ?? importedName;
26
+ return {
27
+ raw: item,
28
+ importedName,
29
+ localName
30
+ };
31
+ });
32
+ }
33
+
34
+ /**
35
+ * Official Visage Lucide Vite Plugin
36
+ *
37
+ * Statically transforms `import { Icon } from "lucide-react"` at build time into
38
+ * native, tree-shaken Visage SVG component instances using `createIcon` and `lucide`.
39
+ *
40
+ * Example:
41
+ * ```ts
42
+ * // vite.config.ts
43
+ * import { defineConfig } from "vite";
44
+ * import visage, { visageLucide } from "visage/vite";
45
+ *
46
+ * export default defineConfig({
47
+ * plugins: [visage(), visageLucide()]
48
+ * });
49
+ * ```
50
+ */
51
+ export function visageLucide(options: VisageLucideOptions = {}): VisageVitePlugin {
52
+ const aliases = options.aliases ?? ["lucide-react"];
53
+
54
+ return {
55
+ name: "visage:lucide",
56
+ enforce: "pre",
57
+
58
+ config() {
59
+ return {
60
+ optimizeDeps: {
61
+ exclude: aliases
62
+ }
63
+ };
64
+ },
65
+
66
+ resolveId(id: string) {
67
+ if (aliases.includes(id)) {
68
+ return `\0visage-lucide:${id}`;
69
+ }
70
+ return null;
71
+ },
72
+
73
+ load(id: string) {
74
+ if (id.startsWith("\0visage-lucide:")) {
75
+ return "export default {};";
76
+ }
77
+ return null;
78
+ },
79
+
80
+ transform(code: string, id: string) {
81
+ if (!id.match(/\.[jt]sx?$/) || id.includes("node_modules")) {
82
+ return null;
83
+ }
84
+
85
+ let transformed = code;
86
+ let hasMatches = false;
87
+
88
+ for (const alias of aliases) {
89
+ // Regex to match: import { ... } from "lucide-react" or 'lucide-react'
90
+ const regex = new RegExp(`import\\s*\\{([^}]+)\\}\\s*from\\s*['"]${alias}['"];?`, "g");
91
+
92
+ transformed = transformed.replace(regex, (_match, importClause) => {
93
+ const imports = parseNamedImports(importClause);
94
+ if (imports.length === 0) return "";
95
+
96
+ hasMatches = true;
97
+
98
+ const lucideNamedImports = imports
99
+ .map((imp) => `${imp.importedName} as __icon_node_${imp.importedName}`)
100
+ .join(", ");
101
+
102
+ const componentDeclarations = imports
103
+ .map((imp) => {
104
+ return `const ${imp.localName} = /* @__PURE__ */ __visageCreateIcon("${imp.importedName}", __icon_node_${imp.importedName});`;
105
+ })
106
+ .join("\n");
107
+
108
+ return `import { createIcon as __visageCreateIcon } from "visage/ui";\nimport { ${lucideNamedImports} } from "lucide";\n${componentDeclarations}`;
109
+ });
110
+ }
111
+
112
+ if (!hasMatches) {
113
+ return null;
114
+ }
115
+
116
+ return {
117
+ code: transformed,
118
+ map: null
119
+ };
120
+ }
121
+ };
122
+ }
123
+
124
+ export default visageLucide;