@sanity/ui 4.2.0 → 5.0.0-alpha.10

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 (339) hide show
  1. package/README.md +194 -28
  2. package/bin/sanity-ui.js +14 -0
  3. package/dist/_chunks-es/LazyRefractor.js +20 -0
  4. package/dist/_chunks-es/LazyRefractor.js.map +1 -0
  5. package/dist/cli/checks.js +148 -0
  6. package/dist/cli/config.js +23 -0
  7. package/dist/cli/detect.js +181 -0
  8. package/dist/cli/doctor.js +49 -0
  9. package/dist/cli/index.js +69 -0
  10. package/dist/cli/init.js +189 -0
  11. package/dist/cli/install.js +79 -0
  12. package/dist/cli/log.js +66 -0
  13. package/dist/cli/selfPackage.js +11 -0
  14. package/dist/cli/styles.js +185 -0
  15. package/dist/cli/test-utils.js +8 -0
  16. package/dist/cli/tsconfig.js +236 -0
  17. package/dist/cli/types.js +1 -0
  18. package/dist/cli/versions.js +45 -0
  19. package/dist/index.d.ts +1203 -1469
  20. package/dist/index.js +2123 -1989
  21. package/dist/index.js.map +1 -1
  22. package/dist/polyfills.d.ts +1 -0
  23. package/dist/polyfills.js +5 -0
  24. package/dist/polyfills.js.map +1 -0
  25. package/dist/styles.css +1 -547
  26. package/package.json +57 -58
  27. package/src/cli/checks.test.ts +38 -0
  28. package/src/cli/checks.ts +200 -0
  29. package/src/cli/config.ts +44 -0
  30. package/src/cli/detect.test.ts +191 -0
  31. package/src/cli/detect.ts +205 -0
  32. package/src/cli/doctor.test.ts +52 -0
  33. package/src/cli/doctor.ts +65 -0
  34. package/src/cli/index.ts +77 -0
  35. package/src/cli/init.ts +225 -0
  36. package/src/cli/install.test.ts +52 -0
  37. package/src/cli/install.ts +108 -0
  38. package/src/cli/log.ts +79 -0
  39. package/src/cli/selfPackage.ts +14 -0
  40. package/src/cli/styles.test.ts +130 -0
  41. package/src/cli/styles.ts +205 -0
  42. package/src/cli/test-utils.ts +10 -0
  43. package/src/cli/tsconfig.test.ts +174 -0
  44. package/src/cli/tsconfig.ts +282 -0
  45. package/src/cli/types.ts +77 -0
  46. package/src/cli/versions.test.ts +35 -0
  47. package/src/cli/versions.ts +41 -0
  48. package/src/components/badge/Badge.tsx +53 -0
  49. package/src/components/badge/badge.css +6 -0
  50. package/src/components/badge/badge.props.ts +31 -0
  51. package/src/components/box/Box.tsx +30 -0
  52. package/src/components/box/box.css +4 -0
  53. package/src/components/box/box.props.ts +29 -0
  54. package/src/components/button/Button.tsx +78 -0
  55. package/src/components/button/button.css +72 -0
  56. package/src/components/button/button.props.ts +122 -0
  57. package/src/components/card/Card.tsx +31 -0
  58. package/src/components/card/card.css +4 -0
  59. package/src/components/card/card.props.ts +58 -0
  60. package/src/components/checkbox/Checkbox.tsx +64 -0
  61. package/src/components/checkbox/checkbox.css +100 -0
  62. package/src/components/checkbox/checkbox.props.ts +33 -0
  63. package/src/components/code/Code.tsx +38 -0
  64. package/src/components/code/LazyRefractor.tsx +9 -0
  65. package/src/components/code/code.css +40 -0
  66. package/src/components/code/code.props.ts +44 -0
  67. package/src/components/container/Container.tsx +30 -0
  68. package/src/components/container/container.css +4 -0
  69. package/src/components/container/container.props.ts +28 -0
  70. package/src/components/dialog/Dialog.tsx +139 -0
  71. package/src/components/dialog/dialog.css +49 -0
  72. package/src/components/dialog/dialog.props.ts +45 -0
  73. package/src/components/divider/Divider.tsx +23 -0
  74. package/src/components/divider/divider.props.ts +9 -0
  75. package/src/components/eyebrow/Eyebrow.tsx +54 -0
  76. package/src/components/eyebrow/eyebrow.css +11 -0
  77. package/src/components/eyebrow/eyebrow.props.ts +28 -0
  78. package/src/components/flex/Flex.tsx +30 -0
  79. package/src/components/flex/flex.css +4 -0
  80. package/src/components/flex/flex.props.ts +33 -0
  81. package/src/components/grid/Grid.tsx +30 -0
  82. package/src/components/grid/grid.css +4 -0
  83. package/src/components/grid/grid.props.ts +33 -0
  84. package/src/components/h-stack/HStack.tsx +34 -0
  85. package/src/components/h-stack/hStack.props.ts +17 -0
  86. package/src/components/heading/Heading.tsx +54 -0
  87. package/src/components/heading/heading.css +9 -0
  88. package/src/components/heading/heading.props.ts +31 -0
  89. package/src/components/icon/Icon.tsx +24 -0
  90. package/src/components/icon/icon.css +17 -0
  91. package/src/components/icon/icon.props.ts +35 -0
  92. package/src/components/icon-button/IconButton.tsx +32 -0
  93. package/src/components/icon-button/iconButton.props.ts +31 -0
  94. package/src/components/index.css +28 -0
  95. package/src/components/indicator/Indicator.tsx +42 -0
  96. package/src/components/indicator/indicator.css +4 -0
  97. package/src/components/indicator/indicator.props.ts +24 -0
  98. package/src/components/indicator-stack/IndicatorStack.tsx +31 -0
  99. package/src/components/indicator-stack/indicator-stack.css +7 -0
  100. package/src/components/indicator-stack/indicatorStack.props.ts +15 -0
  101. package/src/components/inline/Inline.tsx +38 -0
  102. package/src/components/inline/inline.css +4 -0
  103. package/src/components/inline/inline.props.ts +100 -0
  104. package/src/components/label/Label.tsx +34 -0
  105. package/src/components/label/label.css +3 -0
  106. package/src/components/label/label.props.ts +25 -0
  107. package/src/components/link/Link.tsx +29 -0
  108. package/src/components/link/link.css +12 -0
  109. package/src/components/link/link.props.ts +23 -0
  110. package/src/components/list/List.tsx +167 -0
  111. package/src/components/list/list.css +25 -0
  112. package/src/components/list/list.props.ts +130 -0
  113. package/src/components/popover/Popover.tsx +85 -0
  114. package/src/components/popover/popover.css +21 -0
  115. package/src/components/popover/popover.props.ts +38 -0
  116. package/src/components/press-area/PressArea.tsx +29 -0
  117. package/src/components/press-area/press-area.css +22 -0
  118. package/src/components/press-area/pressArea.props.ts +16 -0
  119. package/src/components/radio/Radio.tsx +48 -0
  120. package/src/components/radio/radio.css +97 -0
  121. package/src/components/radio/radio.props.ts +25 -0
  122. package/src/components/skip-to-content/SkipToContent.tsx +35 -0
  123. package/src/components/skip-to-content/skip-to-content.css +9 -0
  124. package/src/components/skip-to-content/skipToContent.props.ts +25 -0
  125. package/src/components/spinner/Spinner.tsx +28 -0
  126. package/src/components/spinner/spinner.css +10 -0
  127. package/src/components/spinner/spinner.props.ts +19 -0
  128. package/src/components/switch/Switch.tsx +48 -0
  129. package/src/components/switch/switch.css +119 -0
  130. package/src/components/switch/switch.props.ts +25 -0
  131. package/src/components/text/Text.tsx +53 -0
  132. package/src/components/text/text.css +9 -0
  133. package/src/components/text/text.props.ts +28 -0
  134. package/src/components/tooltip/Tooltip.tsx +97 -0
  135. package/src/components/tooltip/tooltip.css +31 -0
  136. package/src/components/tooltip/tooltip.props.ts +36 -0
  137. package/src/components/tooltip-group/TooltipGroup.tsx +64 -0
  138. package/src/components/tooltip-group/tooltipGroup.props.ts +15 -0
  139. package/src/components/v-stack/VStack.tsx +32 -0
  140. package/src/components/v-stack/vStack.props.ts +17 -0
  141. package/src/components/visually-hidden/VisuallyHidden.tsx +29 -0
  142. package/src/components/visually-hidden/visually-hidden.css +9 -0
  143. package/src/components/visually-hidden/visuallyHidden.props.ts +23 -0
  144. package/src/css/classes/dynamic/flex-child.css +5 -0
  145. package/src/css/classes/dynamic/grid-child.css +8 -0
  146. package/src/css/classes/dynamic/grid-parent.css +6 -0
  147. package/src/css/classes/dynamic/height.css +5 -0
  148. package/src/css/classes/dynamic/index.css +7 -0
  149. package/src/css/classes/dynamic/line-clamp.css +5 -0
  150. package/src/css/classes/dynamic/readme.md +3 -0
  151. package/src/css/classes/dynamic/width.css +7 -0
  152. package/src/css/classes/dynamic/z-index.css +3 -0
  153. package/src/css/classes/generic/display.css +10 -0
  154. package/src/css/classes/generic/flex.css +24 -0
  155. package/src/css/classes/generic/grid.css +7 -0
  156. package/src/css/classes/generic/index.css +7 -0
  157. package/src/css/classes/generic/overflow.css +19 -0
  158. package/src/css/classes/generic/placement.css +202 -0
  159. package/src/css/classes/generic/position.css +7 -0
  160. package/src/css/classes/generic/readme.md +3 -0
  161. package/src/css/classes/generic/text-align.css +5 -0
  162. package/src/css/classes/index.css +4 -0
  163. package/src/css/classes/local/animation.css +4 -0
  164. package/src/css/classes/local/border.css +20 -0
  165. package/src/css/classes/local/index.css +6 -0
  166. package/src/css/classes/local/readme.md +3 -0
  167. package/src/css/classes/local/text-overflow.css +5 -0
  168. package/src/css/classes/local/text-trim.css +45 -0
  169. package/src/css/classes/local/tone.css +75 -0
  170. package/src/css/classes/local/top-anchor.css +4 -0
  171. package/src/css/classes/system/color.css +223 -0
  172. package/src/css/classes/system/container.css +8 -0
  173. package/src/css/classes/system/font-family.css +2 -0
  174. package/src/css/classes/system/font-weight.css +4 -0
  175. package/src/css/classes/system/gap.css +37 -0
  176. package/src/css/classes/system/index.css +10 -0
  177. package/src/css/classes/system/inset.css +56 -0
  178. package/src/css/classes/system/margin.css +148 -0
  179. package/src/css/classes/system/padding.css +78 -0
  180. package/src/css/classes/system/radius.css +10 -0
  181. package/src/css/classes/system/readme.md +3 -0
  182. package/src/css/classes/system/shadow.css +6 -0
  183. package/src/css/classes/system/typography.css +31 -0
  184. package/src/css/global/font-face.css +20 -0
  185. package/src/css/global/index.css +2 -0
  186. package/src/css/global/reset.css +76 -0
  187. package/src/css/tokens/index.css +3 -0
  188. package/src/css/tokens/opacity.css +6 -0
  189. package/src/css/tokens/semantic.css +25 -0
  190. package/src/css/tokens/surface.css +27 -0
  191. package/src/hooks/useIsClient.ts +10 -0
  192. package/src/index.css +18 -0
  193. package/src/index.ts +65 -0
  194. package/src/polyfills.ts +15 -0
  195. package/src/props/border.ts +51 -0
  196. package/src/props/flexChild.ts +29 -0
  197. package/src/props/flexParent.ts +47 -0
  198. package/src/props/gap.ts +31 -0
  199. package/src/props/gridChild.ts +50 -0
  200. package/src/props/gridParent.ts +45 -0
  201. package/src/props/height.ts +29 -0
  202. package/src/props/layout.ts +44 -0
  203. package/src/props/margin.ts +59 -0
  204. package/src/props/overflow.ts +30 -0
  205. package/src/props/padding.ts +59 -0
  206. package/src/props/placement.ts +16 -0
  207. package/src/props/position.ts +52 -0
  208. package/src/props/shadow.ts +15 -0
  209. package/src/props/tone.ts +15 -0
  210. package/src/props/typography.ts +62 -0
  211. package/src/props/width.ts +29 -0
  212. package/src/props/zIndex.ts +15 -0
  213. package/src/styles.css.d.ts +1 -0
  214. package/src/styles.css.node.js +2 -0
  215. package/src/styles.css.test.ts +24 -0
  216. package/src/types/Button.ts +8 -0
  217. package/src/types/Code.ts +6 -0
  218. package/src/types/Container.ts +3 -0
  219. package/src/types/Density.ts +3 -0
  220. package/src/types/Display.ts +11 -0
  221. package/src/types/Eyebrow.ts +3 -0
  222. package/src/types/Flex.ts +22 -0
  223. package/src/types/FontWeight.ts +3 -0
  224. package/src/types/Grid.ts +3 -0
  225. package/src/types/Heading.ts +6 -0
  226. package/src/types/Icon.ts +3 -0
  227. package/src/types/Interactive.ts +8 -0
  228. package/src/types/List.ts +2 -0
  229. package/src/types/Overflow.ts +3 -0
  230. package/src/types/Placement.ts +16 -0
  231. package/src/types/Position.ts +3 -0
  232. package/src/types/PropDef.ts +73 -0
  233. package/src/types/Radius.ts +3 -0
  234. package/src/types/Responsive.ts +10 -0
  235. package/src/types/Shadow.ts +3 -0
  236. package/src/types/Space.ts +15 -0
  237. package/src/types/Text.ts +3 -0
  238. package/src/types/TextAlign.ts +3 -0
  239. package/src/types/Tone.ts +3 -0
  240. package/src/utils/getProps.test.ts +156 -0
  241. package/src/utils/getProps.ts +156 -0
  242. package/src/utils/mergeTriggerProps.ts +66 -0
  243. package/src/utils/renderPortal.tsx +14 -0
  244. package/src/utils/suffixClassName.ts +5 -0
  245. package/src/version.ts +3 -0
  246. package/LICENSE +0 -21
  247. package/dist/_system-CTe2lb5l.d.ts +0 -40
  248. package/dist/_system-CTe2lb5l.d.ts.map +0 -1
  249. package/dist/arrow-BmX6qbAS.js +0 -170
  250. package/dist/arrow-BmX6qbAS.js.map +0 -1
  251. package/dist/autocomplete.d.ts +0 -140
  252. package/dist/autocomplete.d.ts.map +0 -1
  253. package/dist/autocomplete.js +0 -470
  254. package/dist/autocomplete.js.map +0 -1
  255. package/dist/breadcrumbs.d.ts +0 -18
  256. package/dist/breadcrumbs.d.ts.map +0 -1
  257. package/dist/breadcrumbs.js +0 -119
  258. package/dist/breadcrumbs.js.map +0 -1
  259. package/dist/button-BNyKSDyA.js +0 -180
  260. package/dist/button-BNyKSDyA.js.map +0 -1
  261. package/dist/button-dnqMFkWg.d.ts +0 -76
  262. package/dist/button-dnqMFkWg.d.ts.map +0 -1
  263. package/dist/card-C0a0wQTc.d.ts +0 -7
  264. package/dist/card-C0a0wQTc.d.ts.map +0 -1
  265. package/dist/code.d.ts +0 -20
  266. package/dist/code.d.ts.map +0 -1
  267. package/dist/code.js +0 -90
  268. package/dist/code.js.map +0 -1
  269. package/dist/codeFontStyle-84SMhFtI.js +0 -11
  270. package/dist/codeFontStyle-84SMhFtI.js.map +0 -1
  271. package/dist/component-DVlwaWB-.d.ts +0 -25
  272. package/dist/component-DVlwaWB-.d.ts.map +0 -1
  273. package/dist/element-BYgCE_iK.js +0 -51
  274. package/dist/element-BYgCE_iK.js.map +0 -1
  275. package/dist/factory-BzXTX8_Q.js +0 -3056
  276. package/dist/factory-BzXTX8_Q.js.map +0 -1
  277. package/dist/flex-DmbDG2ks.js +0 -76
  278. package/dist/flex-DmbDG2ks.js.map +0 -1
  279. package/dist/font-EtfxKPUz.d.ts +0 -49
  280. package/dist/font-EtfxKPUz.d.ts.map +0 -1
  281. package/dist/getElementRef-Jw7gY0eV.js +0 -102
  282. package/dist/getElementRef-Jw7gY0eV.js.map +0 -1
  283. package/dist/getScopedTheme-C-sZsyAz.js +0 -267
  284. package/dist/getScopedTheme-C-sZsyAz.js.map +0 -1
  285. package/dist/getTheme_v2-y09KqtEb.js +0 -487
  286. package/dist/getTheme_v2-y09KqtEb.js.map +0 -1
  287. package/dist/grid-6JSgMj1O.js +0 -115
  288. package/dist/grid-6JSgMj1O.js.map +0 -1
  289. package/dist/helpers-C8xxXAQC.js +0 -63
  290. package/dist/helpers-C8xxXAQC.js.map +0 -1
  291. package/dist/hotkeys-CaHsQlsO.js +0 -168
  292. package/dist/hotkeys-CaHsQlsO.js.map +0 -1
  293. package/dist/index.d.ts.map +0 -1
  294. package/dist/layer-Bl5R3uNR.js +0 -61
  295. package/dist/layer-Bl5R3uNR.js.map +0 -1
  296. package/dist/layer-ChlP9Ik6.d.ts +0 -242
  297. package/dist/layer-ChlP9Ik6.d.ts.map +0 -1
  298. package/dist/menu.d.ts +0 -146
  299. package/dist/menu.d.ts.map +0 -1
  300. package/dist/menu.js +0 -626
  301. package/dist/menu.js.map +0 -1
  302. package/dist/popover-B7-28L8y.js +0 -408
  303. package/dist/popover-B7-28L8y.js.map +0 -1
  304. package/dist/popover-CMlQeKn0.d.ts +0 -96
  305. package/dist/popover-CMlQeKn0.d.ts.map +0 -1
  306. package/dist/popover.d.ts +0 -2
  307. package/dist/popover.js +0 -3
  308. package/dist/refractor-CC48DwHi.js +0 -20
  309. package/dist/refractor-CC48DwHi.js.map +0 -1
  310. package/dist/responsiveFont-CilwaORG.js +0 -55
  311. package/dist/responsiveFont-CilwaORG.js.map +0 -1
  312. package/dist/rgba-BSYaEUW6.d.ts +0 -846
  313. package/dist/rgba-BSYaEUW6.d.ts.map +0 -1
  314. package/dist/selectable-B0GUIL74.d.ts +0 -7
  315. package/dist/selectable-B0GUIL74.d.ts.map +0 -1
  316. package/dist/stack-rsNFPq2-.js +0 -112
  317. package/dist/stack-rsNFPq2-.js.map +0 -1
  318. package/dist/styles-css.d.ts +0 -2
  319. package/dist/styles-css.js +0 -4
  320. package/dist/textInput-pSfQjkmx.js +0 -313
  321. package/dist/textInput-pSfQjkmx.js.map +0 -1
  322. package/dist/theme.d.ts +0 -280
  323. package/dist/theme.d.ts.map +0 -1
  324. package/dist/theme.js +0 -55
  325. package/dist/theme.js.map +0 -1
  326. package/dist/toast.d.ts +0 -73
  327. package/dist/toast.d.ts.map +0 -1
  328. package/dist/toast.js +0 -302
  329. package/dist/toast.js.map +0 -1
  330. package/dist/tooltip.d.ts +0 -88
  331. package/dist/tooltip.d.ts.map +0 -1
  332. package/dist/tooltip.js +0 -435
  333. package/dist/tooltip.js.map +0 -1
  334. package/dist/useClickOutsideEvent-wGLGWBZN.js +0 -36
  335. package/dist/useClickOutsideEvent-wGLGWBZN.js.map +0 -1
  336. package/dist/useElementSize-BJvbGHq8.js +0 -51
  337. package/dist/useElementSize-BJvbGHq8.js.map +0 -1
  338. package/dist/useLayer-D1usxIE0.js +0 -712
  339. package/dist/useLayer-D1usxIE0.js.map +0 -1
