@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,108 @@
1
+ import {spawnSync} from 'node:child_process'
2
+ import process from 'node:process'
3
+
4
+ import {isPackageInstalled, PACKAGE_NAME} from './detect.js'
5
+ import {readSelfPackage} from './selfPackage.js'
6
+ import type {PackageManager} from './types.js'
7
+
8
+ const ICONS = '@sanity/icons'
9
+ const DEV_FLAG: Record<PackageManager, string> = {
10
+ npm: '--save-dev',
11
+ pnpm: '-D',
12
+ yarn: '-D',
13
+ bun: '-d',
14
+ }
15
+
16
+ export interface InstallResult {
17
+ ok: boolean
18
+ command: string
19
+ args: string[]
20
+ reason?: 'spawn' | 'exit'
21
+ message?: string
22
+ status?: number | null
23
+ }
24
+
25
+ /** Strips range operators to the exact floor version, e.g. "^3.7.4" -> "3.7.4". */
26
+ function exactFromRange(range: string | undefined): string | null {
27
+ if (typeof range !== 'string') return null
28
+ const match = range.match(/\d+\.\d+\.\d+(?:-[\w.]+)?/)
29
+ return match ? match[0] : null
30
+ }
31
+
32
+ /**
33
+ * Builds the exact specs to install from this package's own manifest: the
34
+ * package at its shipped version, and icons at the version it declares in
35
+ * dependencies. Nothing here is invented.
36
+ *
37
+ * Icons is already a dependency, so the package manager installs it either way.
38
+ * It is listed explicitly because apps import icons directly to pass to
39
+ * components (`import {AddIcon} from '@sanity/icons/Add'`, then
40
+ * `<Button iconStart={AddIcon} />`), and under pnpm an import that
41
+ * isn't in the app's own package.json doesn't resolve. Dependencies the app
42
+ * never imports itself (react-refractor, clsx) are left to the package manager.
43
+ * React/react-dom are peers that belong to the host app, so they are left out
44
+ * too (the prereq check verifies them).
45
+ */
46
+ export function resolveInstallSpecs(): string[] {
47
+ const self = readSelfPackage()
48
+ const deps = self.dependencies ?? {}
49
+
50
+ const specs = [`${PACKAGE_NAME}@${self.version ?? 'latest'}`]
51
+
52
+ const iconsVersion = exactFromRange(deps[ICONS])
53
+ specs.push(iconsVersion ? `${ICONS}@${iconsVersion}` : ICONS)
54
+
55
+ return specs
56
+ }
57
+
58
+ /**
59
+ * The React type packages a TypeScript project needs to type-check JSX and the
60
+ * component props. Only the ones not already installed are returned, pinned to
61
+ * the project's React major (defaulting to the supported React 19).
62
+ */
63
+ export function resolveTypeSpecs(cwd: string, {reactMajor = 19} = {}): string[] {
64
+ const major = Number.isInteger(reactMajor) ? reactMajor : 19
65
+ return ['@types/react', '@types/react-dom']
66
+ .filter((name) => !isPackageInstalled(cwd, name))
67
+ .map((name) => `${name}@^${major}`)
68
+ }
69
+
70
+ export function installCommand(
71
+ packageManager: PackageManager,
72
+ specs: string[],
73
+ {dev = false} = {},
74
+ ): {command: string; args: string[]} {
75
+ const verb = packageManager === 'npm' ? 'install' : 'add'
76
+ const flags = dev ? [DEV_FLAG[packageManager]] : []
77
+ return {command: packageManager, args: [verb, ...flags, ...specs]}
78
+ }
79
+
80
+ /**
81
+ * Runs the install with inherited stdio. Returns a structured result so the
82
+ * caller can explain a peer-dependency conflict in plain language instead of
83
+ * failing silently.
84
+ */
85
+ export function runInstall(
86
+ cwd: string,
87
+ packageManager: PackageManager,
88
+ specs: string[],
89
+ {dev = false} = {},
90
+ ): InstallResult {
91
+ const {command, args} = installCommand(packageManager, specs, {dev})
92
+ // On Windows the package managers are .cmd shims, and spawning them without a
93
+ // shell throws (ENOENT, or EINVAL since the Node .cmd security fix). The args
94
+ // are package specs we build ourselves, so there is nothing to escape.
95
+ const result = spawnSync(command, args, {
96
+ cwd,
97
+ stdio: 'inherit',
98
+ shell: process.platform === 'win32',
99
+ })
100
+
101
+ if (result.error) {
102
+ return {ok: false, reason: 'spawn', message: result.error.message, command, args}
103
+ }
104
+ if (result.status !== 0) {
105
+ return {ok: false, reason: 'exit', status: result.status, command, args}
106
+ }
107
+ return {ok: true, command, args}
108
+ }
package/src/cli/log.ts ADDED
@@ -0,0 +1,79 @@
1
+ import process from 'node:process'
2
+ import readline from 'node:readline/promises'
3
+
4
+ // Colorize only a real terminal, and honor the NO_COLOR convention (https://no-color.org).
5
+ const noColor = process.env['NO_COLOR'] !== undefined
6
+ const supportsColor = process.stdout.isTTY && !noColor
7
+
8
+ function paint(code: string, text: string): string {
9
+ if (!supportsColor) return text
10
+ return `\u001b[${code}m${text}\u001b[0m`
11
+ }
12
+
13
+ export const color = {
14
+ bold: (t: string) => paint('1', t),
15
+ dim: (t: string) => paint('2', t),
16
+ red: (t: string) => paint('31', t),
17
+ green: (t: string) => paint('32', t),
18
+ yellow: (t: string) => paint('33', t),
19
+ blue: (t: string) => paint('34', t),
20
+ cyan: (t: string) => paint('36', t),
21
+ }
22
+
23
+ export function info(message: string): void {
24
+ process.stdout.write(`${message}\n`)
25
+ }
26
+
27
+ export function step(message: string): void {
28
+ process.stdout.write(`${color.cyan('›')} ${message}\n`)
29
+ }
30
+
31
+ export function success(message: string): void {
32
+ process.stdout.write(`${color.green('✓')} ${message}\n`)
33
+ }
34
+
35
+ export function warn(message: string): void {
36
+ process.stdout.write(`${color.yellow('!')} ${message}\n`)
37
+ }
38
+
39
+ export function fail(message: string): void {
40
+ process.stdout.write(`${color.red('✗')} ${message}\n`)
41
+ }
42
+
43
+ /** Real errors (unexpected exceptions) go to stderr; report output uses fail(). */
44
+ export function errorLine(message: string): void {
45
+ // Gate color on stderr (where this writes), not stdout, so a redirected stderr
46
+ // doesn't receive ANSI codes while stdout is still a TTY.
47
+ const mark = process.stderr.isTTY && !noColor ? '\u001b[31m✗\u001b[0m' : '✗'
48
+ process.stderr.write(`${mark} ${message}\n`)
49
+ }
50
+
51
+ export function heading(message: string): void {
52
+ process.stdout.write(`\n${color.bold(message)}\n`)
53
+ }
54
+
55
+ interface ConfirmOptions {
56
+ defaultValue?: boolean
57
+ assumeYes?: boolean
58
+ }
59
+
60
+ /**
61
+ * Ask a yes/no question. In non-interactive contexts (no TTY) it resolves to
62
+ * the provided default so CI and agents never block.
63
+ */
64
+ export async function confirm(question: string, options: ConfirmOptions = {}): Promise<boolean> {
65
+ const {defaultValue = true, assumeYes = false} = options
66
+ if (assumeYes || !process.stdin.isTTY) return defaultValue
67
+
68
+ const rl = readline.createInterface({input: process.stdin, output: process.stdout})
69
+ try {
70
+ const hint = defaultValue ? 'Y/n' : 'y/N'
71
+ const answer = (await rl.question(`${question} ${color.dim(`(${hint})`)} `))
72
+ .trim()
73
+ .toLowerCase()
74
+ if (answer === '') return defaultValue
75
+ return answer === 'y' || answer === 'yes'
76
+ } finally {
77
+ rl.close()
78
+ }
79
+ }
@@ -0,0 +1,14 @@
1
+ import {readFileSync} from 'node:fs'
2
+
3
+ import type {PackageJson} from './types.js'
4
+
5
+ /**
6
+ * Reads this package's own package.json so the CLI installs the exact version
7
+ * it ships with and the real declared ranges, never invented ones. The path is
8
+ * two levels up from both src/cli (dev) and dist/cli (published), which both
9
+ * resolve to the package root.
10
+ */
11
+ export function readSelfPackage(): PackageJson {
12
+ const url = new URL('../../package.json', import.meta.url)
13
+ return JSON.parse(readFileSync(url, 'utf8')) as PackageJson
14
+ }
@@ -0,0 +1,130 @@
1
+ import {mkdirSync, mkdtempSync, readFileSync, 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} from 'vitest'
6
+
7
+ import {hasStylesImport, inspectStylesheet, wireStylesheet} from './styles.js'
8
+
9
+ describe('hasStylesImport', () => {
10
+ it('matches single and double quoted imports', () => {
11
+ expect(hasStylesImport(`import '@sanity/ui/styles.css'`)).toBe(true)
12
+ expect(hasStylesImport(`import "@sanity/ui/styles.css"`)).toBe(true)
13
+ expect(hasStylesImport(`import './app.css'`)).toBe(false)
14
+ })
15
+
16
+ it('matches a require of the stylesheet', () => {
17
+ expect(hasStylesImport(`require('@sanity/ui/styles.css')`)).toBe(true)
18
+ })
19
+
20
+ it('ignores a commented-out or string mention of the path', () => {
21
+ expect(hasStylesImport(`// import '@sanity/ui/styles.css'`)).toBe(false)
22
+ expect(hasStylesImport(`const note = 'see @sanity/ui/styles.css'`)).toBe(false)
23
+ })
24
+ })
25
+
26
+ describe('wireStylesheet', () => {
27
+ let dir: string
28
+ beforeEach(() => {
29
+ dir = mkdtempSync(join(tmpdir(), 'sui-styles-'))
30
+ })
31
+ afterEach(() => {
32
+ rmSync(dir, {recursive: true, force: true})
33
+ })
34
+
35
+ function write(rel: string, content: string): string {
36
+ const full = join(dir, rel)
37
+ mkdirSync(join(full, '..'), {recursive: true})
38
+ writeFileSync(full, content)
39
+ return full
40
+ }
41
+
42
+ it('inserts after the leading import block', () => {
43
+ write(
44
+ 'src/main.tsx',
45
+ `import {createRoot} from 'react-dom/client'\nimport App from './App'\n\ncreateRoot()\n`,
46
+ )
47
+ const result = wireStylesheet(dir, 'src/main.tsx')
48
+ expect(result.wrote).toBe(true)
49
+ const lines = readFileSync(join(dir, 'src/main.tsx'), 'utf8').split('\n')
50
+ expect(lines[2]).toBe(`import '@sanity/ui/styles.css'`)
51
+ })
52
+
53
+ it('inserts after a multiline import without splitting it', () => {
54
+ write(
55
+ 'src/main.tsx',
56
+ `import {\n StrictMode,\n useState,\n} from 'react'\nimport App from './App'\n\ncreateRoot()\n`,
57
+ )
58
+ wireStylesheet(dir, 'src/main.tsx')
59
+ const content = readFileSync(join(dir, 'src/main.tsx'), 'utf8')
60
+ // The import must land after the whole react import, not between its lines.
61
+ expect(content).toContain(
62
+ `} from 'react'\nimport App from './App'\nimport '@sanity/ui/styles.css'`,
63
+ )
64
+ expect(content).not.toMatch(/import \{\nimport '@sanity-labs/)
65
+ })
66
+
67
+ it('keeps a leading use-client directive first', () => {
68
+ write('app/layout.tsx', `'use client'\nimport {x} from 'y'\n`)
69
+ wireStylesheet(dir, 'app/layout.tsx')
70
+ const lines = readFileSync(join(dir, 'app/layout.tsx'), 'utf8').split('\n')
71
+ expect(lines[0]).toBe(`'use client'`)
72
+ expect(lines.includes(`import '@sanity/ui/styles.css'`)).toBe(true)
73
+ })
74
+
75
+ it('injects into astro frontmatter', () => {
76
+ write('src/layouts/Layout.astro', `---\nconst {title} = Astro.props\n---\n<html></html>\n`)
77
+ wireStylesheet(dir, 'src/layouts/Layout.astro')
78
+ const content = readFileSync(join(dir, 'src/layouts/Layout.astro'), 'utf8')
79
+ expect(content).toContain(`import '@sanity/ui/styles.css'`)
80
+ expect(content.indexOf(`import '@sanity/ui/styles.css'`)).toBeLessThan(
81
+ content.indexOf('<html>'),
82
+ )
83
+ })
84
+
85
+ it('is idempotent', () => {
86
+ write('src/main.tsx', `import App from './App'\n`)
87
+ wireStylesheet(dir, 'src/main.tsx')
88
+ const second = wireStylesheet(dir, 'src/main.tsx')
89
+ expect(second.already).toBe(true)
90
+ expect(second.wrote).toBe(false)
91
+ })
92
+
93
+ it('reports a missing entry instead of throwing', () => {
94
+ const result = wireStylesheet(dir, 'src/main.tsx')
95
+ expect(result.missing).toBe(true)
96
+ })
97
+ })
98
+
99
+ describe('inspectStylesheet', () => {
100
+ let dir: string
101
+ beforeEach(() => {
102
+ dir = mkdtempSync(join(tmpdir(), 'sui-inspect-'))
103
+ })
104
+ afterEach(() => {
105
+ rmSync(dir, {recursive: true, force: true})
106
+ })
107
+
108
+ it('finds the import in a sibling file when not in the entry', () => {
109
+ mkdirSync(join(dir, 'src'), {recursive: true})
110
+ writeFileSync(join(dir, 'src/main.tsx'), `import App from './App'\n`)
111
+ writeFileSync(join(dir, 'src/App.tsx'), `import '@sanity/ui/styles.css'\n`)
112
+ const result = inspectStylesheet(dir, 'src/main.tsx')
113
+ expect(result.imported).toBe(true)
114
+ expect(result.importedIn).toBe(join('src', 'App.tsx'))
115
+ })
116
+
117
+ it('finds the import in a nested route file', () => {
118
+ mkdirSync(join(dir, 'app/routes'), {recursive: true})
119
+ writeFileSync(join(dir, 'app/root.tsx'), `export default function Root() {}\n`)
120
+ writeFileSync(join(dir, 'app/routes/home.tsx'), `import '@sanity/ui/styles.css'\n`)
121
+ const result = inspectStylesheet(dir, 'app/root.tsx')
122
+ expect(result.imported).toBe(true)
123
+ })
124
+
125
+ it('reports missing when absent everywhere', () => {
126
+ mkdirSync(join(dir, 'src'), {recursive: true})
127
+ writeFileSync(join(dir, 'src/main.tsx'), `import App from './App'\n`)
128
+ expect(inspectStylesheet(dir, 'src/main.tsx').imported).toBe(false)
129
+ })
130
+ })
@@ -0,0 +1,205 @@
1
+ import {existsSync, readdirSync, readFileSync, statSync, writeFileSync} from 'node:fs'
2
+ import {dirname, join, relative} from 'node:path'
3
+
4
+ export const STYLES_SPECIFIER = '@sanity/ui/styles.css'
5
+ const IMPORT_LINE = `import '${STYLES_SPECIFIER}'`
6
+ // Match a real import/require of the stylesheet, not a bare mention. A
7
+ // commented-out or string occurrence of the path must not count as "wired",
8
+ // or doctor/init would report success when the import isn't active.
9
+ const SPEC = String.raw`@sanity/ui/styles\.css`
10
+ const PRESENT = new RegExp(
11
+ String.raw`(?:^|\n)\s*import\s+(?:[^\n'"]*\bfrom\s+)?['"]${SPEC}['"]` +
12
+ String.raw`|\brequire\(\s*['"]${SPEC}['"]\s*\)`,
13
+ )
14
+ const SOURCE_EXT = /\.[jt]sx?$|\.astro$/
15
+ const SKIP_DIRS = new Set([
16
+ 'node_modules',
17
+ 'dist',
18
+ 'build',
19
+ '.next',
20
+ '.react-router',
21
+ '.git',
22
+ 'out',
23
+ ])
24
+ const SOURCE_ROOTS = ['src', 'app', 'pages', 'components']
25
+ const MAX_FILES = 800
26
+
27
+ export interface StylesheetInfo {
28
+ entry: string
29
+ file: string
30
+ exists: boolean
31
+ imported: boolean
32
+ importedIn?: string
33
+ }
34
+
35
+ export interface WireResult {
36
+ entry: string
37
+ file: string
38
+ imported: boolean
39
+ wrote: boolean
40
+ missing?: boolean
41
+ already?: boolean
42
+ }
43
+
44
+ export function hasStylesImport(content: string): boolean {
45
+ return PRESENT.test(content)
46
+ }
47
+
48
+ function fileImports(file: string): boolean {
49
+ try {
50
+ return hasStylesImport(readFileSync(file, 'utf8'))
51
+ } catch {
52
+ return false
53
+ }
54
+ }
55
+
56
+ /** Bounded recursive walk that returns the first source file importing the stylesheet. */
57
+ function findImportInDir(root: string, budget: {count: number}): string | null {
58
+ const stack = [root]
59
+ while (stack.length > 0 && budget.count < MAX_FILES) {
60
+ const dir = stack.pop()
61
+ if (dir === undefined) break
62
+ let entries
63
+ try {
64
+ entries = readdirSync(dir, {withFileTypes: true})
65
+ } catch {
66
+ continue
67
+ }
68
+ for (const entry of entries) {
69
+ const full = join(dir, entry.name)
70
+ if (entry.isDirectory()) {
71
+ if (!SKIP_DIRS.has(entry.name)) stack.push(full)
72
+ } else if (SOURCE_EXT.test(entry.name)) {
73
+ budget.count++
74
+ if (budget.count > MAX_FILES) break
75
+ if (fileImports(full)) return full
76
+ }
77
+ }
78
+ }
79
+ return null
80
+ }
81
+
82
+ /**
83
+ * Reports whether the stylesheet is imported. The import only needs to exist
84
+ * once in the module graph, so beyond the prescribed entry we scan the project
85
+ * source roots (bounded) before reporting it missing. That catches imports
86
+ * placed in App.tsx, a route file, or a layout component.
87
+ */
88
+ export function inspectStylesheet(cwd: string, entry: string): StylesheetInfo {
89
+ const file = join(cwd, entry)
90
+ const exists = existsSync(file)
91
+ if (exists && fileImports(file)) return {entry, file, exists, imported: true}
92
+
93
+ const budget = {count: 0}
94
+ const roots = new Set([dirname(entry), ...SOURCE_ROOTS].map((r) => join(cwd, r)))
95
+ for (const root of Array.from(roots)) {
96
+ if (!existsSync(root)) continue
97
+ try {
98
+ if (!statSync(root).isDirectory()) continue
99
+ } catch {
100
+ continue
101
+ }
102
+ const found = findImportInDir(root, budget)
103
+ if (found)
104
+ return {
105
+ entry,
106
+ file,
107
+ exists,
108
+ imported: true,
109
+ importedIn: relative(cwd, found),
110
+ }
111
+ }
112
+
113
+ return {entry, file, exists, imported: false}
114
+ }
115
+
116
+ function isDirective(line: string): boolean {
117
+ const trimmed = line.trim()
118
+ return /^['"]use (client|server|strict)['"];?$/.test(trimmed)
119
+ }
120
+
121
+ function isImportLine(line: string): boolean {
122
+ return /^\s*import\b/.test(line)
123
+ }
124
+
125
+ /**
126
+ * True when an import statement finishes on this line: either it ends with a
127
+ * `from '...'` clause or it is a bare side-effect `import '...'`. Used to find
128
+ * the real end of an import block so we never split a multiline import.
129
+ */
130
+ function importStatementEnds(line: string): boolean {
131
+ const t = line.trim()
132
+ return /\bfrom\s*['"][^'"]+['"];?\s*$/.test(t) || /^import\s+['"][^'"]+['"];?\s*$/.test(t)
133
+ }
134
+
135
+ /** Inserts the import into a JS/TS module after any leading directive + import block. */
136
+ function injectIntoModule(content: string): string {
137
+ const lines = content.split('\n')
138
+ let insertAt = 0
139
+
140
+ while (insertAt < lines.length) {
141
+ const line = lines[insertAt] ?? ''
142
+ if (isDirective(line) || line.trim() === '') insertAt++
143
+ else break
144
+ }
145
+
146
+ // Track the last line that closes an import statement so a multiline import
147
+ // (`import {\n a,\n} from 'x'`) is treated as a single unit and we insert
148
+ // after its closing line, never between its lines.
149
+ let lastImport = -1
150
+ let inMultilineImport = false
151
+ for (let i = insertAt; i < lines.length; i++) {
152
+ const line = lines[i] ?? ''
153
+ if (inMultilineImport) {
154
+ if (importStatementEnds(line)) {
155
+ inMultilineImport = false
156
+ lastImport = i
157
+ }
158
+ continue
159
+ }
160
+ if (isImportLine(line)) {
161
+ lastImport = i
162
+ if (!importStatementEnds(line)) inMultilineImport = true
163
+ } else if (line.trim() !== '' && lastImport !== -1) {
164
+ break
165
+ }
166
+ }
167
+
168
+ const at = lastImport === -1 ? insertAt : lastImport + 1
169
+ lines.splice(at, 0, IMPORT_LINE)
170
+ return lines.join('\n')
171
+ }
172
+
173
+ /** Inserts the import inside an Astro frontmatter fence, creating one if absent. */
174
+ function injectIntoAstro(content: string): string {
175
+ if (content.startsWith('---')) {
176
+ const end = content.indexOf('\n---', 3)
177
+ if (end !== -1) {
178
+ const head = content.slice(0, end)
179
+ const tail = content.slice(end)
180
+ return `${head}\n${IMPORT_LINE}${tail}`
181
+ }
182
+ }
183
+ return `---\n${IMPORT_LINE}\n---\n${content}`
184
+ }
185
+
186
+ export function wireStylesheet(cwd: string, entry: string, {dryRun = false} = {}): WireResult {
187
+ const file = join(cwd, entry)
188
+ if (!existsSync(file)) return {entry, file, missing: true, imported: false, wrote: false}
189
+
190
+ const content = readFileSync(file, 'utf8')
191
+ if (hasStylesImport(content)) {
192
+ return {
193
+ entry,
194
+ file,
195
+ missing: false,
196
+ imported: true,
197
+ wrote: false,
198
+ already: true,
199
+ }
200
+ }
201
+
202
+ const next = entry.endsWith('.astro') ? injectIntoAstro(content) : injectIntoModule(content)
203
+ if (!dryRun) writeFileSync(file, next)
204
+ return {entry, file, missing: false, imported: true, wrote: !dryRun}
205
+ }
@@ -0,0 +1,10 @@
1
+ import {mkdirSync, writeFileSync} from 'node:fs'
2
+ import {join} from 'node:path'
3
+
4
+ import {PACKAGE_NAME} from './detect.js'
5
+
6
+ export function writeInstalledUiVersion(dir: string, version: string): void {
7
+ const root = join(dir, 'node_modules', PACKAGE_NAME)
8
+ mkdirSync(root, {recursive: true})
9
+ writeFileSync(join(root, 'package.json'), JSON.stringify({name: PACKAGE_NAME, version}))
10
+ }