@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,174 @@
1
+ import {mkdtempSync, readFileSync, rmSync, writeFileSync} from 'node:fs'
2
+ import {tmpdir} from 'node:os'
3
+ import {join} from 'node:path'
4
+
5
+ import {parse} from 'comment-json'
6
+ import {afterEach, beforeEach, describe, expect, it} from 'vitest'
7
+
8
+ import {planTsconfigChanges, reconcileTsconfig, resolveTargetTsconfig} from './tsconfig.js'
9
+ import type {TsconfigChange} from './types.js'
10
+
11
+ function keysChanged(changes: TsconfigChange[]): string[] {
12
+ return changes.map((c) => c.key).sort()
13
+ }
14
+
15
+ interface ParsedTsconfig {
16
+ compilerOptions?: Record<string, unknown>
17
+ }
18
+
19
+ function parseConfig(raw: string): ParsedTsconfig {
20
+ return parse(raw) as unknown as ParsedTsconfig
21
+ }
22
+
23
+ describe('planTsconfigChanges', () => {
24
+ it('reports nothing for an already-good config', () => {
25
+ const changes = planTsconfigChanges({
26
+ jsx: 'react-jsx',
27
+ module: 'esnext',
28
+ moduleResolution: 'bundler',
29
+ lib: ['DOM', 'DOM.Iterable', 'ES2022'],
30
+ skipLibCheck: true,
31
+ })
32
+ expect(changes).toEqual([])
33
+ })
34
+
35
+ it('fixes a classic moduleResolution and incompatible module together', () => {
36
+ const changes = planTsconfigChanges({
37
+ jsx: 'react-jsx',
38
+ module: 'commonjs',
39
+ moduleResolution: 'node',
40
+ lib: ['DOM', 'DOM.Iterable', 'ES2022'],
41
+ skipLibCheck: true,
42
+ })
43
+ expect(keysChanged(changes)).toEqual(['module', 'moduleResolution'])
44
+ expect(changes.find((c) => c.key === 'moduleResolution')?.to).toBe('bundler')
45
+ expect(changes.find((c) => c.key === 'module')?.to).toBe('esnext')
46
+ })
47
+
48
+ it('leaves a compatible module alone when resolution is already modern', () => {
49
+ const changes = planTsconfigChanges({
50
+ jsx: 'react-jsx',
51
+ module: 'commonjs',
52
+ moduleResolution: 'nodenext',
53
+ lib: ['DOM', 'DOM.Iterable', 'ES2022'],
54
+ skipLibCheck: true,
55
+ })
56
+ // nodenext is modern, but commonjs is not bundler-compatible; nodenext does
57
+ // not force the change, so module stays untouched.
58
+ expect(keysChanged(changes)).toEqual([])
59
+ })
60
+
61
+ it('does not force module when resolution is nodenext and module is missing', () => {
62
+ const changes = planTsconfigChanges({
63
+ jsx: 'react-jsx',
64
+ moduleResolution: 'nodenext',
65
+ lib: ['DOM', 'DOM.Iterable', 'ES2022'],
66
+ skipLibCheck: true,
67
+ })
68
+ expect(changes.find((c) => c.key === 'module')).toBeUndefined()
69
+ })
70
+
71
+ it('adds only missing DOM libs and keeps the existing ES baseline', () => {
72
+ const changes = planTsconfigChanges({
73
+ jsx: 'react-jsx',
74
+ module: 'esnext',
75
+ moduleResolution: 'bundler',
76
+ lib: ['ES2023', 'DOM'],
77
+ skipLibCheck: true,
78
+ })
79
+ expect(keysChanged(changes)).toEqual(['lib'])
80
+ const lib = changes.find((c) => c.key === 'lib')
81
+ expect(lib?.merge).toBe(true)
82
+ expect(lib?.to).toEqual(['DOM.Iterable'])
83
+ })
84
+
85
+ it('treats jsx and moduleResolution as load-bearing', () => {
86
+ const changes = planTsconfigChanges({})
87
+ const load = changes.filter((c) => c.load).map((c) => c.key)
88
+ expect(load).toContain('jsx')
89
+ expect(load).toContain('moduleResolution')
90
+ })
91
+ })
92
+
93
+ describe('reconcileTsconfig', () => {
94
+ let dir: string
95
+ beforeEach(() => {
96
+ dir = mkdtempSync(join(tmpdir(), 'sui-tsconfig-'))
97
+ })
98
+ afterEach(() => {
99
+ rmSync(dir, {recursive: true, force: true})
100
+ })
101
+
102
+ it('creates a tsconfig when none exists', () => {
103
+ const result = reconcileTsconfig(dir)
104
+ expect(result.created).toBe(true)
105
+ const written = parseConfig(readFileSync(join(dir, 'tsconfig.json'), 'utf8'))
106
+ expect(written.compilerOptions?.['jsx']).toBe('react-jsx')
107
+ expect(written.compilerOptions?.['moduleResolution']).toBe('bundler')
108
+ })
109
+
110
+ it('merges required options while preserving comments and unrelated settings', () => {
111
+ writeFileSync(
112
+ join(dir, 'tsconfig.json'),
113
+ `{
114
+ // keep me
115
+ "compilerOptions": {
116
+ "moduleResolution": "node",
117
+ "module": "commonjs",
118
+ "baseUrl": "."
119
+ }
120
+ }
121
+ `,
122
+ )
123
+ const result = reconcileTsconfig(dir)
124
+ expect(result.created).toBe(false)
125
+ const raw = readFileSync(join(dir, 'tsconfig.json'), 'utf8')
126
+ expect(raw).toContain('// keep me')
127
+ const written = parseConfig(raw)
128
+ expect(written.compilerOptions?.['baseUrl']).toBe('.')
129
+ expect(written.compilerOptions?.['moduleResolution']).toBe('bundler')
130
+ expect(written.compilerOptions?.['module']).toBe('esnext')
131
+ expect(written.compilerOptions?.['jsx']).toBe('react-jsx')
132
+ })
133
+
134
+ it('is idempotent: a second run makes no changes', () => {
135
+ reconcileTsconfig(dir)
136
+ const second = reconcileTsconfig(dir)
137
+ expect(second.changes).toEqual([])
138
+ })
139
+
140
+ it('targets the referenced config in a solution-style setup', () => {
141
+ writeFileSync(
142
+ join(dir, 'tsconfig.json'),
143
+ `{"files": [], "references": [{"path": "./tsconfig.app.json"}]}\n`,
144
+ )
145
+ writeFileSync(
146
+ join(dir, 'tsconfig.app.json'),
147
+ `{"compilerOptions": {"moduleResolution": "bundler", "module": "esnext", "jsx": "react-jsx", "lib": ["ES2023", "DOM"], "skipLibCheck": true}}\n`,
148
+ )
149
+ const result = reconcileTsconfig(dir)
150
+ expect(result.file.endsWith('tsconfig.app.json')).toBe(true)
151
+ expect(keysChanged(result.changes)).toEqual(['lib'])
152
+ })
153
+
154
+ it('skips a referenced config without compilerOptions and picks one that has them', () => {
155
+ writeFileSync(
156
+ join(dir, 'tsconfig.json'),
157
+ `{"files": [], "references": [{"path": "./tsconfig.app.json"}, {"path": "./tsconfig.node.json"}]}\n`,
158
+ )
159
+ // The "app" config is preferred by name but carries no options, so it must be skipped.
160
+ writeFileSync(join(dir, 'tsconfig.app.json'), `{"extends": "./base.json"}\n`)
161
+ writeFileSync(
162
+ join(dir, 'tsconfig.node.json'),
163
+ `{"compilerOptions": {"moduleResolution": "bundler"}}\n`,
164
+ )
165
+ const target = resolveTargetTsconfig(dir)
166
+ expect(target.file.endsWith('tsconfig.node.json')).toBe(true)
167
+ })
168
+
169
+ it('treats a non-object tsconfig.json as unreadable instead of crashing', () => {
170
+ writeFileSync(join(dir, 'tsconfig.json'), `null\n`)
171
+ expect(resolveTargetTsconfig(dir).unreadable).toBe(true)
172
+ expect(reconcileTsconfig(dir).unreadable).toBe(true)
173
+ })
174
+ })
@@ -0,0 +1,282 @@
1
+ import {existsSync, readFileSync, writeFileSync} from 'node:fs'
2
+ import {dirname, isAbsolute, join, resolve} from 'node:path'
3
+
4
+ import {parse, stringify} from 'comment-json'
5
+
6
+ import type {TsconfigChange} from './types.js'
7
+
8
+ interface TsconfigDoc {
9
+ extends?: string | string[]
10
+ compilerOptions?: Record<string, unknown>
11
+ references?: Array<{path?: string}>
12
+ }
13
+
14
+ /** moduleResolution values that read a package `exports` map and its types. */
15
+ const MODERN_RESOLUTIONS = new Set(['bundler', 'node16', 'nodenext'])
16
+ /** module values compatible with moduleResolution: bundler. */
17
+ const BUNDLER_MODULES = new Set(['esnext', 'preserve', 'es2015', 'es2020', 'es2022'])
18
+ const VALID_JSX = new Set(['react-jsx', 'react-jsxdev', 'preserve', 'react', 'react-native'])
19
+
20
+ /** DOM globals the component types reference, plus a modern JS baseline. */
21
+ const DOM_LIBS = ['DOM', 'DOM.Iterable']
22
+ const ES_BASELINE = 'ES2022'
23
+
24
+ /**
25
+ * The full known-good config written when no tsconfig.json exists. Mirrors the
26
+ * repo's own framework apps so a fresh project type-checks predictably.
27
+ */
28
+ const DEFAULT_TSCONFIG = {
29
+ compilerOptions: {
30
+ target: ES_BASELINE,
31
+ lib: [...DOM_LIBS, ES_BASELINE],
32
+ module: 'esnext',
33
+ moduleResolution: 'bundler',
34
+ jsx: 'react-jsx',
35
+ strict: true,
36
+ esModuleInterop: true,
37
+ skipLibCheck: true,
38
+ resolveJsonModule: true,
39
+ isolatedModules: true,
40
+ moduleDetection: 'force',
41
+ },
42
+ include: ['src', 'app'],
43
+ }
44
+
45
+ export interface TsconfigInspection {
46
+ file: string
47
+ exists: boolean
48
+ unreadable?: boolean
49
+ effective?: Record<string, unknown>
50
+ changes: TsconfigChange[]
51
+ }
52
+
53
+ export interface TsconfigResult {
54
+ file: string
55
+ created: boolean
56
+ changes: TsconfigChange[]
57
+ wrote: boolean
58
+ unreadable?: boolean
59
+ }
60
+
61
+ function readJsonc(file: string): TsconfigDoc {
62
+ const data = parse(readFileSync(file, 'utf8'))
63
+ // comment-json happily parses valid-but-non-object JSON (null, true, []). Treat
64
+ // those as unreadable so callers don't crash on later property reads.
65
+ if (typeof data !== 'object' || data === null || Array.isArray(data)) {
66
+ throw new Error('tsconfig.json is not an object')
67
+ }
68
+ return data as unknown as TsconfigDoc
69
+ }
70
+
71
+ /**
72
+ * Picks the tsconfig file to edit. Solution-style configs (Vite) keep their
73
+ * real options in a referenced file, so we follow the first reference that
74
+ * actually carries compilerOptions.
75
+ */
76
+ export function resolveTargetTsconfig(cwd: string): {
77
+ file: string
78
+ exists: boolean
79
+ unreadable?: boolean
80
+ } {
81
+ const root = join(cwd, 'tsconfig.json')
82
+ if (!existsSync(root)) return {file: root, exists: false}
83
+
84
+ let data: TsconfigDoc
85
+ try {
86
+ data = readJsonc(root)
87
+ } catch {
88
+ return {file: root, exists: true, unreadable: true}
89
+ }
90
+
91
+ const hasOwnOptions = data.compilerOptions && Object.keys(data.compilerOptions).length > 0
92
+ if (hasOwnOptions || !Array.isArray(data.references)) {
93
+ return {file: root, exists: true}
94
+ }
95
+
96
+ const referenced = data.references
97
+ .map((ref) => ref.path)
98
+ .filter((p): p is string => typeof p === 'string')
99
+ .map((p) => join(cwd, p.endsWith('.json') ? p : join(p, 'tsconfig.json')))
100
+ .filter((p) => existsSync(p))
101
+
102
+ // Keep only references we can parse and that actually carry compilerOptions, so
103
+ // we never write into a reference that has none or crash on an unreadable one.
104
+ // Prefer an "app" config, the common Vite solution-style layout.
105
+ const withOptions = referenced.filter((p) => {
106
+ try {
107
+ const doc = readJsonc(p)
108
+ return Boolean(doc.compilerOptions && Object.keys(doc.compilerOptions).length > 0)
109
+ } catch {
110
+ return false
111
+ }
112
+ })
113
+
114
+ const appConfig = withOptions.find((p) => p.includes('app')) ?? withOptions[0]
115
+ return {file: appConfig ?? root, exists: true}
116
+ }
117
+
118
+ /** Merges compilerOptions across the resolvable `extends` chain (relative paths only). */
119
+ function effectiveCompilerOptions(file: string, seen = new Set<string>()): Record<string, unknown> {
120
+ if (!existsSync(file) || seen.has(file)) return {}
121
+ seen.add(file)
122
+
123
+ let data: TsconfigDoc
124
+ try {
125
+ data = readJsonc(file)
126
+ } catch {
127
+ return {}
128
+ }
129
+
130
+ const base: Record<string, unknown> = {}
131
+ const extendsField = data.extends
132
+ const extendsList = Array.isArray(extendsField)
133
+ ? extendsField
134
+ : extendsField
135
+ ? [extendsField]
136
+ : []
137
+ for (const ext of extendsList) {
138
+ if (typeof ext !== 'string') continue
139
+ if (ext.startsWith('.') || isAbsolute(ext)) {
140
+ const resolved = isAbsolute(ext) ? ext : resolve(dirname(file), ext)
141
+ const withExt = resolved.endsWith('.json') ? resolved : `${resolved}.json`
142
+ Object.assign(base, effectiveCompilerOptions(withExt, seen))
143
+ }
144
+ }
145
+
146
+ return {...base, ...data.compilerOptions}
147
+ }
148
+
149
+ function libIncludes(libs: unknown, name: string): boolean {
150
+ if (!Array.isArray(libs)) return false
151
+ return libs.some((entry) => String(entry).toLowerCase() === name.toLowerCase())
152
+ }
153
+
154
+ function hasEsBaseline(libs: unknown): boolean {
155
+ if (!Array.isArray(libs)) return false
156
+ return libs.some((entry) => /^es(\d{4}|next)$/i.test(String(entry)))
157
+ }
158
+
159
+ /**
160
+ * Compares effective options against the minimum a project needs to consume
161
+ * this package and returns the changes to make. Pure: callers decide whether to
162
+ * apply them.
163
+ *
164
+ * Only options that actually break consumption are touched, so merging into an
165
+ * existing project never introduces type errors in the user's own code:
166
+ * - jsx: required to compile .tsx
167
+ * - moduleResolution: required to read the package exports map + its types
168
+ * - module: must stay compatible with a bundler resolution
169
+ * - lib: DOM/DOM.Iterable back the component element types
170
+ * - skipLibCheck: only ever removes errors, safe to add
171
+ *
172
+ * `load: true` marks the load-bearing changes that doctor reports as failures.
173
+ */
174
+ export function planTsconfigChanges(effective: Record<string, unknown>): TsconfigChange[] {
175
+ const changes: TsconfigChange[] = []
176
+ const opts = effective
177
+
178
+ const jsx = opts['jsx']
179
+ if (!jsx) changes.push({key: 'jsx', from: undefined, to: 'react-jsx', load: true})
180
+ else if (typeof jsx !== 'string' || !VALID_JSX.has(jsx)) {
181
+ changes.push({key: 'jsx', from: jsx, to: 'react-jsx', load: true})
182
+ }
183
+
184
+ const resolution = opts['moduleResolution']
185
+ const resolutionLower = typeof resolution === 'string' ? resolution.toLowerCase() : null
186
+ const resolutionIsModern = Boolean(resolutionLower && MODERN_RESOLUTIONS.has(resolutionLower))
187
+ let willSetBundler = false
188
+ if (!resolution) {
189
+ changes.push({key: 'moduleResolution', from: undefined, to: 'bundler', load: true})
190
+ willSetBundler = true
191
+ } else if (!resolutionIsModern) {
192
+ changes.push({key: 'moduleResolution', from: resolution, to: 'bundler', load: true})
193
+ willSetBundler = true
194
+ }
195
+
196
+ // module only matters when resolution is (or becomes) bundler, which requires
197
+ // esnext/preserve/es2015+. node16/nodenext pair with their own module values,
198
+ // so we leave those alone.
199
+ const resolutionWillBeBundler = willSetBundler || resolutionLower === 'bundler'
200
+ const module = opts['module']
201
+ const moduleLower = typeof module === 'string' ? module.toLowerCase() : null
202
+ const moduleCompatible = Boolean(moduleLower && BUNDLER_MODULES.has(moduleLower))
203
+ // Only enforce module when resolution is (or becomes) bundler. node16/nodenext
204
+ // pair with their own module values, so a missing or non-bundler module under
205
+ // those resolutions is left alone rather than forced to esnext (which would be
206
+ // an invalid combination and could change a NodeNext project's semantics).
207
+ if (resolutionWillBeBundler && !moduleCompatible) {
208
+ changes.push({key: 'module', from: module, to: 'esnext', load: true})
209
+ }
210
+
211
+ const lib = opts['lib']
212
+ if (lib === undefined) {
213
+ changes.push({key: 'lib', from: undefined, to: [...DOM_LIBS, ES_BASELINE], load: true})
214
+ } else {
215
+ const missing = DOM_LIBS.filter((entry) => !libIncludes(lib, entry))
216
+ if (!hasEsBaseline(lib)) missing.push(ES_BASELINE)
217
+ if (missing.length > 0) {
218
+ changes.push({key: 'lib', from: lib, to: missing, merge: true, load: true})
219
+ }
220
+ }
221
+
222
+ if (opts['skipLibCheck'] === undefined) {
223
+ changes.push({key: 'skipLibCheck', from: undefined, to: true})
224
+ }
225
+
226
+ return changes
227
+ }
228
+
229
+ /** Read-only: what would `init` change about tsconfig? Used by `doctor` too. */
230
+ export function inspectTsconfig(cwd: string): TsconfigInspection {
231
+ const target = resolveTargetTsconfig(cwd)
232
+ if (!target.exists) {
233
+ return {file: target.file, exists: false, changes: planTsconfigChanges({})}
234
+ }
235
+ if (target.unreadable) {
236
+ return {file: target.file, exists: true, unreadable: true, changes: []}
237
+ }
238
+ const effective = effectiveCompilerOptions(target.file)
239
+ return {file: target.file, exists: true, effective, changes: planTsconfigChanges(effective)}
240
+ }
241
+
242
+ /**
243
+ * Applies the required options. Existing comments and unrelated settings are
244
+ * preserved (comment-json round-trips them). Returns what happened.
245
+ */
246
+ export function reconcileTsconfig(cwd: string, {dryRun = false} = {}): TsconfigResult {
247
+ const target = resolveTargetTsconfig(cwd)
248
+
249
+ if (!target.exists) {
250
+ if (!dryRun) {
251
+ writeFileSync(target.file, `${JSON.stringify(DEFAULT_TSCONFIG, null, 2)}\n`)
252
+ }
253
+ return {file: target.file, created: true, changes: [], wrote: !dryRun}
254
+ }
255
+
256
+ if (target.unreadable) {
257
+ return {file: target.file, created: false, changes: [], wrote: false, unreadable: true}
258
+ }
259
+
260
+ const effective = effectiveCompilerOptions(target.file)
261
+ const changes = planTsconfigChanges(effective)
262
+ if (changes.length === 0) {
263
+ return {file: target.file, created: false, changes, wrote: false}
264
+ }
265
+
266
+ if (!dryRun) {
267
+ const doc = readJsonc(target.file)
268
+ const options: Record<string, unknown> = doc.compilerOptions ?? {}
269
+ doc.compilerOptions = options
270
+ for (const change of changes) {
271
+ if (change.merge && change.key === 'lib') {
272
+ const current = Array.isArray(options['lib']) ? (options['lib'] as unknown[]) : []
273
+ options['lib'] = [...current, ...(change.to as unknown[])]
274
+ } else {
275
+ options[change.key] = change.to
276
+ }
277
+ }
278
+ writeFileSync(target.file, `${stringify(doc, null, 2)}\n`)
279
+ }
280
+
281
+ return {file: target.file, created: false, changes, wrote: !dryRun}
282
+ }
@@ -0,0 +1,77 @@
1
+ export type Framework =
2
+ | 'next-app'
3
+ | 'next-pages'
4
+ | 'react-router'
5
+ | 'remix'
6
+ | 'vite'
7
+ | 'astro'
8
+ | 'react'
9
+
10
+ export type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun'
11
+
12
+ export interface PackageJson {
13
+ name?: string
14
+ version?: string
15
+ dependencies?: Record<string, string>
16
+ devDependencies?: Record<string, string>
17
+ peerDependencies?: Record<string, string>
18
+ packageManager?: string
19
+ eslintConfig?: unknown
20
+ }
21
+
22
+ export interface ReactInfo {
23
+ version: string | null
24
+ source: 'installed' | 'declared' | 'missing'
25
+ }
26
+
27
+ export interface EntryInfo {
28
+ entry: string
29
+ exists: boolean
30
+ }
31
+
32
+ export interface Detected {
33
+ pkg: PackageJson | null
34
+ packageManager: PackageManager
35
+ framework: Framework
36
+ typescript: boolean
37
+ entry: string
38
+ exists: boolean
39
+ react: ReactInfo
40
+ aliasedInstall: string | null
41
+ legacyInstall: string | null
42
+ }
43
+
44
+ export type CheckStatus = 'pass' | 'warn' | 'fail'
45
+
46
+ /**
47
+ * Result of one verification. `status` carries the three-way outcome; `ok` is the
48
+ * boolean shorthand. A `warn` is not ok but does not fail the run (e.g. React not
49
+ * detected at all, versus a hard `fail` like a missing stylesheet import).
50
+ */
51
+ export interface Check {
52
+ id: string
53
+ title: string
54
+ ok: boolean
55
+ status: CheckStatus
56
+ detail?: string
57
+ fix?: string
58
+ }
59
+
60
+ /**
61
+ * One pending change to a compilerOptions key. `merge` appends to an array value
62
+ * (used for `lib`) instead of replacing it. `load` marks options that actually
63
+ * break consumption, which doctor reports as failures rather than optional extras.
64
+ */
65
+ export interface TsconfigChange {
66
+ key: string
67
+ from: unknown
68
+ to: unknown
69
+ merge?: boolean
70
+ load?: boolean
71
+ }
72
+
73
+ export interface CliOptions {
74
+ cwd: string
75
+ dry: boolean
76
+ yes: boolean
77
+ }
@@ -0,0 +1,35 @@
1
+ import {describe, expect, it} from 'vitest'
2
+
3
+ import {meetsNode, meetsReact, parseVersion} from './versions.js'
4
+
5
+ describe('parseVersion', () => {
6
+ it('parses full and partial versions', () => {
7
+ expect(parseVersion('19.2.6')).toEqual([19, 2, 6])
8
+ expect(parseVersion('19.2')).toEqual([19, 2, 0])
9
+ expect(parseVersion('^19.2.0')).toEqual([19, 2, 0])
10
+ expect(parseVersion('not-a-version')).toBeNull()
11
+ })
12
+ })
13
+
14
+ describe('meetsReact', () => {
15
+ it('requires >= 19.2', () => {
16
+ expect(meetsReact('19.2.0')).toBe(true)
17
+ expect(meetsReact('19.2.6')).toBe(true)
18
+ expect(meetsReact('20.0.0')).toBe(true)
19
+ expect(meetsReact('19.1.9')).toBe(false)
20
+ expect(meetsReact('18.3.1')).toBe(false)
21
+ })
22
+ })
23
+
24
+ describe('meetsNode', () => {
25
+ it('implements >=20.19 <22 || >=22.12', () => {
26
+ expect(meetsNode('20.19.0')).toBe(true)
27
+ expect(meetsNode('21.7.0')).toBe(true)
28
+ expect(meetsNode('22.12.0')).toBe(true)
29
+ expect(meetsNode('24.0.0')).toBe(true)
30
+ expect(meetsNode('20.18.0')).toBe(false)
31
+ expect(meetsNode('22.0.0')).toBe(false)
32
+ expect(meetsNode('22.11.9')).toBe(false)
33
+ expect(meetsNode('18.20.0')).toBe(false)
34
+ })
35
+ })
@@ -0,0 +1,41 @@
1
+ export const REACT_MIN = '19.2'
2
+ export const NODE_REQUIREMENT = '>=20.19 <22 || >=22.12'
3
+
4
+ type Version = [number, number, number]
5
+
6
+ /** Parses a loose semver string into [major, minor, patch]; missing parts are 0. */
7
+ export function parseVersion(value: unknown): Version | null {
8
+ if (typeof value !== 'string') return null
9
+ const match = value.match(/(\d+)(?:\.(\d+))?(?:\.(\d+))?/)
10
+ if (!match) return null
11
+ return [Number(match[1] ?? 0), Number(match[2] ?? 0), Number(match[3] ?? 0)]
12
+ }
13
+
14
+ function gte(version: unknown, target: unknown): boolean {
15
+ const a = parseVersion(version)
16
+ const b = parseVersion(target)
17
+ if (!a || !b) return false
18
+ for (let i = 0; i < 3; i++) {
19
+ const left = a[i] ?? 0
20
+ const right = b[i] ?? 0
21
+ if (left > right) return true
22
+ if (left < right) return false
23
+ }
24
+ return true
25
+ }
26
+
27
+ export function meetsReact(version: unknown): boolean {
28
+ return gte(version, REACT_MIN)
29
+ }
30
+
31
+ /** Implements the package's engines range: >=20.19 <22 || >=22.12 */
32
+ export function meetsNode(version: unknown): boolean {
33
+ const v = parseVersion(version)
34
+ if (!v) return false
35
+ const [major, minor] = v
36
+ if (major === 20 && minor >= 19) return true
37
+ if (major === 21) return true
38
+ if (major === 22 && minor >= 12) return true
39
+ if (major > 22) return true
40
+ return false
41
+ }
@@ -0,0 +1,53 @@
1
+ import clsx from 'clsx'
2
+ import {isValidElement, type ComponentPropsWithRef, type ElementType} from 'react'
3
+
4
+ import {getProps} from '../../utils/getProps'
5
+ import {suffixClassName} from '../../utils/suffixClassName'
6
+ import {Icon} from '../icon/Icon'
7
+ import {Text} from '../text/Text'
8
+ import {type BadgeProps, badgeProps} from './badge.props'
9
+
10
+ const badgeClassName = suffixClassName('sui-Badge')
11
+
12
+ /** @public */
13
+ export function Badge<T extends ElementType = 'span'>({
14
+ tone = 'neutral',
15
+ ...props
16
+ }: BadgeProps<T> & Omit<ComponentPropsWithRef<T>, keyof BadgeProps<T>>) {
17
+ const {
18
+ as,
19
+ children,
20
+ className,
21
+ style,
22
+ iconStart: IconStart,
23
+ text,
24
+ ...rest
25
+ } = getProps({tone, ...props}, badgeProps)
26
+ const Component = as || 'span'
27
+
28
+ const paddingClasses = IconStart ? 'sui-p1' : 'sui-px2 sui-py1'
29
+
30
+ const badgeClasses = clsx(
31
+ badgeClassName,
32
+ `${paddingClasses} sui-radius-full sui-display-inline-flex sui-align-items-center sui-gap1`,
33
+ )
34
+
35
+ return (
36
+ <Component className={clsx(badgeClasses, className)} style={style} data-ui="Badge" {...rest}>
37
+ {IconStart &&
38
+ (isValidElement(IconStart) ? (
39
+ IconStart
40
+ ) : (
41
+ <Icon icon={IconStart} marginX={-1} marginY={-2} size={0} />
42
+ ))}
43
+ {text && (
44
+ <Text size={1} trim>
45
+ {text}
46
+ </Text>
47
+ )}
48
+ {children}
49
+ </Component>
50
+ )
51
+ }
52
+
53
+ export type {BadgeProps}
@@ -0,0 +1,6 @@
1
+ .sui-Badge {
2
+ --badge-background: var(--background-high);
3
+ --badge-color: var(--foreground-high);
4
+ background-color: var(--badge-background);
5
+ color: var(--badge-color);
6
+ }