@@ -0,0 +1,205 @@
1
+ import {existsSync, readFileSync} from 'node:fs'
2
+ import {join} from 'node:path'
3
+
4
+ import type {
5
+ Detected,
6
+ EntryInfo,
7
+ Framework,
8
+ PackageJson,
9
+ PackageManager,
10
+ ReactInfo,
11
+ } from './types.js'
12
+ import {parseVersion} from './versions.js'
13
+
14
+ export const PACKAGE_NAME = '@sanity/ui'
15
+
16
+ export const LEGACY_COEXISTENCE_HINT =
17
+ 'To add v5 alongside an existing @sanity/ui install, use a package alias — see the README section "For apps using Sanity UI v3".'
18
+
19
+ /**
20
+ * Framework ids map 1:1 to the `framework` field written into sanity-ui.json.
21
+ * Each carries the ordered list of candidate entry files where the stylesheet
22
+ * import belongs.
23
+ */
24
+ const FRAMEWORK_ENTRIES: Record<Framework, string[]> = {
25
+ 'next-app': ['app/layout.tsx', 'app/layout.jsx', 'src/app/layout.tsx', 'src/app/layout.jsx'],
26
+ 'next-pages': ['pages/_app.tsx', 'pages/_app.jsx', 'src/pages/_app.tsx', 'src/pages/_app.jsx'],
27
+ 'react-router': ['app/root.tsx', 'app/root.jsx'],
28
+ 'remix': ['app/root.tsx', 'app/root.jsx'],
29
+ 'vite': ['src/main.tsx', 'src/main.jsx', 'src/main.ts'],
30
+ 'astro': [],
31
+ 'react': ['src/index.tsx', 'src/main.tsx', 'src/index.jsx', 'src/main.jsx'],
32
+ }
33
+
34
+ export function readPackageJson(cwd: string): PackageJson | null {
35
+ const file = join(cwd, 'package.json')
36
+ if (!existsSync(file)) return null
37
+ try {
38
+ return JSON.parse(readFileSync(file, 'utf8')) as PackageJson
39
+ } catch {
40
+ return null
41
+ }
42
+ }
43
+
44
+ export function allDeps(pkg: PackageJson | null): Record<string, string | undefined> {
45
+ if (!pkg) return {}
46
+ return {...pkg.dependencies, ...pkg.devDependencies, ...pkg.peerDependencies}
47
+ }
48
+
49
+ export function detectPackageManager(cwd: string): PackageManager {
50
+ if (existsSync(join(cwd, 'bun.lockb')) || existsSync(join(cwd, 'bun.lock'))) return 'bun'
51
+ if (existsSync(join(cwd, 'pnpm-lock.yaml'))) return 'pnpm'
52
+ if (existsSync(join(cwd, 'yarn.lock'))) return 'yarn'
53
+ if (existsSync(join(cwd, 'package-lock.json'))) return 'npm'
54
+
55
+ const declared = readPackageJson(cwd)?.packageManager
56
+ if (typeof declared === 'string') {
57
+ const name = declared.split('@')[0]
58
+ if (name === 'pnpm' || name === 'yarn' || name === 'npm' || name === 'bun') return name
59
+ }
60
+ return 'npm'
61
+ }
62
+
63
+ export function detectFramework(
64
+ cwd: string,
65
+ pkg: PackageJson | null = readPackageJson(cwd),
66
+ ): Framework {
67
+ const deps = allDeps(pkg)
68
+ const has = (name: string): boolean => Boolean(deps[name])
69
+ const dir = (relative: string): boolean => existsSync(join(cwd, relative))
70
+
71
+ if (has('next')) {
72
+ if (dir('app') || dir('src/app')) return 'next-app'
73
+ if (dir('pages') || dir('src/pages')) return 'next-pages'
74
+ return 'next-app'
75
+ }
76
+ if (has('@remix-run/react') || has('@remix-run/dev')) return 'remix'
77
+ if (has('@react-router/dev') || has('react-router')) return 'react-router'
78
+ if (has('astro')) return 'astro'
79
+ if (has('vite')) return 'vite'
80
+ return 'react'
81
+ }
82
+
83
+ export function detectTypeScript(
84
+ cwd: string,
85
+ pkg: PackageJson | null = readPackageJson(cwd),
86
+ ): boolean {
87
+ return existsSync(join(cwd, 'tsconfig.json')) || Boolean(allDeps(pkg)['typescript'])
88
+ }
89
+
90
+ /**
91
+ * Returns the first existing candidate entry file, otherwise the first
92
+ * candidate as the place to create one. Astro has no fixed entry, so we look
93
+ * for a layout component.
94
+ */
95
+ export function detectEntry(cwd: string, framework: Framework): EntryInfo {
96
+ if (framework === 'astro') return detectAstroLayout(cwd)
97
+
98
+ const candidates = FRAMEWORK_ENTRIES[framework]
99
+ const existing = candidates.find((rel) => existsSync(join(cwd, rel)))
100
+ return {entry: existing ?? candidates[0] ?? 'src/index.tsx', exists: Boolean(existing)}
101
+ }
102
+
103
+ function detectAstroLayout(cwd: string): EntryInfo {
104
+ const candidates = [
105
+ 'src/layouts/Layout.astro',
106
+ 'src/layouts/BaseLayout.astro',
107
+ 'src/layouts/index.astro',
108
+ 'src/pages/index.astro',
109
+ ]
110
+ const existing = candidates.find((rel) => existsSync(join(cwd, rel)))
111
+ return {entry: existing ?? candidates[0] ?? 'src/layouts/Layout.astro', exists: Boolean(existing)}
112
+ }
113
+
114
+ /**
115
+ * Resolves the installed React version from node_modules, falling back to the
116
+ * declared range in package.json (carets/tildes stripped).
117
+ */
118
+ export function detectReactVersion(
119
+ cwd: string,
120
+ pkg: PackageJson | null = readPackageJson(cwd),
121
+ ): ReactInfo {
122
+ const installed = readInstalledVersion(cwd, 'react')
123
+ if (installed) return {version: installed, source: 'installed'}
124
+
125
+ const declared = allDeps(pkg)['react']
126
+ if (typeof declared === 'string') {
127
+ return {version: declared.replace(/^[\^~>=<\s]+/, ''), source: 'declared'}
128
+ }
129
+ return {version: null, source: 'missing'}
130
+ }
131
+
132
+ function readInstalledVersion(cwd: string, name: string): string | null {
133
+ const file = join(cwd, 'node_modules', name, 'package.json')
134
+ if (!existsSync(file)) return null
135
+ try {
136
+ const pkg = JSON.parse(readFileSync(file, 'utf8')) as PackageJson
137
+ return pkg.version ?? null
138
+ } catch {
139
+ return null
140
+ }
141
+ }
142
+
143
+ export function isPackageInstalled(cwd: string, name: string): boolean {
144
+ return existsSync(join(cwd, 'node_modules', name, 'package.json'))
145
+ }
146
+
147
+ export function readInstalledPackageVersion(cwd: string, name: string): string | null {
148
+ return readInstalledVersion(cwd, name)
149
+ }
150
+
151
+ export function isLegacyUiVersion(version: unknown): boolean {
152
+ const major = parseVersion(version)?.[0]
153
+ return major !== undefined && major < 5
154
+ }
155
+
156
+ function detectAliasedInstall(deps: Record<string, string | undefined>): string | null {
157
+ for (const [name, spec] of Object.entries(deps)) {
158
+ if (typeof spec === 'string' && /^npm:@sanity\/ui@(5(?:\..*)?|alpha)$/.test(spec)) return name
159
+ }
160
+ return null
161
+ }
162
+
163
+ /** Reads the major from a declared range, skipping workspace/catalog/link specs. */
164
+ function declaredMajor(spec: string | undefined): number | null {
165
+ if (typeof spec !== 'string') return null
166
+ if (/^(workspace:|catalog:|link:|file:)/.test(spec)) return null
167
+
168
+ const npmAlias = spec.match(/^npm:@sanity\/ui@(\d+)/)
169
+ if (npmAlias) return Number(npmAlias[1])
170
+
171
+ return parseVersion(spec.replace(/^[\^~>=<\s]+/, ''))?.[0] ?? null
172
+ }
173
+
174
+ function detectLegacyInstall(cwd: string, deps: Record<string, string | undefined>): string | null {
175
+ const installed = readInstalledPackageVersion(cwd, PACKAGE_NAME)
176
+ if (installed && isLegacyUiVersion(installed)) {
177
+ return installed
178
+ }
179
+
180
+ const declared = deps[PACKAGE_NAME]
181
+ const major = declaredMajor(declared)
182
+ if (major !== null && major < 5) {
183
+ return declared ?? 'unknown'
184
+ }
185
+
186
+ return null
187
+ }
188
+
189
+ export function detect(cwd: string): Detected {
190
+ const pkg = readPackageJson(cwd)
191
+ const deps = allDeps(pkg)
192
+ const framework = detectFramework(cwd, pkg)
193
+ const entry = detectEntry(cwd, framework)
194
+ const typescript = detectTypeScript(cwd, pkg) || (entry.exists && /\.tsx?$/.test(entry.entry))
195
+ return {
196
+ pkg,
197
+ packageManager: detectPackageManager(cwd),
198
+ framework,
199
+ typescript,
200
+ ...entry,
201
+ react: detectReactVersion(cwd, pkg),
202
+ aliasedInstall: detectAliasedInstall(deps),
203
+ legacyInstall: detectLegacyInstall(cwd, deps),
204
+ }
205
+ }
@@ -0,0 +1,52 @@
1
+ import {mkdtempSync, rmSync, writeFileSync} from 'node:fs'
2
+ import {tmpdir} from 'node:os'
3
+ import {join} from 'node:path'
4
+
5
+ import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
6
+
7
+ import * as checks from './checks.js'
8
+ import {runDoctor} from './doctor.js'
9
+ import {writeInstalledUiVersion} from './test-utils.js'
10
+
11
+ describe('runDoctor', () => {
12
+ let dir: string
13
+ let output: string
14
+
15
+ beforeEach(() => {
16
+ dir = mkdtempSync(join(tmpdir(), 'sui-doctor-'))
17
+ output = ''
18
+ vi.spyOn(process.stdout, 'write').mockImplementation((chunk) => {
19
+ output += String(chunk)
20
+ return true
21
+ })
22
+ })
23
+
24
+ afterEach(() => {
25
+ rmSync(dir, {recursive: true, force: true})
26
+ vi.restoreAllMocks()
27
+ })
28
+
29
+ it('warns and skips checks when @sanity/ui is pre-v5', () => {
30
+ writeFileSync(join(dir, 'package.json'), JSON.stringify({dependencies: {}}))
31
+ writeInstalledUiVersion(dir, '3.5.1')
32
+ const collectChecks = vi.spyOn(checks, 'collectChecks')
33
+
34
+ expect(runDoctor({cwd: dir})).toBe(0)
35
+ expect(output).toMatch(/@sanity\/ui@\^?3\.5\.1 \(pre-v5\)/)
36
+ expect(output).toMatch(/doctor checks v5 installs only/)
37
+ expect(collectChecks).not.toHaveBeenCalled()
38
+ })
39
+
40
+ it('warns and skips checks when v5 is installed under an alias', () => {
41
+ writeFileSync(
42
+ join(dir, 'package.json'),
43
+ JSON.stringify({dependencies: {ui5: 'npm:@sanity/ui@alpha'}}),
44
+ )
45
+ const collectChecks = vi.spyOn(checks, 'collectChecks')
46
+
47
+ expect(runDoctor({cwd: dir})).toBe(0)
48
+ expect(output).toMatch(/alias "ui5"/)
49
+ expect(output).toMatch(/doctor checks direct @sanity\/ui v5 installs only/)
50
+ expect(collectChecks).not.toHaveBeenCalled()
51
+ })
52
+ })
@@ -0,0 +1,65 @@
1
+ import {collectChecks} from './checks.js'
2
+ import {detect, LEGACY_COEXISTENCE_HINT, PACKAGE_NAME} from './detect.js'
3
+ import {color, fail, heading, info, success, warn} from './log.js'
4
+ import type {Check, CliOptions} from './types.js'
5
+
6
+ export function printChecks(checks: Check[]): void {
7
+ for (const c of checks) {
8
+ const label = `${color.bold(c.title)}${c.detail ? ` ${color.dim(`— ${c.detail}`)}` : ''}`
9
+ if (c.status === 'pass') success(label)
10
+ else if (c.status === 'warn') warn(label)
11
+ else fail(label)
12
+
13
+ if (c.status !== 'pass' && c.fix) {
14
+ info(` ${color.dim('fix:')} ${c.fix}`)
15
+ }
16
+ }
17
+ }
18
+
19
+ /** Runs every check and prints it. Returns 1 only when something fails; warnings still pass. */
20
+ export function runDoctor({cwd}: Pick<CliOptions, 'cwd'>): number {
21
+ const detected = detect(cwd)
22
+
23
+ heading('Sanity UI doctor')
24
+
25
+ if (detected.aliasedInstall) {
26
+ warn(
27
+ `This project installs v5 under the alias "${detected.aliasedInstall}". doctor checks direct @sanity/ui v5 installs only.`,
28
+ )
29
+ info(` ${color.dim('hint:')} ${LEGACY_COEXISTENCE_HINT}`)
30
+ return 0
31
+ }
32
+
33
+ if (detected.legacyInstall) {
34
+ warn(
35
+ `This project has ${PACKAGE_NAME}@${detected.legacyInstall} (pre-v5). doctor checks v5 installs only.`,
36
+ )
37
+ info(` ${color.dim('hint:')} ${LEGACY_COEXISTENCE_HINT}`)
38
+ return 0
39
+ }
40
+
41
+ const {checks} = collectChecks(cwd)
42
+ info(
43
+ color.dim(
44
+ `framework: ${detected.framework} · package manager: ${detected.packageManager} · entry: ${detected.entry}`,
45
+ ),
46
+ )
47
+ info('')
48
+
49
+ printChecks(checks)
50
+
51
+ const failed = checks.filter((c) => c.status === 'fail')
52
+ const warned = checks.filter((c) => c.status === 'warn')
53
+
54
+ info('')
55
+ if (failed.length === 0 && warned.length === 0) {
56
+ success('Everything looks good.')
57
+ return 0
58
+ }
59
+ if (failed.length === 0) {
60
+ warn(`${warned.length} warning${warned.length === 1 ? '' : 's'}, no blocking issues.`)
61
+ return 0
62
+ }
63
+ fail(`${failed.length} issue${failed.length === 1 ? '' : 's'} found. See fixes above.`)
64
+ return 1
65
+ }
@@ -0,0 +1,77 @@
1
+ import {resolve} from 'node:path'
2
+ import process from 'node:process'
3
+ import {parseArgs} from 'node:util'
4
+
5
+ import {runDoctor} from './doctor.js'
6
+ import {runInit} from './init.js'
7
+ import {color, fail, info} from './log.js'
8
+ import {readSelfPackage} from './selfPackage.js'
9
+
10
+ const HELP = `${color.bold('@sanity/ui')} setup CLI
11
+
12
+ ${color.bold('Usage')}
13
+ npx @sanity/ui@alpha <command> [options]
14
+
15
+ ${color.bold('Commands')}
16
+ init Scaffold a correct Sanity UI install (deps, stylesheet, tsconfig)
17
+ doctor Check an existing setup and report fixes
18
+
19
+ ${color.bold('Options')}
20
+ --dry Print the plan and exit without changing anything (init)
21
+ --yes, -y Accept every prompt with its default (non-interactive)
22
+ --cwd <dir> Run against another directory (default: current directory)
23
+ --version Print the CLI version
24
+ --help, -h Show this help
25
+ `
26
+
27
+ /**
28
+ * Parses arguments and dispatches to a command. `parseArgs` runs in strict mode,
29
+ * so an unknown flag throws from here and is caught by the bin wrapper. Exit
30
+ * codes: 0 on success or an explicit `--help`, 1 on misuse (no or unknown command).
31
+ */
32
+ export async function main(argv: string[]): Promise<number> {
33
+ const {values, positionals} = parseArgs({
34
+ args: argv,
35
+ allowPositionals: true,
36
+ options: {
37
+ dry: {type: 'boolean', default: false},
38
+ yes: {type: 'boolean', short: 'y', default: false},
39
+ cwd: {type: 'string'},
40
+ version: {type: 'boolean', default: false},
41
+ help: {type: 'boolean', short: 'h', default: false},
42
+ },
43
+ })
44
+
45
+ if (values.version) {
46
+ info(readSelfPackage().version ?? 'unknown')
47
+ return 0
48
+ }
49
+
50
+ const command = positionals[0]
51
+
52
+ if (values.help) {
53
+ info(HELP)
54
+ return 0
55
+ }
56
+ if (!command) {
57
+ info(HELP)
58
+ return 1
59
+ }
60
+
61
+ const options = {
62
+ dry: values.dry ?? false,
63
+ yes: values.yes ?? false,
64
+ cwd: values.cwd ? resolve(process.cwd(), values.cwd) : process.cwd(),
65
+ }
66
+
67
+ switch (command) {
68
+ case 'init':
69
+ return runInit(options)
70
+ case 'doctor':
71
+ return runDoctor(options)
72
+ default:
73
+ fail(`Unknown command: ${command}`)
74
+ info(HELP)
75
+ return 1
76
+ }
77
+ }
@@ -0,0 +1,225 @@
1
+ import {basename} from 'node:path'
2
+ import process from 'node:process'
3
+
4
+ import {verifyAfterInit} from './checks.js'
5
+ import {buildConfig, CONFIG_FILE, configExists, writeConfig} from './config.js'
6
+ import {detect, LEGACY_COEXISTENCE_HINT, PACKAGE_NAME} from './detect.js'
7
+ import {printChecks} from './doctor.js'
8
+ import {installCommand, resolveInstallSpecs, resolveTypeSpecs, runInstall} from './install.js'
9
+ import {color, confirm, fail, heading, info, step, success, warn} from './log.js'
10
+ import {inspectStylesheet, STYLES_SPECIFIER, wireStylesheet} from './styles.js'
11
+ import type {StylesheetInfo} from './styles.js'
12
+ import {inspectTsconfig, reconcileTsconfig} from './tsconfig.js'
13
+ import type {TsconfigInspection} from './tsconfig.js'
14
+ import type {CliOptions, Detected} from './types.js'
15
+ import {meetsNode, meetsReact, NODE_REQUIREMENT, REACT_MIN} from './versions.js'
16
+
17
+ interface Plan {
18
+ specs: string[]
19
+ typeSpecs: string[]
20
+ install: string
21
+ stylesheet: StylesheetInfo
22
+ tsconfig: TsconfigInspection | null
23
+ }
24
+
25
+ /**
26
+ * Prints the React and Node checks. Missing React only warns, since the user may
27
+ * be scaffolding before installing it; an out-of-range React or Node sets
28
+ * `blocking`. `blocking` never hard-stops init. It only triggers a louder warning
29
+ * before the apply confirmation, so the user stays in control either way.
30
+ */
31
+ function reportPrereqs({react}: Detected): {blocking: boolean} {
32
+ let blocking = false
33
+
34
+ const reactVersion = react.version
35
+ if (!reactVersion) {
36
+ warn(`React not detected. This package needs React >= ${REACT_MIN}.`)
37
+ } else if (meetsReact(reactVersion)) {
38
+ success(`React ${reactVersion} meets the >= ${REACT_MIN} requirement.`)
39
+ } else {
40
+ warn(`React ${reactVersion} is below ${REACT_MIN}. Components may not render correctly.`)
41
+ info(` ${color.dim('fix:')} Upgrade React to >= ${REACT_MIN}`)
42
+ blocking = true
43
+ }
44
+
45
+ const nodeVersion = process.versions.node
46
+ if (meetsNode(nodeVersion)) {
47
+ success(`Node ${nodeVersion} meets the ${NODE_REQUIREMENT} requirement.`)
48
+ } else {
49
+ warn(`Node ${nodeVersion} is outside ${NODE_REQUIREMENT}. Install may behave unexpectedly.`)
50
+ info(` ${color.dim('fix:')} Use Node ${NODE_REQUIREMENT}`)
51
+ blocking = true
52
+ }
53
+
54
+ return {blocking}
55
+ }
56
+
57
+ function parseReactMajor(version: string | null): number {
58
+ const match = typeof version === 'string' ? version.match(/\d+/) : null
59
+ return match ? Number(match[0]) : 19
60
+ }
61
+
62
+ function buildPlan(cwd: string, detected: Detected): Plan {
63
+ const specs = resolveInstallSpecs()
64
+ const {command, args} = installCommand(detected.packageManager, specs)
65
+ const reactMajor = parseReactMajor(detected.react.version)
66
+ const typeSpecs = detected.typescript ? resolveTypeSpecs(cwd, {reactMajor}) : []
67
+ const stylesheet = inspectStylesheet(cwd, detected.entry)
68
+ const tsconfig = detected.typescript ? inspectTsconfig(cwd) : null
69
+
70
+ return {specs, typeSpecs, install: `${command} ${args.join(' ')}`, stylesheet, tsconfig}
71
+ }
72
+
73
+ function printPlan(cwd: string, detected: Detected, plan: Plan): void {
74
+ heading('Plan')
75
+
76
+ step(`Install: ${color.bold(plan.install)}`)
77
+
78
+ if (plan.typeSpecs.length > 0) {
79
+ step(`Dev types: ${color.bold(plan.typeSpecs.join(' '))}`)
80
+ }
81
+
82
+ if (plan.stylesheet.imported) {
83
+ step(`Stylesheet: already imported in ${plan.stylesheet.importedIn ?? detected.entry}`)
84
+ } else if (plan.stylesheet.exists) {
85
+ step(`Stylesheet: add ${color.bold(`import '${STYLES_SPECIFIER}'`)} to ${detected.entry}`)
86
+ } else {
87
+ step(`Stylesheet: entry ${detected.entry} not found, will skip (add the import manually)`)
88
+ }
89
+
90
+ if (!detected.typescript || !plan.tsconfig) {
91
+ step('tsconfig: no TypeScript detected, skipping')
92
+ } else if (!plan.tsconfig.exists) {
93
+ step('tsconfig: create tsconfig.json with required options')
94
+ } else if (plan.tsconfig.changes.length === 0) {
95
+ step('tsconfig: already has required options')
96
+ } else {
97
+ const summary = plan.tsconfig.changes.map((c) => c.key).join(', ')
98
+ step(`tsconfig: update ${basename(plan.tsconfig.file)} (${summary})`)
99
+ }
100
+
101
+ step(configExists(cwd) ? `${CONFIG_FILE}: overwrite` : `${CONFIG_FILE}: create`)
102
+ }
103
+
104
+ /**
105
+ * The init lifecycle: detect the project, report prerequisites, build and print
106
+ * the plan, confirm, apply each step, then verify. `--dry` stops after the plan;
107
+ * `--yes` accepts the confirmation without prompting.
108
+ */
109
+ export async function runInit(options: CliOptions): Promise<number> {
110
+ const {cwd, dry, yes} = options
111
+ const detected = detect(cwd)
112
+
113
+ heading('Sanity UI init')
114
+ info(
115
+ color.dim(
116
+ `framework: ${detected.framework} · package manager: ${detected.packageManager} · entry: ${detected.entry} · typescript: ${detected.typescript}`,
117
+ ),
118
+ )
119
+
120
+ if (detected.aliasedInstall) {
121
+ fail(
122
+ `This project installs v5 under the alias "${detected.aliasedInstall}". init is for new apps only.`,
123
+ )
124
+ info(` ${color.dim('fix:')} ${LEGACY_COEXISTENCE_HINT}`)
125
+ return 1
126
+ }
127
+
128
+ if (detected.legacyInstall) {
129
+ fail(
130
+ `This project already has ${PACKAGE_NAME}@${detected.legacyInstall} (pre-v5). init is for new apps only.`,
131
+ )
132
+ info(` ${color.dim('fix:')} ${LEGACY_COEXISTENCE_HINT}`)
133
+ return 1
134
+ }
135
+
136
+ heading('Requirements')
137
+ const {blocking} = reportPrereqs(detected)
138
+
139
+ const plan = buildPlan(cwd, detected)
140
+ printPlan(cwd, detected, plan)
141
+
142
+ if (dry) {
143
+ info('')
144
+ info(color.dim('Dry run: no changes made.'))
145
+ return 0
146
+ }
147
+
148
+ if (blocking) {
149
+ info('')
150
+ warn('Some requirements are unmet (see above). Continuing may not work.')
151
+ }
152
+
153
+ // Without a TTY there's no one to answer the prompt, so confirm() falls back to
154
+ // the default (apply). Say so explicitly rather than appearing to act silently;
155
+ // --dry is the preview-only path for anyone who doesn't want writes.
156
+ if (!yes && !process.stdin.isTTY) {
157
+ info('')
158
+ info(color.dim('Non-interactive (no TTY): applying automatically. Use --dry to preview.'))
159
+ }
160
+
161
+ const proceed = await confirm('\nApply these changes?', {defaultValue: true, assumeYes: yes})
162
+ if (!proceed) {
163
+ info('Aborted. No changes made.')
164
+ return 0
165
+ }
166
+
167
+ heading('Applying')
168
+
169
+ step(`Installing with ${detected.packageManager}…`)
170
+ const install = runInstall(cwd, detected.packageManager, plan.specs)
171
+ if (install.ok) {
172
+ success('Dependencies installed.')
173
+ } else if (install.reason === 'spawn') {
174
+ warn(`Could not run ${detected.packageManager}: ${install.message ?? 'unknown error'}`)
175
+ info(` ${color.dim('fix:')} Install manually: ${install.command} ${install.args.join(' ')}`)
176
+ } else {
177
+ warn(`Install exited with code ${install.status ?? 'unknown'}.`)
178
+ info(
179
+ ` ${color.dim('hint:')} This is usually a peer-dependency conflict. Re-run the install and read the conflict, or retry with your package manager's legacy peer-deps flag.`,
180
+ )
181
+ }
182
+
183
+ if (plan.typeSpecs.length > 0) {
184
+ step('Installing React type definitions…')
185
+ const typeInstall = runInstall(cwd, detected.packageManager, plan.typeSpecs, {dev: true})
186
+ if (typeInstall.ok) success('Type definitions installed.')
187
+ else warn(`Could not install ${plan.typeSpecs.join(' ')}. Add them manually.`)
188
+ }
189
+
190
+ if (plan.stylesheet.exists && !plan.stylesheet.imported) {
191
+ const wired = wireStylesheet(cwd, detected.entry, {dryRun: false})
192
+ if (wired.wrote) success(`Imported the stylesheet in ${detected.entry}.`)
193
+ } else if (plan.stylesheet.imported) {
194
+ success('Stylesheet already imported.')
195
+ } else {
196
+ warn(`Could not find ${detected.entry}. Add \`import '${STYLES_SPECIFIER}'\` manually.`)
197
+ }
198
+
199
+ if (detected.typescript) {
200
+ const result = reconcileTsconfig(cwd, {dryRun: false})
201
+ if (result.created) success('Created tsconfig.json with required options.')
202
+ else if (result.unreadable) warn('tsconfig.json could not be parsed; left untouched.')
203
+ else if (result.changes.length === 0) success('tsconfig already has the required options.')
204
+ else success(`Updated tsconfig: ${result.changes.map((c) => c.key).join(', ')}.`)
205
+ }
206
+
207
+ const config = buildConfig({framework: detected.framework, entry: detected.entry})
208
+ writeConfig(cwd, config)
209
+ success(`Wrote ${CONFIG_FILE}.`)
210
+
211
+ heading('Verifying')
212
+ const results = verifyAfterInit(cwd)
213
+ printChecks(results)
214
+
215
+ const failed = results.filter((c) => c.status === 'fail')
216
+ info('')
217
+ if (failed.length === 0) {
218
+ success('Setup verified. Import components from @sanity/ui and start building.')
219
+ return 0
220
+ }
221
+ fail(
222
+ `${failed.length} check${failed.length === 1 ? '' : 's'} failed. Run \`npx ${PACKAGE_NAME}@alpha doctor\` for details.`,
223
+ )
224
+ return 1
225
+ }
@@ -0,0 +1,52 @@
1
+ import {mkdtempSync, rmSync} from 'node:fs'
2
+ import {tmpdir} from 'node:os'
3
+ import {join} from 'node:path'
4
+
5
+ import {afterEach, beforeEach, describe, expect, it} from 'vitest'
6
+
7
+ import {installCommand, resolveInstallSpecs, resolveTypeSpecs} from './install.js'
8
+
9
+ describe('resolveInstallSpecs', () => {
10
+ it('pins the package to its shipped version and icons to the declared version', () => {
11
+ const specs = resolveInstallSpecs()
12
+ expect(specs[0]).toMatch(/^@sanity\/ui@\d+\.\d+\.\d+/)
13
+ expect(specs.some((s) => s.startsWith('@sanity/icons@'))).toBe(true)
14
+ })
15
+
16
+ it('leaves dependencies the app never imports itself to the package manager', () => {
17
+ const specs = resolveInstallSpecs()
18
+ expect(specs.some((s) => s.startsWith('react-refractor'))).toBe(false)
19
+ expect(specs.some((s) => s.startsWith('clsx'))).toBe(false)
20
+ })
21
+ })
22
+
23
+ describe('installCommand', () => {
24
+ it('uses install for npm and add for others', () => {
25
+ expect(installCommand('npm', ['x']).args).toEqual(['install', 'x'])
26
+ expect(installCommand('pnpm', ['x']).args).toEqual(['add', 'x'])
27
+ })
28
+
29
+ it('applies the right dev flag per package manager', () => {
30
+ expect(installCommand('npm', ['x'], {dev: true}).args).toEqual(['install', '--save-dev', 'x'])
31
+ expect(installCommand('pnpm', ['x'], {dev: true}).args).toEqual(['add', '-D', 'x'])
32
+ expect(installCommand('yarn', ['x'], {dev: true}).args).toEqual(['add', '-D', 'x'])
33
+ expect(installCommand('bun', ['x'], {dev: true}).args).toEqual(['add', '-d', 'x'])
34
+ })
35
+ })
36
+
37
+ describe('resolveTypeSpecs', () => {
38
+ let dir: string
39
+ beforeEach(() => {
40
+ dir = mkdtempSync(join(tmpdir(), 'sui-types-'))
41
+ })
42
+ afterEach(() => {
43
+ rmSync(dir, {recursive: true, force: true})
44
+ })
45
+
46
+ it('returns React type packages pinned to the major when none are installed', () => {
47
+ expect(resolveTypeSpecs(dir, {reactMajor: 19})).toEqual([
48
+ '@types/react@^19',
49
+ '@types/react-dom@^19',
50
+ ])
51
+ })
52
+ })