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,138 @@
1
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
+
6
+ import { aliasImports } from "./index.mjs";
7
+
8
+ let root;
9
+
10
+ /**
11
+ * Build a project fixture whose `typescript` resolves to this repo's copy.
12
+ *
13
+ * The symlinked `node_modules` is what makes `loadTypeScript` succeed from a
14
+ * directory outside the repo — the codemod deliberately refuses to fall back to
15
+ * a compiler the project does not have, so a fixture without it can only ever
16
+ * exercise the `no-typescript` branch.
17
+ */
18
+ beforeEach(() => {
19
+ root = mkdtempSync(join(tmpdir(), "tempest-alias-run-"));
20
+ symlinkSync(join(process.cwd(), "node_modules"), join(root, "node_modules"), "dir");
21
+ mkdirSync(join(root, "src"), { recursive: true });
22
+ writeFileSync(
23
+ join(root, "tsconfig.json"),
24
+ JSON.stringify({ compilerOptions: { paths: { "@/*": ["./src/*"] } } }),
25
+ );
26
+ });
27
+
28
+ afterEach(() => {
29
+ rmSync(root, { recursive: true, force: true });
30
+ });
31
+
32
+ /** Write a fixture file, creating parent directories. */
33
+ function write(rel, contents) {
34
+ const path = join(root, rel);
35
+ mkdirSync(join(path, ".."), { recursive: true });
36
+ writeFileSync(path, contents);
37
+ }
38
+
39
+ /** Read a fixture file back. */
40
+ function read(rel) {
41
+ return readFileSync(join(root, rel), "utf8");
42
+ }
43
+
44
+ const run = (targets = ["."], dryRun = false) => aliasImports({ root, targets, dryRun });
45
+
46
+ describe("aliasImports — writing", () => {
47
+ it("rewrites source and style files and reports the totals", () => {
48
+ write(
49
+ "src/pages/admin/Login.tsx",
50
+ `import { api } from "../../lib/api";\nimport "../x.css";\n`,
51
+ );
52
+ write("src/pages/admin/Login.module.css", `@import "../../styles/tokens.css";\n`);
53
+
54
+ const result = run();
55
+
56
+ expect(result.status).toBe("ok");
57
+ expect(result.prefix).toBe("@");
58
+ expect(result.total).toBe(3);
59
+ expect(result.files.map((f) => f.path.split("\\").join("/")).sort()).toEqual([
60
+ "src/pages/admin/Login.module.css",
61
+ "src/pages/admin/Login.tsx",
62
+ ]);
63
+ expect(read("src/pages/admin/Login.tsx")).toBe(
64
+ `import { api } from "@/lib/api";\nimport "@/pages/x.css";\n`,
65
+ );
66
+ expect(read("src/pages/admin/Login.module.css")).toBe(`@import "@/styles/tokens.css";\n`);
67
+ });
68
+
69
+ it("leaves files it must not touch alone", () => {
70
+ write("src/pages/admin/Login.tsx", `import { Row } from "./Row";\n`);
71
+ write("src/pages/admin/Deep.tsx", `import cfg from "../../../vite.config";\n`);
72
+ write("vite.config.ts", `import x from "./src/lib/api";\n`);
73
+ write("e2e/spec.ts", `import { api } from "../src/lib/api";\n`);
74
+
75
+ const result = run();
76
+
77
+ expect(result.total).toBe(0);
78
+ expect(result.files).toEqual([]);
79
+ expect(read("src/pages/admin/Deep.tsx")).toBe(`import cfg from "../../../vite.config";\n`);
80
+ expect(read("vite.config.ts")).toBe(`import x from "./src/lib/api";\n`);
81
+ expect(read("e2e/spec.ts")).toBe(`import { api } from "../src/lib/api";\n`);
82
+ });
83
+
84
+ it("is idempotent across two runs", () => {
85
+ write("src/pages/admin/Login.tsx", `import { api } from "../../lib/api";\n`);
86
+ expect(run().total).toBe(1);
87
+ const after = read("src/pages/admin/Login.tsx");
88
+ expect(run().total).toBe(0);
89
+ expect(read("src/pages/admin/Login.tsx")).toBe(after);
90
+ });
91
+
92
+ it("restricts the rewrite to a positional path", () => {
93
+ write("src/pages/admin/Login.tsx", `import { api } from "../../lib/api";\n`);
94
+ write("src/widgets/nested/Card.tsx", `import { api } from "../../lib/api";\n`);
95
+
96
+ expect(run(["src/pages"]).total).toBe(1);
97
+ expect(read("src/widgets/nested/Card.tsx")).toBe(`import { api } from "../../lib/api";\n`);
98
+ });
99
+ });
100
+
101
+ describe("aliasImports — dry run", () => {
102
+ it("reports the changes without writing", () => {
103
+ const before = `import { api } from "../../lib/api";\n`;
104
+ write("src/pages/admin/Login.tsx", before);
105
+
106
+ const result = run(["."], true);
107
+
108
+ expect(result.total).toBe(1);
109
+ expect(result.files[0].changes).toEqual([
110
+ { from: "../../lib/api", to: "@/lib/api", line: 1 },
111
+ ]);
112
+ expect(read("src/pages/admin/Login.tsx")).toBe(before);
113
+ });
114
+ });
115
+
116
+ describe("aliasImports — degraded projects", () => {
117
+ it("reports no-typescript when the project has no compiler", () => {
118
+ const bare = mkdtempSync(join(tmpdir(), "tempest-alias-bare-"));
119
+ try {
120
+ expect(aliasImports({ root: bare, targets: ["."] })).toMatchObject({
121
+ status: "no-typescript",
122
+ total: 0,
123
+ files: [],
124
+ });
125
+ } finally {
126
+ rmSync(bare, { recursive: true, force: true });
127
+ }
128
+ });
129
+
130
+ it("reports no-alias and writes nothing when no tsconfig declares one", () => {
131
+ rmSync(join(root, "tsconfig.json"));
132
+ const before = `import { api } from "../../lib/api";\n`;
133
+ write("src/pages/admin/Login.tsx", before);
134
+
135
+ expect(run()).toMatchObject({ status: "no-alias", total: 0, files: [] });
136
+ expect(read("src/pages/admin/Login.tsx")).toBe(before);
137
+ });
138
+ });
@@ -0,0 +1,76 @@
1
+ // Rewrite relative paths inside CSS to the project alias. Vite resolves aliases
2
+ // in stylesheets too, but the TypeScript AST pass cannot see `.css` files.
3
+ import { isInsideBase, toAlias } from "./specifier.mjs";
4
+
5
+ /**
6
+ * `url(...)` in any position — `@import url("x")`, `background: url(x)`.
7
+ * Group 1 is everything before the value, group 2 the optional quote, group 3 the
8
+ * value, so the value's offset is derivable without searching inside the match.
9
+ */
10
+ const URL_PATTERN = /(url\(\s*)(["']?)([^"')]+)\2(\s*\))/g;
11
+
12
+ /**
13
+ * Quoted `@import` without `url()`. The quote must follow the whitespace
14
+ * directly, which is what keeps this from also matching the `url()` form above
15
+ * and rewriting the same span twice.
16
+ */
17
+ const IMPORT_PATTERN = /(@import\s+)(["'])([^"']+)\2/g;
18
+
19
+ /**
20
+ * Collect the rewritable value spans of a stylesheet.
21
+ *
22
+ * @param {string} text - Stylesheet contents.
23
+ * @returns {Array<{ start: number, end: number, value: string }>}
24
+ */
25
+ function collectSpans(text) {
26
+ const spans = [];
27
+ for (const pattern of [URL_PATTERN, IMPORT_PATTERN]) {
28
+ pattern.lastIndex = 0;
29
+ let match;
30
+ while ((match = pattern.exec(text)) !== null) {
31
+ const start = match.index + match[1].length + match[2].length;
32
+ spans.push({ start, end: start + match[3].length, value: match[3] });
33
+ }
34
+ }
35
+ return spans.sort((a, b) => a.start - b.start);
36
+ }
37
+
38
+ /**
39
+ * Rewrite the relative paths of one stylesheet to the alias form.
40
+ *
41
+ * Only values that climb out of the file's directory *and* land inside the alias
42
+ * base are converted, so `data:`, `http:` and fragment URLs need no special
43
+ * casing — none of them starts with `../`.
44
+ *
45
+ * Pure, and back-to-front like the TypeScript pass, for the same reason: earlier
46
+ * offsets must stay valid while later edits are applied.
47
+ *
48
+ * @param {object} params
49
+ * @param {string} params.text - Stylesheet contents.
50
+ * @param {string} params.filePath - Absolute path of the file.
51
+ * @param {string} params.prefix - Alias prefix, e.g. `"@"`.
52
+ * @param {string} params.baseDir - Absolute alias base directory.
53
+ * @returns {{ text: string, changes: Array<{ from: string, to: string, line: number }> }}
54
+ */
55
+ export function rewriteCss({ text, filePath, prefix, baseDir }) {
56
+ if (!isInsideBase(filePath, baseDir)) return { text, changes: [] };
57
+
58
+ const edits = [];
59
+ const changes = [];
60
+ for (const { start, end, value } of collectSpans(text)) {
61
+ const to = toAlias({ spec: value, filePath, prefix, baseDir });
62
+ if (!to) continue;
63
+ edits.push({ start, end, to });
64
+ changes.push({
65
+ from: value,
66
+ to,
67
+ line: text.slice(0, start).split("\n").length,
68
+ });
69
+ }
70
+
71
+ let out = text;
72
+ for (const { start, end, to } of edits.reverse()) {
73
+ out = `${out.slice(0, start)}${to}${out.slice(end)}`;
74
+ }
75
+ return { text: out, changes };
76
+ }
@@ -0,0 +1,85 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { rewriteCss } from "./rewrite-css.mjs";
4
+
5
+ const BASE = "/proj/src";
6
+ const FILE = "/proj/src/components/Card/Card.module.css";
7
+
8
+ const rewrite = (text, filePath = FILE) =>
9
+ rewriteCss({ text, filePath, prefix: "@", baseDir: BASE });
10
+
11
+ describe("rewriteCss — @import", () => {
12
+ it("converts a quoted @import", () => {
13
+ const { text, changes } = rewrite(`@import "../../styles/tokens.css";\n`);
14
+ expect(text).toBe(`@import "@/styles/tokens.css";\n`);
15
+ expect(changes).toEqual([
16
+ { from: "../../styles/tokens.css", to: "@/styles/tokens.css", line: 1 },
17
+ ]);
18
+ });
19
+ it("converts @import url(...) exactly once", () => {
20
+ const { text, changes } = rewrite(`@import url("../../styles/tokens.css");\n`);
21
+ expect(text).toBe(`@import url("@/styles/tokens.css");\n`);
22
+ expect(changes).toHaveLength(1);
23
+ });
24
+ it("preserves single quotes", () => {
25
+ const { text } = rewrite(`@import '../../styles/tokens.css';\n`);
26
+ expect(text).toBe(`@import '@/styles/tokens.css';\n`);
27
+ });
28
+ });
29
+
30
+ describe("rewriteCss — url()", () => {
31
+ it("converts an unquoted url()", () => {
32
+ const { text } = rewrite(`.a { background: url(../../assets/bg.png); }\n`);
33
+ expect(text).toBe(`.a { background: url(@/assets/bg.png); }\n`);
34
+ });
35
+ it("converts a quoted url()", () => {
36
+ const { text } = rewrite(`.a { background: url("../../assets/bg.png"); }\n`);
37
+ expect(text).toBe(`.a { background: url("@/assets/bg.png"); }\n`);
38
+ });
39
+ it("converts several url() on one line without sliding offsets", () => {
40
+ const { text } = rewrite(
41
+ `.a { background: url(../../a.png); border-image: url(../../b.png); }\n`,
42
+ );
43
+ expect(text).toBe(`.a { background: url(@/a.png); border-image: url(@/b.png); }\n`);
44
+ });
45
+ });
46
+
47
+ describe("rewriteCss — what stays", () => {
48
+ it("leaves data:, http: and fragment URLs alone", () => {
49
+ const src = `.a { background: url(data:image/png;base64,AAA); }\n.b { background: url("https://x/y.png"); }\n.c { fill: url(#grad); }\n`;
50
+ expect(rewrite(src).text).toBe(src);
51
+ });
52
+ it("leaves a sibling path alone", () => {
53
+ const src = `@import "./tokens.css";\n.a { background: url(./bg.png); }\n`;
54
+ expect(rewrite(src).text).toBe(src);
55
+ });
56
+ it("leaves a root-absolute path alone", () => {
57
+ const src = `.a { background: url(/icon.svg); }\n`;
58
+ expect(rewrite(src).text).toBe(src);
59
+ });
60
+ it("leaves a target outside the alias base alone", () => {
61
+ const src = `.a { background: url(../../../public/bg.png); }\n`;
62
+ expect(rewrite(src).text).toBe(src);
63
+ });
64
+ it("leaves a file outside the alias base untouched", () => {
65
+ const src = `@import "../src/styles/tokens.css";\n`;
66
+ const { text, changes } = rewrite(src, "/proj/e2e/fixture.css");
67
+ expect(text).toBe(src);
68
+ expect(changes).toEqual([]);
69
+ });
70
+ });
71
+
72
+ describe("rewriteCss — mechanics", () => {
73
+ it("reports the line of each change", () => {
74
+ const { changes } = rewrite(
75
+ `@import "../../styles/a.css";\n\n.x { background: url(../../assets/b.png); }\n`,
76
+ );
77
+ expect(changes.map((ch) => ch.line)).toEqual([1, 3]);
78
+ });
79
+ it("is idempotent", () => {
80
+ const first = rewrite(`@import "../../styles/tokens.css";\n`);
81
+ const second = rewrite(first.text);
82
+ expect(second.text).toBe(first.text);
83
+ expect(second.changes).toEqual([]);
84
+ });
85
+ });
@@ -0,0 +1,141 @@
1
+ // Rewrite relative module specifiers to the project alias, driven by the
2
+ // TypeScript AST so only real specifier positions are touched.
3
+ import { extname } from "node:path";
4
+
5
+ import { isInsideBase, toAlias } from "./specifier.mjs";
6
+
7
+ const SCRIPT_KIND_BY_EXT = {
8
+ ".ts": "TS",
9
+ ".mts": "TS",
10
+ ".cts": "TS",
11
+ ".tsx": "TSX",
12
+ ".js": "JS",
13
+ ".mjs": "JS",
14
+ ".cjs": "JS",
15
+ ".jsx": "JSX",
16
+ };
17
+
18
+ const MOCK_METHODS = new Set(["mock", "doMock", "unmock", "doUnmock"]);
19
+
20
+ /**
21
+ * Pick the `ts.ScriptKind` for a file so JSX parses as JSX.
22
+ *
23
+ * Getting this wrong is silent: parsed with `ScriptKind.TS`, a `.tsx` file's
24
+ * `<Foo />` becomes a type assertion and the specifiers after it are never
25
+ * visited.
26
+ *
27
+ * @param {string} filePath
28
+ * @param {object} ts - The `typescript` module.
29
+ * @returns {number} A `ts.ScriptKind` value.
30
+ */
31
+ function scriptKindFor(filePath, ts) {
32
+ return ts.ScriptKind[SCRIPT_KIND_BY_EXT[extname(filePath)] ?? "TSX"];
33
+ }
34
+
35
+ /**
36
+ * True for a `vi.mock(...)`-family call expression.
37
+ *
38
+ * The mock path has to travel with the import it shadows: Vitest matches mocks
39
+ * by resolved id, so converting the import while leaving the mock relative keeps
40
+ * working, but a half-converted *pair* in a file someone later moves is exactly
41
+ * the silent-mock failure this avoids.
42
+ *
43
+ * @param {object} node - A `ts.CallExpression`.
44
+ * @param {object} ts - The `typescript` module.
45
+ * @returns {boolean}
46
+ */
47
+ function isViMockCall(node, ts) {
48
+ const target = node.expression;
49
+ return (
50
+ ts.isPropertyAccessExpression(target) &&
51
+ ts.isIdentifier(target.expression) &&
52
+ target.expression.text === "vi" &&
53
+ ts.isIdentifier(target.name) &&
54
+ MOCK_METHODS.has(target.name.text)
55
+ );
56
+ }
57
+
58
+ /**
59
+ * Collect every string literal that sits in a module-specifier position.
60
+ *
61
+ * This is the whole reason the codemod parses instead of running a regex: a path
62
+ * inside a comment, a template literal, a plain variable or a `vi.mock` call with
63
+ * a computed argument never reaches this list.
64
+ *
65
+ * @param {object} sourceFile - A `ts.SourceFile`.
66
+ * @param {object} ts - The `typescript` module.
67
+ * @returns {object[]} The `ts.StringLiteral` nodes, in source order.
68
+ */
69
+ function collectSpecifiers(sourceFile, ts) {
70
+ const found = [];
71
+ const push = (node) => {
72
+ if (node && ts.isStringLiteral(node)) found.push(node);
73
+ };
74
+ const visit = (node) => {
75
+ if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
76
+ push(node.moduleSpecifier);
77
+ } else if (ts.isImportTypeNode(node)) {
78
+ if (ts.isLiteralTypeNode(node.argument)) push(node.argument.literal);
79
+ } else if (ts.isCallExpression(node)) {
80
+ const isDynamicImport = node.expression.kind === ts.SyntaxKind.ImportKeyword;
81
+ if (isDynamicImport || isViMockCall(node, ts)) push(node.arguments[0]);
82
+ }
83
+ ts.forEachChild(node, visit);
84
+ };
85
+ visit(sourceFile);
86
+ return found;
87
+ }
88
+
89
+ /**
90
+ * Rewrite the relative specifiers of one source file to the alias form.
91
+ *
92
+ * Pure: takes text, returns text. Nothing is read from or written to disk, which
93
+ * is what makes the rule itself testable without a fixture tree.
94
+ *
95
+ * Edits are applied back-to-front so that earlier offsets stay valid, and the
96
+ * original quote character is reused so the pass never fights Prettier over
97
+ * quote style. Formatting and import order are deliberately left alone — the
98
+ * `eslint --fix` and Prettier passes that follow own those.
99
+ *
100
+ * Files outside `baseDir` are returned untouched: see `isInsideBase`.
101
+ *
102
+ * @param {object} params
103
+ * @param {string} params.text - File contents.
104
+ * @param {string} params.filePath - Absolute path of the file.
105
+ * @param {string} params.prefix - Alias prefix, e.g. `"@"`.
106
+ * @param {string} params.baseDir - Absolute alias base directory.
107
+ * @param {object} params.ts - The `typescript` module.
108
+ * @returns {{ text: string, changes: Array<{ from: string, to: string, line: number }> }}
109
+ */
110
+ export function rewriteSource({ text, filePath, prefix, baseDir, ts }) {
111
+ if (!isInsideBase(filePath, baseDir)) return { text, changes: [] };
112
+
113
+ const sourceFile = ts.createSourceFile(
114
+ filePath,
115
+ text,
116
+ ts.ScriptTarget.Latest,
117
+ true,
118
+ scriptKindFor(filePath, ts),
119
+ );
120
+
121
+ const edits = [];
122
+ const changes = [];
123
+ for (const literal of collectSpecifiers(sourceFile, ts)) {
124
+ const to = toAlias({ spec: literal.text, filePath, prefix, baseDir });
125
+ if (!to) continue;
126
+ const start = literal.getStart(sourceFile);
127
+ const end = literal.getEnd();
128
+ edits.push({ start, end, quote: text[start], to });
129
+ changes.push({
130
+ from: literal.text,
131
+ to,
132
+ line: sourceFile.getLineAndCharacterOfPosition(start).line + 1,
133
+ });
134
+ }
135
+
136
+ let out = text;
137
+ for (const { start, end, quote, to } of edits.reverse()) {
138
+ out = `${out.slice(0, start)}${quote}${to}${quote}${out.slice(end)}`;
139
+ }
140
+ return { text: out, changes };
141
+ }
@@ -0,0 +1,148 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { rewriteSource } from "./rewrite.mjs";
4
+ import { loadTypeScript } from "./typescript.mjs";
5
+
6
+ const ts = loadTypeScript(process.cwd());
7
+ const BASE = "/proj/src";
8
+ const FILE = "/proj/src/pages/admin/Users.tsx";
9
+
10
+ const rewrite = (text, filePath = FILE) =>
11
+ rewriteSource({ text, filePath, prefix: "@", baseDir: BASE, ts });
12
+
13
+ describe("rewriteSource — import / export positions", () => {
14
+ it("converts a value import", () => {
15
+ const { text, changes } = rewrite(`import { api } from "../../lib/api";\n`);
16
+ expect(text).toBe(`import { api } from "@/lib/api";\n`);
17
+ expect(changes).toEqual([{ from: "../../lib/api", to: "@/lib/api", line: 1 }]);
18
+ });
19
+ it("converts a type-only import", () => {
20
+ const { text } = rewrite(`import type { User } from "../../types/user";\n`);
21
+ expect(text).toBe(`import type { User } from "@/types/user";\n`);
22
+ });
23
+ it("converts a side-effect import", () => {
24
+ const { text } = rewrite(`import "../../styles/global.css";\n`);
25
+ expect(text).toBe(`import "@/styles/global.css";\n`);
26
+ });
27
+ it("converts a re-export", () => {
28
+ const { text } = rewrite(`export { api } from "../../lib/api";\nexport * from "../x";\n`);
29
+ expect(text).toBe(`export { api } from "@/lib/api";\nexport * from "@/pages/x";\n`);
30
+ });
31
+ it("converts an inline import type node", () => {
32
+ const { text } = rewrite(`type A = import("../../types/user").User;\n`);
33
+ expect(text).toBe(`type A = import("@/types/user").User;\n`);
34
+ });
35
+ it("converts a dynamic import", () => {
36
+ const { text } = rewrite(`const m = await import("../../pages/Dashboard");\n`);
37
+ expect(text).toBe(`const m = await import("@/pages/Dashboard");\n`);
38
+ });
39
+ it("converts vi.mock and its paired import together", () => {
40
+ const { text, changes } = rewrite(
41
+ `import { api } from "../../lib/api";\nvi.mock("../../lib/api");\n`,
42
+ );
43
+ expect(text).toBe(`import { api } from "@/lib/api";\nvi.mock("@/lib/api");\n`);
44
+ expect(changes.map((ch) => ch.line)).toEqual([1, 2]);
45
+ });
46
+ it("converts vi.doMock", () => {
47
+ const { text } = rewrite(`vi.doMock("../../lib/api", () => ({}));\n`);
48
+ expect(text).toBe(`vi.doMock("@/lib/api", () => ({}));\n`);
49
+ });
50
+ });
51
+
52
+ describe("rewriteSource — positions it must not touch", () => {
53
+ it("ignores a path in a plain variable", () => {
54
+ const src = `const p = "../../lib/api";\n`;
55
+ expect(rewrite(src).text).toBe(src);
56
+ });
57
+ it("ignores a path in a line comment", () => {
58
+ const src = `// see "../../lib/api"\nconst a = 1;\n`;
59
+ expect(rewrite(src).text).toBe(src);
60
+ });
61
+ it("ignores a path in a block comment above an import", () => {
62
+ const src = `/* was "../../lib/old" */\nimport { api } from "../../lib/api";\n`;
63
+ expect(rewrite(src).text).toBe(
64
+ `/* was "../../lib/old" */\nimport { api } from "@/lib/api";\n`,
65
+ );
66
+ });
67
+ it("ignores a template literal", () => {
68
+ const src = "const p = `../../lib/api`;\n";
69
+ expect(rewrite(src).text).toBe(src);
70
+ });
71
+ it("ignores a dynamic import with an interpolated specifier", () => {
72
+ const src = "const m = await import(`../../pages/${name}`);\n";
73
+ expect(rewrite(src).text).toBe(src);
74
+ });
75
+ it("ignores a dynamic import with a computed specifier", () => {
76
+ const src = `const m = await import(path);\n`;
77
+ expect(rewrite(src).text).toBe(src);
78
+ });
79
+ it("ignores require()", () => {
80
+ const src = `const api = require("../../lib/api");\n`;
81
+ expect(rewrite(src).text).toBe(src);
82
+ });
83
+ it("ignores a mock-looking call on another object", () => {
84
+ const src = `jest.mock("../../lib/api");\n`;
85
+ expect(rewrite(src).text).toBe(src);
86
+ });
87
+ it("ignores a sibling import", () => {
88
+ const src = `import { Row } from "./Row";\nimport css from "./Users.module.css";\n`;
89
+ expect(rewrite(src).text).toBe(src);
90
+ });
91
+ it("ignores a target outside the alias base", () => {
92
+ const src = `import cfg from "../../../vite.config";\n`;
93
+ expect(rewrite(src).text).toBe(src);
94
+ });
95
+ });
96
+
97
+ describe("rewriteSource — file scope", () => {
98
+ it("leaves a file outside the alias base untouched", () => {
99
+ const src = `import { x } from "../src/lib/api";\n`;
100
+ const { text, changes } = rewrite(src, "/proj/e2e/spec.ts");
101
+ expect(text).toBe(src);
102
+ expect(changes).toEqual([]);
103
+ });
104
+ });
105
+
106
+ describe("rewriteSource — mechanics", () => {
107
+ it("preserves single quotes", () => {
108
+ const { text } = rewrite(`import { api } from '../../lib/api';\n`);
109
+ expect(text).toBe(`import { api } from '@/lib/api';\n`);
110
+ });
111
+ it("rewrites several specifiers on one line without sliding offsets", () => {
112
+ const { text } = rewrite(
113
+ `import { a } from "../../lib/a";import { b } from "../../lib/b";\n`,
114
+ );
115
+ expect(text).toBe(`import { a } from "@/lib/a";import { b } from "@/lib/b";\n`);
116
+ });
117
+ it("reports the line of each change", () => {
118
+ const { changes } = rewrite(
119
+ `import { a } from "../../lib/a";\n\n\nimport { b } from "../../lib/b";\n`,
120
+ );
121
+ expect(changes.map((ch) => ch.line)).toEqual([1, 4]);
122
+ });
123
+ it("does not reorder or reformat anything", () => {
124
+ const { text } = rewrite(
125
+ `import {z} from "../../lib/z";\nimport {a} from "../../lib/a";\n`,
126
+ );
127
+ expect(text).toBe(`import {z} from "@/lib/z";\nimport {a} from "@/lib/a";\n`);
128
+ });
129
+ it("is idempotent", () => {
130
+ const first = rewrite(`import { api } from "../../lib/api";\n`);
131
+ const second = rewrite(first.text);
132
+ expect(second.text).toBe(first.text);
133
+ expect(second.changes).toEqual([]);
134
+ });
135
+ it("parses JSX without losing the specifiers after it", () => {
136
+ const { text } = rewrite(
137
+ `const el = <div className="x" />;\nimport { api } from "../../lib/api";\n`,
138
+ );
139
+ expect(text).toContain(`"@/lib/api"`);
140
+ });
141
+ it("parses a .ts file where < is a comparison", () => {
142
+ const { text } = rewrite(
143
+ `const ok = a < b;\nimport { api } from "../../lib/api";\n`,
144
+ "/proj/src/pages/admin/users.ts",
145
+ );
146
+ expect(text).toContain(`"@/lib/api"`);
147
+ });
148
+ });
@@ -0,0 +1,49 @@
1
+ // The single rule both rewriters share: when does a relative specifier become an
2
+ // alias specifier, and what does it become.
3
+ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
4
+
5
+ /**
6
+ * True when `filePath` sits inside `baseDir`.
7
+ *
8
+ * The gate exists because `@/` only resolves for files the bundler and the
9
+ * type-checker process through the alias. A file outside the base — a
10
+ * `vite.config.ts`, a script under `scripts/` — would get a specifier that
11
+ * resolves nowhere, and `vite.config.ts` in particular is loaded *without* the
12
+ * aliases it declares.
13
+ *
14
+ * @param {string} filePath - Absolute file path.
15
+ * @param {string} baseDir - Absolute alias base directory.
16
+ * @returns {boolean}
17
+ */
18
+ export function isInsideBase(filePath, baseDir) {
19
+ const rel = relative(baseDir, filePath);
20
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
21
+ }
22
+
23
+ /**
24
+ * Convert one relative specifier to its alias form, or report that it stays.
25
+ *
26
+ * Only specifiers that climb out of their own directory are converted: a sibling
27
+ * `./x` already reads as "next to this file", which is information the alias
28
+ * form throws away. A specifier that resolves *outside* the alias base is left
29
+ * alone — that is what keeps `../../vite.config` and `../../../scripts/x`
30
+ * intact.
31
+ *
32
+ * The extension is never touched: whatever the specifier carried (`.module.css`,
33
+ * `.svg`, nothing at all) survives, because resolution of extensionless imports
34
+ * is the bundler's business, not this codemod's.
35
+ *
36
+ * @param {object} params
37
+ * @param {string} params.spec - The original specifier text.
38
+ * @param {string} params.filePath - Absolute path of the importing file.
39
+ * @param {string} params.prefix - Alias prefix, e.g. `"@"`.
40
+ * @param {string} params.baseDir - Absolute alias base directory.
41
+ * @returns {string | null} The alias specifier, or `null` to leave it as is.
42
+ */
43
+ export function toAlias({ spec, filePath, prefix, baseDir }) {
44
+ if (!spec.startsWith("../")) return null;
45
+ const target = resolve(dirname(filePath), spec);
46
+ const rel = relative(baseDir, target);
47
+ if (!rel || rel.startsWith("..") || isAbsolute(rel)) return null;
48
+ return `${prefix}/${rel.split(sep).join("/")}`;
49
+ }