tempest-react-sdk 0.25.0 → 0.26.1

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 (322) hide show
  1. package/README.md +14 -1
  2. package/bin/lib/alias/collect.mjs +94 -0
  3. package/bin/lib/alias/collect.test.mjs +95 -0
  4. package/bin/lib/alias/discover.mjs +77 -0
  5. package/bin/lib/alias/discover.test.mjs +189 -0
  6. package/bin/lib/alias/index.mjs +88 -0
  7. package/bin/lib/alias/index.test.mjs +138 -0
  8. package/bin/lib/alias/rewrite-css.mjs +76 -0
  9. package/bin/lib/alias/rewrite-css.test.mjs +85 -0
  10. package/bin/lib/alias/rewrite.mjs +141 -0
  11. package/bin/lib/alias/rewrite.test.mjs +148 -0
  12. package/bin/lib/alias/specifier.mjs +49 -0
  13. package/bin/lib/alias/specifier.test.mjs +85 -0
  14. package/bin/lib/alias/tsconfig.mjs +116 -0
  15. package/bin/lib/alias/typescript.mjs +23 -0
  16. package/bin/lib/doctor/lucide.mjs +74 -0
  17. package/bin/lib/doctor/lucide.test.mjs +107 -0
  18. package/bin/lib/icons/generate.mjs +156 -0
  19. package/bin/lib/icons/generate.test.mjs +125 -0
  20. package/bin/tempest.mjs +174 -13
  21. package/dist/audio/use-audio.cjs +1 -1
  22. package/dist/audio/use-audio.cjs.map +1 -1
  23. package/dist/audio/use-audio.js +12 -12
  24. package/dist/audio/use-audio.js.map +1 -1
  25. package/dist/charts/palette.cjs +1 -1
  26. package/dist/charts/palette.cjs.map +1 -1
  27. package/dist/charts/palette.js +2 -2
  28. package/dist/charts/palette.js.map +1 -1
  29. package/dist/charts.d.ts +5 -3
  30. package/dist/components/ClickOutside/ClickOutside.cjs +1 -1
  31. package/dist/components/ClickOutside/ClickOutside.cjs.map +1 -1
  32. package/dist/components/ClickOutside/ClickOutside.js +3 -1
  33. package/dist/components/ClickOutside/ClickOutside.js.map +1 -1
  34. package/dist/components/DataTable/DataTable.cjs +1 -1
  35. package/dist/components/DataTable/DataTable.cjs.map +1 -1
  36. package/dist/components/DataTable/DataTable.js +21 -23
  37. package/dist/components/DataTable/DataTable.js.map +1 -1
  38. package/dist/components/Kanban/Kanban.cjs +2 -0
  39. package/dist/components/Kanban/Kanban.cjs.map +1 -0
  40. package/dist/components/Kanban/Kanban.js +102 -0
  41. package/dist/components/Kanban/Kanban.js.map +1 -0
  42. package/dist/components/Kanban/Kanban.module.cjs +2 -0
  43. package/dist/components/Kanban/Kanban.module.cjs.map +1 -0
  44. package/dist/components/Kanban/Kanban.module.js +19 -0
  45. package/dist/components/Kanban/Kanban.module.js.map +1 -0
  46. package/dist/components/NotificationCenter/NotificationCenter.cjs +2 -0
  47. package/dist/components/NotificationCenter/NotificationCenter.cjs.map +1 -0
  48. package/dist/components/NotificationCenter/NotificationCenter.js +83 -0
  49. package/dist/components/NotificationCenter/NotificationCenter.js.map +1 -0
  50. package/dist/components/NotificationCenter/NotificationCenter.module.cjs +2 -0
  51. package/dist/components/NotificationCenter/NotificationCenter.module.cjs.map +1 -0
  52. package/dist/components/NotificationCenter/NotificationCenter.module.js +21 -0
  53. package/dist/components/NotificationCenter/NotificationCenter.module.js.map +1 -0
  54. package/dist/components/NotificationCenter/use-notification-inbox.cjs +2 -0
  55. package/dist/components/NotificationCenter/use-notification-inbox.cjs.map +1 -0
  56. package/dist/components/NotificationCenter/use-notification-inbox.js +63 -0
  57. package/dist/components/NotificationCenter/use-notification-inbox.js.map +1 -0
  58. package/dist/components/RefreshIndicator/RefreshIndicator.cjs +1 -1
  59. package/dist/components/RefreshIndicator/RefreshIndicator.cjs.map +1 -1
  60. package/dist/components/RefreshIndicator/RefreshIndicator.js +17 -17
  61. package/dist/components/RefreshIndicator/RefreshIndicator.js.map +1 -1
  62. package/dist/components/Tooltip/Tooltip.cjs +1 -1
  63. package/dist/components/Tooltip/Tooltip.cjs.map +1 -1
  64. package/dist/components/Tooltip/Tooltip.js +26 -20
  65. package/dist/components/Tooltip/Tooltip.js.map +1 -1
  66. package/dist/components/VirtualTable/VirtualTable.cjs +2 -0
  67. package/dist/components/VirtualTable/VirtualTable.cjs.map +1 -0
  68. package/dist/components/VirtualTable/VirtualTable.js +116 -0
  69. package/dist/components/VirtualTable/VirtualTable.js.map +1 -0
  70. package/dist/components/VirtualTable/VirtualTable.module.cjs +2 -0
  71. package/dist/components/VirtualTable/VirtualTable.module.cjs.map +1 -0
  72. package/dist/components/VirtualTable/VirtualTable.module.js +20 -0
  73. package/dist/components/VirtualTable/VirtualTable.module.js.map +1 -0
  74. package/dist/components/Wizard/Wizard.cjs +1 -1
  75. package/dist/components/Wizard/Wizard.cjs.map +1 -1
  76. package/dist/components/Wizard/Wizard.js +53 -53
  77. package/dist/components/Wizard/Wizard.js.map +1 -1
  78. package/dist/geo/use-position-tracker.cjs +1 -1
  79. package/dist/geo/use-position-tracker.cjs.map +1 -1
  80. package/dist/geo/use-position-tracker.js +3 -1
  81. package/dist/geo/use-position-tracker.js.map +1 -1
  82. package/dist/hooks/use-async.cjs +1 -1
  83. package/dist/hooks/use-async.cjs.map +1 -1
  84. package/dist/hooks/use-async.js +3 -1
  85. package/dist/hooks/use-async.js.map +1 -1
  86. package/dist/hooks/use-click-outside.cjs +1 -1
  87. package/dist/hooks/use-click-outside.cjs.map +1 -1
  88. package/dist/hooks/use-click-outside.js +3 -1
  89. package/dist/hooks/use-click-outside.js.map +1 -1
  90. package/dist/hooks/use-deep-memo.cjs.map +1 -1
  91. package/dist/hooks/use-deep-memo.js.map +1 -1
  92. package/dist/hooks/use-event-listener.cjs +1 -1
  93. package/dist/hooks/use-event-listener.cjs.map +1 -1
  94. package/dist/hooks/use-event-listener.js +3 -1
  95. package/dist/hooks/use-event-listener.js.map +1 -1
  96. package/dist/hooks/use-interval.cjs +1 -1
  97. package/dist/hooks/use-interval.cjs.map +1 -1
  98. package/dist/hooks/use-interval.js +3 -1
  99. package/dist/hooks/use-interval.js.map +1 -1
  100. package/dist/hooks/use-is-first-render.cjs.map +1 -1
  101. package/dist/hooks/use-is-first-render.js.map +1 -1
  102. package/dist/hooks/use-long-press-handlers.cjs +1 -1
  103. package/dist/hooks/use-long-press-handlers.cjs.map +1 -1
  104. package/dist/hooks/use-long-press-handlers.js +25 -23
  105. package/dist/hooks/use-long-press-handlers.js.map +1 -1
  106. package/dist/hooks/use-long-press.cjs +1 -1
  107. package/dist/hooks/use-long-press.cjs.map +1 -1
  108. package/dist/hooks/use-long-press.js +3 -1
  109. package/dist/hooks/use-long-press.js.map +1 -1
  110. package/dist/hooks/use-online.cjs +1 -1
  111. package/dist/hooks/use-online.cjs.map +1 -1
  112. package/dist/hooks/use-online.js +6 -4
  113. package/dist/hooks/use-online.js.map +1 -1
  114. package/dist/hooks/use-previous.cjs.map +1 -1
  115. package/dist/hooks/use-previous.js.map +1 -1
  116. package/dist/hooks/use-service-worker-update.cjs +1 -1
  117. package/dist/hooks/use-service-worker-update.cjs.map +1 -1
  118. package/dist/hooks/use-service-worker-update.js +3 -1
  119. package/dist/hooks/use-service-worker-update.js.map +1 -1
  120. package/dist/hooks/use-sortable.cjs +2 -0
  121. package/dist/hooks/use-sortable.cjs.map +1 -0
  122. package/dist/hooks/use-sortable.js +169 -0
  123. package/dist/hooks/use-sortable.js.map +1 -0
  124. package/dist/hooks/use-stable-callback.cjs.map +1 -1
  125. package/dist/hooks/use-stable-callback.js.map +1 -1
  126. package/dist/hooks/use-timeout.cjs +1 -1
  127. package/dist/hooks/use-timeout.cjs.map +1 -1
  128. package/dist/hooks/use-timeout.js +3 -1
  129. package/dist/hooks/use-timeout.js.map +1 -1
  130. package/dist/icons/Icon.cjs +2 -0
  131. package/dist/icons/Icon.cjs.map +1 -0
  132. package/dist/icons/Icon.js +24 -0
  133. package/dist/icons/Icon.js.map +1 -0
  134. package/dist/icons/IconProvider.cjs +2 -0
  135. package/dist/icons/IconProvider.cjs.map +1 -0
  136. package/dist/icons/IconProvider.js +24 -0
  137. package/dist/icons/IconProvider.js.map +1 -0
  138. package/dist/icons/generated/aliases.cjs +2 -0
  139. package/dist/icons/generated/aliases.cjs.map +1 -0
  140. package/dist/icons/generated/aliases.js +255 -0
  141. package/dist/icons/generated/aliases.js.map +1 -0
  142. package/dist/icons/generated/icon-names.cjs +2 -0
  143. package/dist/icons/generated/icon-names.cjs.map +1 -0
  144. package/dist/icons/generated/icon-names.js +6 -0
  145. package/dist/icons/generated/icon-names.js.map +1 -0
  146. package/dist/icons/generated/loaders.cjs +2 -0
  147. package/dist/icons/generated/loaders.cjs.map +1 -0
  148. package/dist/icons/generated/loaders.js +32 -0
  149. package/dist/icons/generated/loaders.js.map +1 -0
  150. package/dist/icons/generated/shard-a.cjs +2 -0
  151. package/dist/icons/generated/shard-a.cjs.map +1 -0
  152. package/dist/icons/generated/shard-a.js +114 -0
  153. package/dist/icons/generated/shard-a.js.map +1 -0
  154. package/dist/icons/generated/shard-b.cjs +2 -0
  155. package/dist/icons/generated/shard-b.cjs.map +1 -0
  156. package/dist/icons/generated/shard-b.js +156 -0
  157. package/dist/icons/generated/shard-b.js.map +1 -0
  158. package/dist/icons/generated/shard-c.cjs +2 -0
  159. package/dist/icons/generated/shard-c.cjs.map +1 -0
  160. package/dist/icons/generated/shard-c.js +269 -0
  161. package/dist/icons/generated/shard-c.js.map +1 -0
  162. package/dist/icons/generated/shard-d.cjs +2 -0
  163. package/dist/icons/generated/shard-d.cjs.map +1 -0
  164. package/dist/icons/generated/shard-d.js +62 -0
  165. package/dist/icons/generated/shard-d.js.map +1 -0
  166. package/dist/icons/generated/shard-e.cjs +2 -0
  167. package/dist/icons/generated/shard-e.cjs.map +1 -0
  168. package/dist/icons/generated/shard-e.js +32 -0
  169. package/dist/icons/generated/shard-e.js.map +1 -0
  170. package/dist/icons/generated/shard-f.cjs +2 -0
  171. package/dist/icons/generated/shard-f.cjs.map +1 -0
  172. package/dist/icons/generated/shard-f.js +139 -0
  173. package/dist/icons/generated/shard-f.js.map +1 -0
  174. package/dist/icons/generated/shard-g.cjs +2 -0
  175. package/dist/icons/generated/shard-g.cjs.map +1 -0
  176. package/dist/icons/generated/shard-g.js +61 -0
  177. package/dist/icons/generated/shard-g.js.map +1 -0
  178. package/dist/icons/generated/shard-h.cjs +2 -0
  179. package/dist/icons/generated/shard-h.cjs.map +1 -0
  180. package/dist/icons/generated/shard-h.js +63 -0
  181. package/dist/icons/generated/shard-h.js.map +1 -0
  182. package/dist/icons/generated/shard-i.cjs +2 -0
  183. package/dist/icons/generated/shard-i.cjs.map +1 -0
  184. package/dist/icons/generated/shard-i.js +30 -0
  185. package/dist/icons/generated/shard-i.js.map +1 -0
  186. package/dist/icons/generated/shard-j.cjs +2 -0
  187. package/dist/icons/generated/shard-j.cjs.map +1 -0
  188. package/dist/icons/generated/shard-j.js +10 -0
  189. package/dist/icons/generated/shard-j.js.map +1 -0
  190. package/dist/icons/generated/shard-k.cjs +2 -0
  191. package/dist/icons/generated/shard-k.cjs.map +1 -0
  192. package/dist/icons/generated/shard-k.js +16 -0
  193. package/dist/icons/generated/shard-k.js.map +1 -0
  194. package/dist/icons/generated/shard-l.cjs +2 -0
  195. package/dist/icons/generated/shard-l.cjs.map +1 -0
  196. package/dist/icons/generated/shard-l.js +88 -0
  197. package/dist/icons/generated/shard-l.js.map +1 -0
  198. package/dist/icons/generated/shard-m.cjs +2 -0
  199. package/dist/icons/generated/shard-m.cjs.map +1 -0
  200. package/dist/icons/generated/shard-m.js +140 -0
  201. package/dist/icons/generated/shard-m.js.map +1 -0
  202. package/dist/icons/generated/shard-n.cjs +2 -0
  203. package/dist/icons/generated/shard-n.cjs.map +1 -0
  204. package/dist/icons/generated/shard-n.js +24 -0
  205. package/dist/icons/generated/shard-n.js.map +1 -0
  206. package/dist/icons/generated/shard-o.cjs +2 -0
  207. package/dist/icons/generated/shard-o.cjs.map +1 -0
  208. package/dist/icons/generated/shard-o.js +17 -0
  209. package/dist/icons/generated/shard-o.js.map +1 -0
  210. package/dist/icons/generated/shard-p.cjs +2 -0
  211. package/dist/icons/generated/shard-p.cjs.map +1 -0
  212. package/dist/icons/generated/shard-p.js +115 -0
  213. package/dist/icons/generated/shard-p.js.map +1 -0
  214. package/dist/icons/generated/shard-q.cjs +2 -0
  215. package/dist/icons/generated/shard-q.cjs.map +1 -0
  216. package/dist/icons/generated/shard-q.js +10 -0
  217. package/dist/icons/generated/shard-q.js.map +1 -0
  218. package/dist/icons/generated/shard-r.cjs +2 -0
  219. package/dist/icons/generated/shard-r.cjs.map +1 -0
  220. package/dist/icons/generated/shard-r.js +77 -0
  221. package/dist/icons/generated/shard-r.js.map +1 -0
  222. package/dist/icons/generated/shard-s.cjs +2 -0
  223. package/dist/icons/generated/shard-s.cjs.map +1 -0
  224. package/dist/icons/generated/shard-s.js +255 -0
  225. package/dist/icons/generated/shard-s.js.map +1 -0
  226. package/dist/icons/generated/shard-t.cjs +2 -0
  227. package/dist/icons/generated/shard-t.cjs.map +1 -0
  228. package/dist/icons/generated/shard-t.js +109 -0
  229. package/dist/icons/generated/shard-t.js.map +1 -0
  230. package/dist/icons/generated/shard-u.cjs +2 -0
  231. package/dist/icons/generated/shard-u.cjs.map +1 -0
  232. package/dist/icons/generated/shard-u.js +49 -0
  233. package/dist/icons/generated/shard-u.js.map +1 -0
  234. package/dist/icons/generated/shard-v.cjs +2 -0
  235. package/dist/icons/generated/shard-v.cjs.map +1 -0
  236. package/dist/icons/generated/shard-v.js +30 -0
  237. package/dist/icons/generated/shard-v.js.map +1 -0
  238. package/dist/icons/generated/shard-w.cjs +2 -0
  239. package/dist/icons/generated/shard-w.cjs.map +1 -0
  240. package/dist/icons/generated/shard-w.js +48 -0
  241. package/dist/icons/generated/shard-w.js.map +1 -0
  242. package/dist/icons/generated/shard-x.cjs +2 -0
  243. package/dist/icons/generated/shard-x.cjs.map +1 -0
  244. package/dist/icons/generated/shard-x.js +10 -0
  245. package/dist/icons/generated/shard-x.js.map +1 -0
  246. package/dist/icons/generated/shard-z.cjs +2 -0
  247. package/dist/icons/generated/shard-z.cjs.map +1 -0
  248. package/dist/icons/generated/shard-z.js +25 -0
  249. package/dist/icons/generated/shard-z.js.map +1 -0
  250. package/dist/icons/icon-context.cjs +2 -0
  251. package/dist/icons/icon-context.cjs.map +1 -0
  252. package/dist/icons/icon-context.js +13 -0
  253. package/dist/icons/icon-context.js.map +1 -0
  254. package/dist/icons/is-icon-name.cjs +2 -0
  255. package/dist/icons/is-icon-name.cjs.map +1 -0
  256. package/dist/icons/is-icon-name.js +10 -0
  257. package/dist/icons/is-icon-name.js.map +1 -0
  258. package/dist/icons/shard-cache.cjs +2 -0
  259. package/dist/icons/shard-cache.cjs.map +1 -0
  260. package/dist/icons/shard-cache.js +41 -0
  261. package/dist/icons/shard-cache.js.map +1 -0
  262. package/dist/icons/use-icon.cjs +2 -0
  263. package/dist/icons/use-icon.cjs.map +1 -0
  264. package/dist/icons/use-icon.js +20 -0
  265. package/dist/icons/use-icon.js.map +1 -0
  266. package/dist/icons-virtual.d.ts +22 -0
  267. package/dist/icons.cjs +1 -0
  268. package/dist/icons.d.ts +220 -0
  269. package/dist/icons.js +9 -0
  270. package/dist/offline/use-offline-sync.cjs +1 -1
  271. package/dist/offline/use-offline-sync.cjs.map +1 -1
  272. package/dist/offline/use-offline-sync.js +3 -1
  273. package/dist/offline/use-offline-sync.js.map +1 -1
  274. package/dist/push/use-push-subscription.cjs +1 -1
  275. package/dist/push/use-push-subscription.cjs.map +1 -1
  276. package/dist/push/use-push-subscription.js +3 -1
  277. package/dist/push/use-push-subscription.js.map +1 -1
  278. package/dist/sse/use-event-stream.cjs +1 -1
  279. package/dist/sse/use-event-stream.cjs.map +1 -1
  280. package/dist/sse/use-event-stream.js +3 -1
  281. package/dist/sse/use-event-stream.js.map +1 -1
  282. package/dist/styles.css +1 -1
  283. package/dist/tempest-react-sdk.cjs +1 -1
  284. package/dist/tempest-react-sdk.d.ts +585 -3
  285. package/dist/tempest-react-sdk.js +189 -183
  286. package/dist/theme/ThemeProvider.cjs +1 -1
  287. package/dist/theme/ThemeProvider.cjs.map +1 -1
  288. package/dist/theme/ThemeProvider.js +6 -2
  289. package/dist/theme/ThemeProvider.js.map +1 -1
  290. package/dist/theme/color.cjs +1 -1
  291. package/dist/theme/color.cjs.map +1 -1
  292. package/dist/theme/color.js +77 -46
  293. package/dist/theme/color.js.map +1 -1
  294. package/dist/theme/create-theme.cjs +1 -1
  295. package/dist/theme/create-theme.cjs.map +1 -1
  296. package/dist/theme/create-theme.js +9 -3
  297. package/dist/theme/create-theme.js.map +1 -1
  298. package/dist/utils/compare-values.cjs +2 -0
  299. package/dist/utils/compare-values.cjs.map +1 -0
  300. package/dist/utils/compare-values.js +8 -0
  301. package/dist/utils/compare-values.js.map +1 -0
  302. package/dist/vision/use-camera-stream.cjs +1 -1
  303. package/dist/vision/use-camera-stream.cjs.map +1 -1
  304. package/dist/vision/use-camera-stream.js +3 -1
  305. package/dist/vision/use-camera-stream.js.map +1 -1
  306. package/dist/vite/create-vite-config.cjs +1 -1
  307. package/dist/vite/create-vite-config.cjs.map +1 -1
  308. package/dist/vite/create-vite-config.js +30 -22
  309. package/dist/vite/create-vite-config.js.map +1 -1
  310. package/dist/vite/tempest-icons.cjs +9 -0
  311. package/dist/vite/tempest-icons.cjs.map +1 -0
  312. package/dist/vite/tempest-icons.js +99 -0
  313. package/dist/vite/tempest-icons.js.map +1 -0
  314. package/dist/vite.cjs +1 -1
  315. package/dist/vite.d.ts +79 -0
  316. package/dist/vite.js +6 -5
  317. package/dist/ws/use-web-socket.cjs +1 -1
  318. package/dist/ws/use-web-socket.cjs.map +1 -1
  319. package/dist/ws/use-web-socket.js +3 -1
  320. package/dist/ws/use-web-socket.js.map +1 -1
  321. package/package.json +12 -3
  322. package/template/package.json +1 -1
