@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.
- package/README.md +194 -28
- package/bin/sanity-ui.js +14 -0
- package/dist/_chunks-es/LazyRefractor.js +20 -0
- package/dist/_chunks-es/LazyRefractor.js.map +1 -0
- package/dist/cli/checks.js +148 -0
- package/dist/cli/config.js +23 -0
- package/dist/cli/detect.js +181 -0
- package/dist/cli/doctor.js +49 -0
- package/dist/cli/index.js +69 -0
- package/dist/cli/init.js +189 -0
- package/dist/cli/install.js +79 -0
- package/dist/cli/log.js +66 -0
- package/dist/cli/selfPackage.js +11 -0
- package/dist/cli/styles.js +185 -0
- package/dist/cli/test-utils.js +8 -0
- package/dist/cli/tsconfig.js +236 -0
- package/dist/cli/types.js +1 -0
- package/dist/cli/versions.js +45 -0
- package/dist/index.d.ts +1203 -1469
- package/dist/index.js +2123 -1989
- package/dist/index.js.map +1 -1
- package/dist/polyfills.d.ts +1 -0
- package/dist/polyfills.js +5 -0
- package/dist/polyfills.js.map +1 -0
- package/dist/styles.css +1 -547
- package/package.json +57 -58
- package/src/cli/checks.test.ts +38 -0
- package/src/cli/checks.ts +200 -0
- package/src/cli/config.ts +44 -0
- package/src/cli/detect.test.ts +191 -0
- package/src/cli/detect.ts +205 -0
- package/src/cli/doctor.test.ts +52 -0
- package/src/cli/doctor.ts +65 -0
- package/src/cli/index.ts +77 -0
- package/src/cli/init.ts +225 -0
- package/src/cli/install.test.ts +52 -0
- package/src/cli/install.ts +108 -0
- package/src/cli/log.ts +79 -0
- package/src/cli/selfPackage.ts +14 -0
- package/src/cli/styles.test.ts +130 -0
- package/src/cli/styles.ts +205 -0
- package/src/cli/test-utils.ts +10 -0
- package/src/cli/tsconfig.test.ts +174 -0
- package/src/cli/tsconfig.ts +282 -0
- package/src/cli/types.ts +77 -0
- package/src/cli/versions.test.ts +35 -0
- package/src/cli/versions.ts +41 -0
- package/src/components/badge/Badge.tsx +53 -0
- package/src/components/badge/badge.css +6 -0
- package/src/components/badge/badge.props.ts +31 -0
- package/src/components/box/Box.tsx +30 -0
- package/src/components/box/box.css +4 -0
- package/src/components/box/box.props.ts +29 -0
- package/src/components/button/Button.tsx +78 -0
- package/src/components/button/button.css +72 -0
- package/src/components/button/button.props.ts +122 -0
- package/src/components/card/Card.tsx +31 -0
- package/src/components/card/card.css +4 -0
- package/src/components/card/card.props.ts +58 -0
- package/src/components/checkbox/Checkbox.tsx +64 -0
- package/src/components/checkbox/checkbox.css +100 -0
- package/src/components/checkbox/checkbox.props.ts +33 -0
- package/src/components/code/Code.tsx +38 -0
- package/src/components/code/LazyRefractor.tsx +9 -0
- package/src/components/code/code.css +40 -0
- package/src/components/code/code.props.ts +44 -0
- package/src/components/container/Container.tsx +30 -0
- package/src/components/container/container.css +4 -0
- package/src/components/container/container.props.ts +28 -0
- package/src/components/dialog/Dialog.tsx +139 -0
- package/src/components/dialog/dialog.css +49 -0
- package/src/components/dialog/dialog.props.ts +45 -0
- package/src/components/divider/Divider.tsx +23 -0
- package/src/components/divider/divider.props.ts +9 -0
- package/src/components/eyebrow/Eyebrow.tsx +54 -0
- package/src/components/eyebrow/eyebrow.css +11 -0
- package/src/components/eyebrow/eyebrow.props.ts +28 -0
- package/src/components/flex/Flex.tsx +30 -0
- package/src/components/flex/flex.css +4 -0
- package/src/components/flex/flex.props.ts +33 -0
- package/src/components/grid/Grid.tsx +30 -0
- package/src/components/grid/grid.css +4 -0
- package/src/components/grid/grid.props.ts +33 -0
- package/src/components/h-stack/HStack.tsx +34 -0
- package/src/components/h-stack/hStack.props.ts +17 -0
- package/src/components/heading/Heading.tsx +54 -0
- package/src/components/heading/heading.css +9 -0
- package/src/components/heading/heading.props.ts +31 -0
- package/src/components/icon/Icon.tsx +24 -0
- package/src/components/icon/icon.css +17 -0
- package/src/components/icon/icon.props.ts +35 -0
- package/src/components/icon-button/IconButton.tsx +32 -0
- package/src/components/icon-button/iconButton.props.ts +31 -0
- package/src/components/index.css +28 -0
- package/src/components/indicator/Indicator.tsx +42 -0
- package/src/components/indicator/indicator.css +4 -0
- package/src/components/indicator/indicator.props.ts +24 -0
- package/src/components/indicator-stack/IndicatorStack.tsx +31 -0
- package/src/components/indicator-stack/indicator-stack.css +7 -0
- package/src/components/indicator-stack/indicatorStack.props.ts +15 -0
- package/src/components/inline/Inline.tsx +38 -0
- package/src/components/inline/inline.css +4 -0
- package/src/components/inline/inline.props.ts +100 -0
- package/src/components/label/Label.tsx +34 -0
- package/src/components/label/label.css +3 -0
- package/src/components/label/label.props.ts +25 -0
- package/src/components/link/Link.tsx +29 -0
- package/src/components/link/link.css +12 -0
- package/src/components/link/link.props.ts +23 -0
- package/src/components/list/List.tsx +167 -0
- package/src/components/list/list.css +25 -0
- package/src/components/list/list.props.ts +130 -0
- package/src/components/popover/Popover.tsx +85 -0
- package/src/components/popover/popover.css +21 -0
- package/src/components/popover/popover.props.ts +38 -0
- package/src/components/press-area/PressArea.tsx +29 -0
- package/src/components/press-area/press-area.css +22 -0
- package/src/components/press-area/pressArea.props.ts +16 -0
- package/src/components/radio/Radio.tsx +48 -0
- package/src/components/radio/radio.css +97 -0
- package/src/components/radio/radio.props.ts +25 -0
- package/src/components/skip-to-content/SkipToContent.tsx +35 -0
- package/src/components/skip-to-content/skip-to-content.css +9 -0
- package/src/components/skip-to-content/skipToContent.props.ts +25 -0
- package/src/components/spinner/Spinner.tsx +28 -0
- package/src/components/spinner/spinner.css +10 -0
- package/src/components/spinner/spinner.props.ts +19 -0
- package/src/components/switch/Switch.tsx +48 -0
- package/src/components/switch/switch.css +119 -0
- package/src/components/switch/switch.props.ts +25 -0
- package/src/components/text/Text.tsx +53 -0
- package/src/components/text/text.css +9 -0
- package/src/components/text/text.props.ts +28 -0
- package/src/components/tooltip/Tooltip.tsx +97 -0
- package/src/components/tooltip/tooltip.css +31 -0
- package/src/components/tooltip/tooltip.props.ts +36 -0
- package/src/components/tooltip-group/TooltipGroup.tsx +64 -0
- package/src/components/tooltip-group/tooltipGroup.props.ts +15 -0
- package/src/components/v-stack/VStack.tsx +32 -0
- package/src/components/v-stack/vStack.props.ts +17 -0
- package/src/components/visually-hidden/VisuallyHidden.tsx +29 -0
- package/src/components/visually-hidden/visually-hidden.css +9 -0
- package/src/components/visually-hidden/visuallyHidden.props.ts +23 -0
- package/src/css/classes/dynamic/flex-child.css +5 -0
- package/src/css/classes/dynamic/grid-child.css +8 -0
- package/src/css/classes/dynamic/grid-parent.css +6 -0
- package/src/css/classes/dynamic/height.css +5 -0
- package/src/css/classes/dynamic/index.css +7 -0
- package/src/css/classes/dynamic/line-clamp.css +5 -0
- package/src/css/classes/dynamic/readme.md +3 -0
- package/src/css/classes/dynamic/width.css +7 -0
- package/src/css/classes/dynamic/z-index.css +3 -0
- package/src/css/classes/generic/display.css +10 -0
- package/src/css/classes/generic/flex.css +24 -0
- package/src/css/classes/generic/grid.css +7 -0
- package/src/css/classes/generic/index.css +7 -0
- package/src/css/classes/generic/overflow.css +19 -0
- package/src/css/classes/generic/placement.css +202 -0
- package/src/css/classes/generic/position.css +7 -0
- package/src/css/classes/generic/readme.md +3 -0
- package/src/css/classes/generic/text-align.css +5 -0
- package/src/css/classes/index.css +4 -0
- package/src/css/classes/local/animation.css +4 -0
- package/src/css/classes/local/border.css +20 -0
- package/src/css/classes/local/index.css +6 -0
- package/src/css/classes/local/readme.md +3 -0
- package/src/css/classes/local/text-overflow.css +5 -0
- package/src/css/classes/local/text-trim.css +45 -0
- package/src/css/classes/local/tone.css +75 -0
- package/src/css/classes/local/top-anchor.css +4 -0
- package/src/css/classes/system/color.css +223 -0
- package/src/css/classes/system/container.css +8 -0
- package/src/css/classes/system/font-family.css +2 -0
- package/src/css/classes/system/font-weight.css +4 -0
- package/src/css/classes/system/gap.css +37 -0
- package/src/css/classes/system/index.css +10 -0
- package/src/css/classes/system/inset.css +56 -0
- package/src/css/classes/system/margin.css +148 -0
- package/src/css/classes/system/padding.css +78 -0
- package/src/css/classes/system/radius.css +10 -0
- package/src/css/classes/system/readme.md +3 -0
- package/src/css/classes/system/shadow.css +6 -0
- package/src/css/classes/system/typography.css +31 -0
- package/src/css/global/font-face.css +20 -0
- package/src/css/global/index.css +2 -0
- package/src/css/global/reset.css +76 -0
- package/src/css/tokens/index.css +3 -0
- package/src/css/tokens/opacity.css +6 -0
- package/src/css/tokens/semantic.css +25 -0
- package/src/css/tokens/surface.css +27 -0
- package/src/hooks/useIsClient.ts +10 -0
- package/src/index.css +18 -0
- package/src/index.ts +65 -0
- package/src/polyfills.ts +15 -0
- package/src/props/border.ts +51 -0
- package/src/props/flexChild.ts +29 -0
- package/src/props/flexParent.ts +47 -0
- package/src/props/gap.ts +31 -0
- package/src/props/gridChild.ts +50 -0
- package/src/props/gridParent.ts +45 -0
- package/src/props/height.ts +29 -0
- package/src/props/layout.ts +44 -0
- package/src/props/margin.ts +59 -0
- package/src/props/overflow.ts +30 -0
- package/src/props/padding.ts +59 -0
- package/src/props/placement.ts +16 -0
- package/src/props/position.ts +52 -0
- package/src/props/shadow.ts +15 -0
- package/src/props/tone.ts +15 -0
- package/src/props/typography.ts +62 -0
- package/src/props/width.ts +29 -0
- package/src/props/zIndex.ts +15 -0
- package/src/styles.css.d.ts +1 -0
- package/src/styles.css.node.js +2 -0
- package/src/styles.css.test.ts +24 -0
- package/src/types/Button.ts +8 -0
- package/src/types/Code.ts +6 -0
- package/src/types/Container.ts +3 -0
- package/src/types/Density.ts +3 -0
- package/src/types/Display.ts +11 -0
- package/src/types/Eyebrow.ts +3 -0
- package/src/types/Flex.ts +22 -0
- package/src/types/FontWeight.ts +3 -0
- package/src/types/Grid.ts +3 -0
- package/src/types/Heading.ts +6 -0
- package/src/types/Icon.ts +3 -0
- package/src/types/Interactive.ts +8 -0
- package/src/types/List.ts +2 -0
- package/src/types/Overflow.ts +3 -0
- package/src/types/Placement.ts +16 -0
- package/src/types/Position.ts +3 -0
- package/src/types/PropDef.ts +73 -0
- package/src/types/Radius.ts +3 -0
- package/src/types/Responsive.ts +10 -0
- package/src/types/Shadow.ts +3 -0
- package/src/types/Space.ts +15 -0
- package/src/types/Text.ts +3 -0
- package/src/types/TextAlign.ts +3 -0
- package/src/types/Tone.ts +3 -0
- package/src/utils/getProps.test.ts +156 -0
- package/src/utils/getProps.ts +156 -0
- package/src/utils/mergeTriggerProps.ts +66 -0
- package/src/utils/renderPortal.tsx +14 -0
- package/src/utils/suffixClassName.ts +5 -0
- package/src/version.ts +3 -0
- package/LICENSE +0 -21
- package/dist/_system-CTe2lb5l.d.ts +0 -40
- package/dist/_system-CTe2lb5l.d.ts.map +0 -1
- package/dist/arrow-BmX6qbAS.js +0 -170
- package/dist/arrow-BmX6qbAS.js.map +0 -1
- package/dist/autocomplete.d.ts +0 -140
- package/dist/autocomplete.d.ts.map +0 -1
- package/dist/autocomplete.js +0 -470
- package/dist/autocomplete.js.map +0 -1
- package/dist/breadcrumbs.d.ts +0 -18
- package/dist/breadcrumbs.d.ts.map +0 -1
- package/dist/breadcrumbs.js +0 -119
- package/dist/breadcrumbs.js.map +0 -1
- package/dist/button-BNyKSDyA.js +0 -180
- package/dist/button-BNyKSDyA.js.map +0 -1
- package/dist/button-dnqMFkWg.d.ts +0 -76
- package/dist/button-dnqMFkWg.d.ts.map +0 -1
- package/dist/card-C0a0wQTc.d.ts +0 -7
- package/dist/card-C0a0wQTc.d.ts.map +0 -1
- package/dist/code.d.ts +0 -20
- package/dist/code.d.ts.map +0 -1
- package/dist/code.js +0 -90
- package/dist/code.js.map +0 -1
- package/dist/codeFontStyle-84SMhFtI.js +0 -11
- package/dist/codeFontStyle-84SMhFtI.js.map +0 -1
- package/dist/component-DVlwaWB-.d.ts +0 -25
- package/dist/component-DVlwaWB-.d.ts.map +0 -1
- package/dist/element-BYgCE_iK.js +0 -51
- package/dist/element-BYgCE_iK.js.map +0 -1
- package/dist/factory-BzXTX8_Q.js +0 -3056
- package/dist/factory-BzXTX8_Q.js.map +0 -1
- package/dist/flex-DmbDG2ks.js +0 -76
- package/dist/flex-DmbDG2ks.js.map +0 -1
- package/dist/font-EtfxKPUz.d.ts +0 -49
- package/dist/font-EtfxKPUz.d.ts.map +0 -1
- package/dist/getElementRef-Jw7gY0eV.js +0 -102
- package/dist/getElementRef-Jw7gY0eV.js.map +0 -1
- package/dist/getScopedTheme-C-sZsyAz.js +0 -267
- package/dist/getScopedTheme-C-sZsyAz.js.map +0 -1
- package/dist/getTheme_v2-y09KqtEb.js +0 -487
- package/dist/getTheme_v2-y09KqtEb.js.map +0 -1
- package/dist/grid-6JSgMj1O.js +0 -115
- package/dist/grid-6JSgMj1O.js.map +0 -1
- package/dist/helpers-C8xxXAQC.js +0 -63
- package/dist/helpers-C8xxXAQC.js.map +0 -1
- package/dist/hotkeys-CaHsQlsO.js +0 -168
- package/dist/hotkeys-CaHsQlsO.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/layer-Bl5R3uNR.js +0 -61
- package/dist/layer-Bl5R3uNR.js.map +0 -1
- package/dist/layer-ChlP9Ik6.d.ts +0 -242
- package/dist/layer-ChlP9Ik6.d.ts.map +0 -1
- package/dist/menu.d.ts +0 -146
- package/dist/menu.d.ts.map +0 -1
- package/dist/menu.js +0 -626
- package/dist/menu.js.map +0 -1
- package/dist/popover-B7-28L8y.js +0 -408
- package/dist/popover-B7-28L8y.js.map +0 -1
- package/dist/popover-CMlQeKn0.d.ts +0 -96
- package/dist/popover-CMlQeKn0.d.ts.map +0 -1
- package/dist/popover.d.ts +0 -2
- package/dist/popover.js +0 -3
- package/dist/refractor-CC48DwHi.js +0 -20
- package/dist/refractor-CC48DwHi.js.map +0 -1
- package/dist/responsiveFont-CilwaORG.js +0 -55
- package/dist/responsiveFont-CilwaORG.js.map +0 -1
- package/dist/rgba-BSYaEUW6.d.ts +0 -846
- package/dist/rgba-BSYaEUW6.d.ts.map +0 -1
- package/dist/selectable-B0GUIL74.d.ts +0 -7
- package/dist/selectable-B0GUIL74.d.ts.map +0 -1
- package/dist/stack-rsNFPq2-.js +0 -112
- package/dist/stack-rsNFPq2-.js.map +0 -1
- package/dist/styles-css.d.ts +0 -2
- package/dist/styles-css.js +0 -4
- package/dist/textInput-pSfQjkmx.js +0 -313
- package/dist/textInput-pSfQjkmx.js.map +0 -1
- package/dist/theme.d.ts +0 -280
- package/dist/theme.d.ts.map +0 -1
- package/dist/theme.js +0 -55
- package/dist/theme.js.map +0 -1
- package/dist/toast.d.ts +0 -73
- package/dist/toast.d.ts.map +0 -1
- package/dist/toast.js +0 -302
- package/dist/toast.js.map +0 -1
- package/dist/tooltip.d.ts +0 -88
- package/dist/tooltip.d.ts.map +0 -1
- package/dist/tooltip.js +0 -435
- package/dist/tooltip.js.map +0 -1
- package/dist/useClickOutsideEvent-wGLGWBZN.js +0 -36
- package/dist/useClickOutsideEvent-wGLGWBZN.js.map +0 -1
- package/dist/useElementSize-BJvbGHq8.js +0 -51
- package/dist/useElementSize-BJvbGHq8.js.map +0 -1
- package/dist/useLayer-D1usxIE0.js +0 -712
- 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
|
+
}
|
package/src/cli/types.ts
ADDED
|
@@ -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}
|