@@ -0,0 +1,85 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { isInsideBase, toAlias } from "./specifier.mjs";
4
+
5
+ const BASE = "/proj/src";
6
+ const FILE = "/proj/src/components/Card/Card.tsx";
7
+
8
+ const alias = (spec, filePath = FILE) => toAlias({ spec, filePath, prefix: "@", baseDir: BASE });
9
+
10
+ describe("isInsideBase", () => {
11
+ it("accepts a file under the base", () => {
12
+ expect(isInsideBase(FILE, BASE)).toBe(true);
13
+ });
14
+ it("rejects a file outside the base", () => {
15
+ expect(isInsideBase("/proj/vite.config.ts", BASE)).toBe(false);
16
+ expect(isInsideBase("/proj/e2e/spec.ts", BASE)).toBe(false);
17
+ });
18
+ it("rejects the base itself", () => {
19
+ expect(isInsideBase(BASE, BASE)).toBe(false);
20
+ });
21
+ });
22
+
23
+ describe("toAlias — what converts", () => {
24
+ it("converts a specifier climbing two levels", () => {
25
+ expect(alias("../../services/api")).toBe("@/services/api");
26
+ });
27
+ it("converts a specifier climbing one level", () => {
28
+ expect(alias("../Button")).toBe("@/components/Button");
29
+ });
30
+ it("converts up to the base root", () => {
31
+ expect(alias("../../routes")).toBe("@/routes");
32
+ });
33
+ });
34
+
35
+ describe("toAlias — what stays", () => {
36
+ it("leaves a sibling alone", () => {
37
+ expect(alias("./Card.module.css")).toBeNull();
38
+ expect(alias("./nested/deep")).toBeNull();
39
+ });
40
+ it("leaves a bare package alone", () => {
41
+ expect(alias("react")).toBeNull();
42
+ expect(alias("tempest-react-sdk/charts")).toBeNull();
43
+ });
44
+ it("leaves an already-aliased specifier alone", () => {
45
+ expect(alias("@/services/api")).toBeNull();
46
+ });
47
+ it("leaves a target outside the base alone", () => {
48
+ expect(alias("../../../vite.config")).toBeNull();
49
+ expect(alias("../../../../elsewhere/x")).toBeNull();
50
+ });
51
+ it("leaves a climb that lands exactly on the base alone", () => {
52
+ expect(alias("../..")).toBeNull();
53
+ expect(alias("../../")).toBeNull();
54
+ });
55
+ });
56
+
57
+ describe("toAlias — extensions", () => {
58
+ it("preserves whatever extension the specifier carried", () => {
59
+ expect(alias("../../styles/tokens.css")).toBe("@/styles/tokens.css");
60
+ expect(alias("../Card.module.css")).toBe("@/components/Card.module.css");
61
+ expect(alias("../../assets/logo.svg")).toBe("@/assets/logo.svg");
62
+ expect(alias("../../data/seed.json")).toBe("@/data/seed.json");
63
+ });
64
+ it("does not add an extension to an extensionless specifier", () => {
65
+ expect(alias("../../lib/api")).toBe("@/lib/api");
66
+ });
67
+ });
68
+
69
+ describe("toAlias — prefix and base come from the caller", () => {
70
+ it("honours a non-@ prefix", () => {
71
+ expect(toAlias({ spec: "../../lib/api", filePath: FILE, prefix: "~", baseDir: BASE })).toBe(
72
+ "~/lib/api",
73
+ );
74
+ });
75
+ it("honours an app/ base", () => {
76
+ expect(
77
+ toAlias({
78
+ spec: "../../lib/api",
79
+ filePath: "/proj/app/components/Card/Card.tsx",
80
+ prefix: "@",
81
+ baseDir: "/proj/app",
82
+ }),
83
+ ).toBe("@/lib/api");
84
+ });
85
+ });
@@ -0,0 +1,116 @@
1
+ // Read a project's tsconfig.json the way `tsc` does: JSONC comments allowed and
2
+ // the `extends` chain merged — without normalizing option values.
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { createRequire } from "node:module";
5
+ import { dirname, isAbsolute, join, resolve } from "node:path";
6
+
7
+ const MAX_EXTENDS_DEPTH = 16;
8
+
9
+ /**
10
+ * Parse one tsconfig file into its raw JSON object.
11
+ *
12
+ * Prefers `ts.readConfigFile`, which accepts the JSONC dialect `tsc` accepts
13
+ * (comments, trailing commas). Falls back to `JSON.parse` so a project without
14
+ * TypeScript installed still gets the previous behavior instead of nothing.
15
+ *
16
+ * @param {string} path - Absolute path to a tsconfig file.
17
+ * @param {object | null} ts - The `typescript` module, or `null`.
18
+ * @returns {object | null} The raw config object, or `null` when unreadable.
19
+ */
20
+ function parseConfigFile(path, ts) {
21
+ if (ts) {
22
+ const { config, error } = ts.readConfigFile(path, (p) => {
23
+ try {
24
+ return readFileSync(p, "utf8");
25
+ } catch {
26
+ return undefined;
27
+ }
28
+ });
29
+ return error || !config ? null : config;
30
+ }
31
+ try {
32
+ return JSON.parse(readFileSync(path, "utf8"));
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Resolve an `extends` entry to an absolute file path.
40
+ *
41
+ * Handles the two forms `tsc` supports: a relative/absolute path (with the
42
+ * `.json` extension optional) and a bare package specifier such as
43
+ * `@tsconfig/vite-react/tsconfig.json`, resolved from the extending file's
44
+ * directory.
45
+ *
46
+ * @param {string} spec - The `extends` value.
47
+ * @param {string} fromDir - Directory of the config that declares it.
48
+ * @returns {string | null} Absolute path, or `null` when it cannot be resolved.
49
+ */
50
+ function resolveExtends(spec, fromDir) {
51
+ if (spec.startsWith(".") || isAbsolute(spec)) {
52
+ const base = resolve(fromDir, spec);
53
+ for (const candidate of [base, `${base}.json`, join(base, "tsconfig.json")]) {
54
+ if (existsSync(candidate)) return candidate;
55
+ }
56
+ return null;
57
+ }
58
+ try {
59
+ return createRequire(join(fromDir, "package.json")).resolve(spec);
60
+ } catch {
61
+ try {
62
+ return createRequire(join(fromDir, "package.json")).resolve(
63
+ join(spec, "tsconfig.json"),
64
+ );
65
+ } catch {
66
+ return null;
67
+ }
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Read a project's tsconfig with its `extends` chain merged.
73
+ *
74
+ * `compilerOptions` are merged shallowly with the extending file winning, which
75
+ * matches `tsc` for the scalar options this CLI reads. Path-valued options
76
+ * (`baseUrl`, `paths`) are kept together with the directory of the file that
77
+ * declared them, because `tsc` resolves them relative to *that* file — not to
78
+ * the leaf config.
79
+ *
80
+ * Values are returned raw (`"bundler"`, `"react-jsx"`), never the numeric enums
81
+ * `ts.parseJsonConfigFileContent` would produce, so callers can compare strings.
82
+ *
83
+ * @param {object} params
84
+ * @param {string} params.root - Project root.
85
+ * @param {object | null} params.ts - The `typescript` module, or `null`.
86
+ * @param {string} [params.fileName] - Config file name. Default `"tsconfig.json"`.
87
+ * @returns {{ path: string, dir: string, compilerOptions: object, pathsBaseDir: string } | null}
88
+ * `null` when the file is absent or unparseable.
89
+ */
90
+ export function readTsconfig({ root, ts, fileName = "tsconfig.json" }) {
91
+ const path = resolve(root, fileName);
92
+ if (!existsSync(path)) return null;
93
+
94
+ const chain = [];
95
+ let current = path;
96
+ let depth = 0;
97
+ while (current && depth++ < MAX_EXTENDS_DEPTH) {
98
+ const config = parseConfigFile(current, ts);
99
+ if (!config) break;
100
+ chain.push({ dir: dirname(current), config });
101
+ const next = config.extends;
102
+ if (typeof next !== "string") break;
103
+ current = resolveExtends(next, dirname(current));
104
+ }
105
+ if (!chain.length) return null;
106
+
107
+ const compilerOptions = {};
108
+ let pathsBaseDir = dirname(path);
109
+ for (const { dir, config } of chain.reverse()) {
110
+ const co = config.compilerOptions;
111
+ if (!co || typeof co !== "object") continue;
112
+ if (co.paths || co.baseUrl) pathsBaseDir = dir;
113
+ Object.assign(compilerOptions, co);
114
+ }
115
+ return { path, dir: dirname(path), compilerOptions, pathsBaseDir };
116
+ }
@@ -0,0 +1,23 @@
1
+ // Resolve the *project's own* TypeScript install — the alias codemod parses app
2
+ // source, so it must agree with the compiler that type-checks it.
3
+ import { createRequire } from "node:module";
4
+ import { join } from "node:path";
5
+
6
+ /**
7
+ * Load `typescript` from the project's `node_modules`.
8
+ *
9
+ * Resolution is anchored at `root` (not at this file) so the CLI never falls
10
+ * back to a copy bundled with the SDK: parsing app source with a different
11
+ * compiler version than the one that type-checks it would let syntax the app
12
+ * accepts fail here.
13
+ *
14
+ * @param {string} root - Project root, the directory holding `node_modules`.
15
+ * @returns {object | null} The `typescript` module, or `null` when not installed.
16
+ */
17
+ export function loadTypeScript(root) {
18
+ try {
19
+ return createRequire(join(root, "package.json"))("typescript");
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
@@ -0,0 +1,74 @@
1
+ // The `lucide-react` single-instance check for `tempest doctor`.
2
+ //
3
+ // Kept out of the generic stateful-deps check on purpose: two copies of React or
4
+ // of react-query break hooks and context, which is a different failure from what
5
+ // two copies of lucide cause. Lucide is stateless, so nothing "breaks" at
6
+ // runtime — instead the bytes are duplicated and, far worse, the generated slug
7
+ // tables behind `tempest-react-sdk/icons` can reference exports the second copy
8
+ // does not have.
9
+
10
+ /** Major version number from a semver string or range, or null. */
11
+ function major(spec) {
12
+ if (!spec) return null;
13
+ const match = /(\d+)\./.exec(String(spec).replace(/^[^\d]*/, ""));
14
+ return match ? Number(match[1]) : null;
15
+ }
16
+
17
+ /**
18
+ * Audit how many copies of `lucide-react` a project will end up with.
19
+ *
20
+ * @param {object} params
21
+ * @param {string | null} params.appSpec - Range the app declares, or `null`.
22
+ * @param {string | null} params.sdkSpec - Range the installed SDK declares, or `null`.
23
+ * @param {string | null} params.installedVersion - Version resolved at the app root, or `null`.
24
+ * @param {boolean} params.nestedCopy - Whether a copy exists under the SDK's own `node_modules`.
25
+ * @returns {Array<[status: string, label: string, detail?: string]>} Check rows.
26
+ */
27
+ export function checkLucide({ appSpec, sdkSpec, installedVersion, nestedCopy }) {
28
+ const rows = [];
29
+
30
+ // The SDK not declaring it means this project does not use the icons surface
31
+ // at all (or the SDK is not installed) — there is nothing to audit.
32
+ if (!sdkSpec) return rows;
33
+
34
+ if (nestedCopy) {
35
+ rows.push([
36
+ "warn",
37
+ "two copies of lucide-react",
38
+ "nested copy under tempest-react-sdk — duplicated bytes, and the generated " +
39
+ "icon slug tables may not match it; run `npm dedupe` or drop lucide-react " +
40
+ "from your package.json (the SDK ships it)",
41
+ ]);
42
+ }
43
+
44
+ if (appSpec) {
45
+ const sameRange = appSpec === sdkSpec;
46
+ rows.push([
47
+ sameRange ? "info" : "warn",
48
+ "lucide-react declared by the app",
49
+ sameRange
50
+ ? `matches the SDK's ${sdkSpec} so a single copy installs — the declaration is ` +
51
+ "redundant, but harmless (npm/yarn resolve it from the SDK without it)"
52
+ : `you declare ${appSpec}, the SDK declares ${sdkSpec} — two copies install. ` +
53
+ "Run `npm uninstall lucide-react`; only declare it under pnpm, and then " +
54
+ `use ${sdkSpec}`,
55
+ ]);
56
+ }
57
+
58
+ const installedMajor = major(installedVersion);
59
+ const requiredMajor = major(sdkSpec);
60
+ if (installedMajor !== null && requiredMajor !== null && installedMajor < requiredMajor) {
61
+ rows.push([
62
+ "fail",
63
+ `lucide-react v${installedVersion} is older than the SDK needs`,
64
+ `the icon slug tables are generated against ${sdkSpec}, so exports they ` +
65
+ "reference can be missing — the build fails with `… is not exported by " +
66
+ "lucide-react` pointing inside the SDK",
67
+ ]);
68
+ }
69
+
70
+ if (rows.length === 0) {
71
+ rows.push(["ok", "single lucide-react instance", `from the SDK — ${sdkSpec}`]);
72
+ }
73
+ return rows;
74
+ }
@@ -0,0 +1,107 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { checkLucide } from "./lucide.mjs";
4
+
5
+ /** The healthy shape: the SDK brings lucide, the app declares nothing. */
6
+ const CLEAN = {
7
+ appSpec: null,
8
+ sdkSpec: "^1.26.0",
9
+ installedVersion: "1.26.0",
10
+ nestedCopy: false,
11
+ };
12
+
13
+ /** Statuses of the returned rows, in order. */
14
+ const statuses = (rows) => rows.map(([status]) => status);
15
+
16
+ /** All detail text joined, for asserting the advice is actually present. */
17
+ const details = (rows) => rows.map(([, , detail]) => detail ?? "").join(" ");
18
+
19
+ describe("checkLucide — nothing to audit", () => {
20
+ it("returns no rows when the SDK does not declare lucide", () => {
21
+ expect(checkLucide({ ...CLEAN, sdkSpec: null })).toEqual([]);
22
+ });
23
+
24
+ it("stays silent even if the app declares it, with no SDK to compare against", () => {
25
+ expect(checkLucide({ ...CLEAN, sdkSpec: null, appSpec: "^1.0.0" })).toEqual([]);
26
+ });
27
+ });
28
+
29
+ describe("checkLucide — healthy", () => {
30
+ it("reports a single instance and names where it came from", () => {
31
+ const rows = checkLucide(CLEAN);
32
+ expect(statuses(rows)).toEqual(["ok"]);
33
+ expect(rows[0][1]).toBe("single lucide-react instance");
34
+ expect(rows[0][2]).toContain("^1.26.0");
35
+ });
36
+
37
+ it("stays ok when the installed version is newer than required", () => {
38
+ expect(statuses(checkLucide({ ...CLEAN, installedVersion: "2.4.0" }))).toEqual(["ok"]);
39
+ });
40
+
41
+ it("stays ok when the version cannot be resolved at all", () => {
42
+ expect(statuses(checkLucide({ ...CLEAN, installedVersion: null }))).toEqual(["ok"]);
43
+ });
44
+ });
45
+
46
+ describe("checkLucide — the app declares it too", () => {
47
+ it("warns and says to uninstall when the range differs from the SDK's", () => {
48
+ const rows = checkLucide({ ...CLEAN, appSpec: "^0.575.0" });
49
+ expect(statuses(rows)).toEqual(["warn"]);
50
+ expect(details(rows)).toContain("npm uninstall lucide-react");
51
+ expect(details(rows)).toContain("^0.575.0");
52
+ expect(details(rows)).toContain("^1.26.0");
53
+ });
54
+
55
+ it("mentions pnpm, the one case where declaring it is correct", () => {
56
+ expect(details(checkLucide({ ...CLEAN, appSpec: "^0.575.0" }))).toContain("pnpm");
57
+ });
58
+
59
+ it("downgrades to info when the range matches — redundant, not broken", () => {
60
+ const rows = checkLucide({ ...CLEAN, appSpec: "^1.26.0" });
61
+ expect(statuses(rows)).toEqual(["info"]);
62
+ expect(details(rows)).toContain("redundant");
63
+ });
64
+ });
65
+
66
+ describe("checkLucide — a second physical copy", () => {
67
+ it("warns about the nested copy and points at both remedies", () => {
68
+ const rows = checkLucide({ ...CLEAN, nestedCopy: true });
69
+ expect(statuses(rows)).toEqual(["warn"]);
70
+ expect(rows[0][1]).toBe("two copies of lucide-react");
71
+ expect(details(rows)).toContain("npm dedupe");
72
+ expect(details(rows)).toContain("the SDK ships it");
73
+ });
74
+
75
+ it("reports the nested copy and the declaration as separate rows", () => {
76
+ const rows = checkLucide({ ...CLEAN, nestedCopy: true, appSpec: "^0.575.0" });
77
+ expect(statuses(rows)).toEqual(["warn", "warn"]);
78
+ });
79
+ });
80
+
81
+ describe("checkLucide — version older than the generated tables need", () => {
82
+ it("fails, because this one breaks the build rather than wasting bytes", () => {
83
+ const rows = checkLucide({ ...CLEAN, installedVersion: "0.575.0" });
84
+ expect(statuses(rows)).toEqual(["fail"]);
85
+ expect(rows[0][1]).toContain("0.575.0");
86
+ expect(details(rows)).toContain("is not exported by");
87
+ });
88
+
89
+ it("explains that the error surfaces inside the SDK, not in app code", () => {
90
+ expect(details(checkLucide({ ...CLEAN, installedVersion: "0.575.0" }))).toContain(
91
+ "inside the SDK",
92
+ );
93
+ });
94
+
95
+ it("reports the skew alongside the declaration that caused it", () => {
96
+ const rows = checkLucide({
97
+ ...CLEAN,
98
+ appSpec: "^0.575.0",
99
+ installedVersion: "0.575.0",
100
+ });
101
+ expect(statuses(rows)).toEqual(["warn", "fail"]);
102
+ });
103
+
104
+ it("does not fail on a malformed version string", () => {
105
+ expect(statuses(checkLucide({ ...CLEAN, installedVersion: "latest" }))).toEqual(["ok"]);
106
+ });
107
+ });
@@ -0,0 +1,156 @@
1
+ // `tempest gen icons` — write a static icon registry from the slugs an app's
2
+ // source mentions, for projects that want a versioned file instead of the
3
+ // `virtual:tempest-icons` module the Vite plugin generates.
4
+ import { readdir, readFile } from "node:fs/promises";
5
+ import { join } from "node:path";
6
+ import { pathToFileURL } from "node:url";
7
+
8
+ const NAME_PATTERNS = [
9
+ /\bname\s*=\s*["']([a-z0-9-]+)["']/g,
10
+ /\bname\s*=\s*\{\s*["']([a-z0-9-]+)["']\s*\}/g,
11
+ /\bname\s*:\s*["']([a-z0-9-]+)["']/g,
12
+ ];
13
+
14
+ const SOURCE_FILE = /\.[cm]?[jt]sx?$/;
15
+ const SKIP_DIRS = new Set(["node_modules", "dist", "build", "coverage"]);
16
+
17
+ /**
18
+ * Read the slug tables out of the installed SDK build.
19
+ *
20
+ * Deliberately reads `dist/` rather than embedding a copy: the CLI must agree with
21
+ * the `<Icon>` the app actually renders, and both come from the same install.
22
+ *
23
+ * @param {string} selfDir - Directory holding this CLI (`bin/`).
24
+ * @returns {Promise<{ known: Set<string>, aliases: Record<string, string> }>}
25
+ */
26
+ export async function loadIconTables(selfDir) {
27
+ const dist = join(selfDir, "..", "dist", "icons.js");
28
+ const mod = await import(pathToFileURL(dist).href);
29
+ return { known: new Set(mod.iconNames), aliases: mod.iconAliases };
30
+ }
31
+
32
+ /**
33
+ * Collect the icon slugs referenced literally in a source string.
34
+ *
35
+ * @param {string} code - File contents.
36
+ * @param {Set<string>} known - Real lucide slugs, aliases included.
37
+ * @returns {string[]} Validated slugs.
38
+ */
39
+ export function scanIconSlugs(code, known) {
40
+ const found = new Set();
41
+ for (const pattern of NAME_PATTERNS) {
42
+ pattern.lastIndex = 0;
43
+ let match;
44
+ while ((match = pattern.exec(code)) !== null) {
45
+ if (known.has(match[1])) found.add(match[1]);
46
+ }
47
+ }
48
+ return [...found];
49
+ }
50
+
51
+ /**
52
+ * Walk a directory for source files, skipping build output and dot directories.
53
+ *
54
+ * @param {string} dir - Absolute directory.
55
+ * @returns {Promise<string[]>} Absolute file paths.
56
+ */
57
+ export async function collectSourceFiles(dir) {
58
+ const out = [];
59
+ let entries;
60
+ try {
61
+ entries = await readdir(dir, { withFileTypes: true });
62
+ } catch {
63
+ return out;
64
+ }
65
+ for (const entry of entries) {
66
+ if (entry.isDirectory()) {
67
+ if (SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
68
+ out.push(...(await collectSourceFiles(join(dir, entry.name))));
69
+ } else if (entry.isFile() && SOURCE_FILE.test(entry.name)) {
70
+ out.push(join(dir, entry.name));
71
+ }
72
+ }
73
+ return out;
74
+ }
75
+
76
+ /** `circle-alert` → `CircleAlert`; `a-arrow-down` → `AArrowDown`. */
77
+ function pascalCase(slug) {
78
+ return slug
79
+ .split("-")
80
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
81
+ .join("");
82
+ }
83
+
84
+ /**
85
+ * Export names that would shadow a restricted global if bound directly.
86
+ *
87
+ * Lucide ships an icon literally called `infinity`, whose export is `Infinity`.
88
+ * Binding that at module scope shadows the global, so the import is aliased.
89
+ */
90
+ const RESERVED_NAMES = new Set(["Infinity", "NaN", "undefined", "eval", "arguments"]);
91
+
92
+ /** The local binding to use for a lucide export name. */
93
+ function localName(name) {
94
+ return RESERVED_NAMES.has(name) ? `${name}Icon` : name;
95
+ }
96
+
97
+ /**
98
+ * Render the registry module.
99
+ *
100
+ * An alias slug imports its canonical component but keeps its own key, so a name
101
+ * persisted before a lucide rename still resolves with no extra request.
102
+ *
103
+ * @param {readonly string[]} slugs - Validated slugs.
104
+ * @param {Record<string, string>} aliases - Alias slug → canonical slug.
105
+ * @returns {string} TypeScript module source.
106
+ */
107
+ export function renderRegistry(slugs, aliases) {
108
+ const sorted = [...new Set(slugs)].sort();
109
+ const header = `// GENERATED by \`tempest gen icons\` — re-run after adding icons.
110
+ import { createIconRegistry } from "tempest-react-sdk/icons";
111
+ `;
112
+ if (!sorted.length) {
113
+ return `${header}
114
+ export const icons = createIconRegistry({});
115
+ `;
116
+ }
117
+ const componentOf = new Map(sorted.map((slug) => [slug, pascalCase(aliases[slug] ?? slug)]));
118
+ const named = [...new Set(componentOf.values())]
119
+ .sort()
120
+ .map((name) => (localName(name) === name ? name : `${name} as ${localName(name)}`))
121
+ .join(", ");
122
+ const entries = sorted
123
+ .map((slug) => ` "${slug}": ${localName(componentOf.get(slug))},`)
124
+ .join("\n");
125
+ return `${header}import { ${named} } from "lucide-react";
126
+
127
+ export const icons = createIconRegistry({
128
+ ${entries}
129
+ });
130
+ `;
131
+ }
132
+
133
+ /**
134
+ * Scan a directory tree and render the registry module for it.
135
+ *
136
+ * @param {object} params
137
+ * @param {string} params.dir - Absolute directory to scan.
138
+ * @param {Set<string>} params.known - Real lucide slugs.
139
+ * @param {Record<string, string>} params.aliases - Alias slug → canonical slug.
140
+ * @param {readonly string[]} [params.include] - Extra slugs the scan cannot see.
141
+ * @returns {Promise<{ source: string, slugs: string[], files: number }>}
142
+ */
143
+ export async function generateRegistry({ dir, known, aliases, include = [] }) {
144
+ const files = await collectSourceFiles(dir);
145
+ const slugs = new Set(include.filter((slug) => known.has(slug)));
146
+ for (const file of files) {
147
+ try {
148
+ const code = await readFile(file, "utf8");
149
+ for (const slug of scanIconSlugs(code, known)) slugs.add(slug);
150
+ } catch {
151
+ /* an unreadable file just contributes nothing */
152
+ }
153
+ }
154
+ const sorted = [...slugs].sort();
155
+ return { source: renderRegistry(sorted, aliases), slugs: sorted, files: files.length };
156
+ }