@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,205 @@
|
|
|
1
|
+
import {existsSync, readFileSync} from 'node:fs'
|
|
2
|
+
import {join} from 'node:path'
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
Detected,
|
|
6
|
+
EntryInfo,
|
|
7
|
+
Framework,
|
|
8
|
+
PackageJson,
|
|
9
|
+
PackageManager,
|
|
10
|
+
ReactInfo,
|
|
11
|
+
} from './types.js'
|
|
12
|
+
import {parseVersion} from './versions.js'
|
|
13
|
+
|
|
14
|
+
export const PACKAGE_NAME = '@sanity/ui'
|
|
15
|
+
|
|
16
|
+
export const LEGACY_COEXISTENCE_HINT =
|
|
17
|
+
'To add v5 alongside an existing @sanity/ui install, use a package alias — see the README section "For apps using Sanity UI v3".'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Framework ids map 1:1 to the `framework` field written into sanity-ui.json.
|
|
21
|
+
* Each carries the ordered list of candidate entry files where the stylesheet
|
|
22
|
+
* import belongs.
|
|
23
|
+
*/
|
|
24
|
+
const FRAMEWORK_ENTRIES: Record<Framework, string[]> = {
|
|
25
|
+
'next-app': ['app/layout.tsx', 'app/layout.jsx', 'src/app/layout.tsx', 'src/app/layout.jsx'],
|
|
26
|
+
'next-pages': ['pages/_app.tsx', 'pages/_app.jsx', 'src/pages/_app.tsx', 'src/pages/_app.jsx'],
|
|
27
|
+
'react-router': ['app/root.tsx', 'app/root.jsx'],
|
|
28
|
+
'remix': ['app/root.tsx', 'app/root.jsx'],
|
|
29
|
+
'vite': ['src/main.tsx', 'src/main.jsx', 'src/main.ts'],
|
|
30
|
+
'astro': [],
|
|
31
|
+
'react': ['src/index.tsx', 'src/main.tsx', 'src/index.jsx', 'src/main.jsx'],
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function readPackageJson(cwd: string): PackageJson | null {
|
|
35
|
+
const file = join(cwd, 'package.json')
|
|
36
|
+
if (!existsSync(file)) return null
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(readFileSync(file, 'utf8')) as PackageJson
|
|
39
|
+
} catch {
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function allDeps(pkg: PackageJson | null): Record<string, string | undefined> {
|
|
45
|
+
if (!pkg) return {}
|
|
46
|
+
return {...pkg.dependencies, ...pkg.devDependencies, ...pkg.peerDependencies}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function detectPackageManager(cwd: string): PackageManager {
|
|
50
|
+
if (existsSync(join(cwd, 'bun.lockb')) || existsSync(join(cwd, 'bun.lock'))) return 'bun'
|
|
51
|
+
if (existsSync(join(cwd, 'pnpm-lock.yaml'))) return 'pnpm'
|
|
52
|
+
if (existsSync(join(cwd, 'yarn.lock'))) return 'yarn'
|
|
53
|
+
if (existsSync(join(cwd, 'package-lock.json'))) return 'npm'
|
|
54
|
+
|
|
55
|
+
const declared = readPackageJson(cwd)?.packageManager
|
|
56
|
+
if (typeof declared === 'string') {
|
|
57
|
+
const name = declared.split('@')[0]
|
|
58
|
+
if (name === 'pnpm' || name === 'yarn' || name === 'npm' || name === 'bun') return name
|
|
59
|
+
}
|
|
60
|
+
return 'npm'
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function detectFramework(
|
|
64
|
+
cwd: string,
|
|
65
|
+
pkg: PackageJson | null = readPackageJson(cwd),
|
|
66
|
+
): Framework {
|
|
67
|
+
const deps = allDeps(pkg)
|
|
68
|
+
const has = (name: string): boolean => Boolean(deps[name])
|
|
69
|
+
const dir = (relative: string): boolean => existsSync(join(cwd, relative))
|
|
70
|
+
|
|
71
|
+
if (has('next')) {
|
|
72
|
+
if (dir('app') || dir('src/app')) return 'next-app'
|
|
73
|
+
if (dir('pages') || dir('src/pages')) return 'next-pages'
|
|
74
|
+
return 'next-app'
|
|
75
|
+
}
|
|
76
|
+
if (has('@remix-run/react') || has('@remix-run/dev')) return 'remix'
|
|
77
|
+
if (has('@react-router/dev') || has('react-router')) return 'react-router'
|
|
78
|
+
if (has('astro')) return 'astro'
|
|
79
|
+
if (has('vite')) return 'vite'
|
|
80
|
+
return 'react'
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function detectTypeScript(
|
|
84
|
+
cwd: string,
|
|
85
|
+
pkg: PackageJson | null = readPackageJson(cwd),
|
|
86
|
+
): boolean {
|
|
87
|
+
return existsSync(join(cwd, 'tsconfig.json')) || Boolean(allDeps(pkg)['typescript'])
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Returns the first existing candidate entry file, otherwise the first
|
|
92
|
+
* candidate as the place to create one. Astro has no fixed entry, so we look
|
|
93
|
+
* for a layout component.
|
|
94
|
+
*/
|
|
95
|
+
export function detectEntry(cwd: string, framework: Framework): EntryInfo {
|
|
96
|
+
if (framework === 'astro') return detectAstroLayout(cwd)
|
|
97
|
+
|
|
98
|
+
const candidates = FRAMEWORK_ENTRIES[framework]
|
|
99
|
+
const existing = candidates.find((rel) => existsSync(join(cwd, rel)))
|
|
100
|
+
return {entry: existing ?? candidates[0] ?? 'src/index.tsx', exists: Boolean(existing)}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function detectAstroLayout(cwd: string): EntryInfo {
|
|
104
|
+
const candidates = [
|
|
105
|
+
'src/layouts/Layout.astro',
|
|
106
|
+
'src/layouts/BaseLayout.astro',
|
|
107
|
+
'src/layouts/index.astro',
|
|
108
|
+
'src/pages/index.astro',
|
|
109
|
+
]
|
|
110
|
+
const existing = candidates.find((rel) => existsSync(join(cwd, rel)))
|
|
111
|
+
return {entry: existing ?? candidates[0] ?? 'src/layouts/Layout.astro', exists: Boolean(existing)}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Resolves the installed React version from node_modules, falling back to the
|
|
116
|
+
* declared range in package.json (carets/tildes stripped).
|
|
117
|
+
*/
|
|
118
|
+
export function detectReactVersion(
|
|
119
|
+
cwd: string,
|
|
120
|
+
pkg: PackageJson | null = readPackageJson(cwd),
|
|
121
|
+
): ReactInfo {
|
|
122
|
+
const installed = readInstalledVersion(cwd, 'react')
|
|
123
|
+
if (installed) return {version: installed, source: 'installed'}
|
|
124
|
+
|
|
125
|
+
const declared = allDeps(pkg)['react']
|
|
126
|
+
if (typeof declared === 'string') {
|
|
127
|
+
return {version: declared.replace(/^[\^~>=<\s]+/, ''), source: 'declared'}
|
|
128
|
+
}
|
|
129
|
+
return {version: null, source: 'missing'}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function readInstalledVersion(cwd: string, name: string): string | null {
|
|
133
|
+
const file = join(cwd, 'node_modules', name, 'package.json')
|
|
134
|
+
if (!existsSync(file)) return null
|
|
135
|
+
try {
|
|
136
|
+
const pkg = JSON.parse(readFileSync(file, 'utf8')) as PackageJson
|
|
137
|
+
return pkg.version ?? null
|
|
138
|
+
} catch {
|
|
139
|
+
return null
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function isPackageInstalled(cwd: string, name: string): boolean {
|
|
144
|
+
return existsSync(join(cwd, 'node_modules', name, 'package.json'))
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function readInstalledPackageVersion(cwd: string, name: string): string | null {
|
|
148
|
+
return readInstalledVersion(cwd, name)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function isLegacyUiVersion(version: unknown): boolean {
|
|
152
|
+
const major = parseVersion(version)?.[0]
|
|
153
|
+
return major !== undefined && major < 5
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function detectAliasedInstall(deps: Record<string, string | undefined>): string | null {
|
|
157
|
+
for (const [name, spec] of Object.entries(deps)) {
|
|
158
|
+
if (typeof spec === 'string' && /^npm:@sanity\/ui@(5(?:\..*)?|alpha)$/.test(spec)) return name
|
|
159
|
+
}
|
|
160
|
+
return null
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Reads the major from a declared range, skipping workspace/catalog/link specs. */
|
|
164
|
+
function declaredMajor(spec: string | undefined): number | null {
|
|
165
|
+
if (typeof spec !== 'string') return null
|
|
166
|
+
if (/^(workspace:|catalog:|link:|file:)/.test(spec)) return null
|
|
167
|
+
|
|
168
|
+
const npmAlias = spec.match(/^npm:@sanity\/ui@(\d+)/)
|
|
169
|
+
if (npmAlias) return Number(npmAlias[1])
|
|
170
|
+
|
|
171
|
+
return parseVersion(spec.replace(/^[\^~>=<\s]+/, ''))?.[0] ?? null
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function detectLegacyInstall(cwd: string, deps: Record<string, string | undefined>): string | null {
|
|
175
|
+
const installed = readInstalledPackageVersion(cwd, PACKAGE_NAME)
|
|
176
|
+
if (installed && isLegacyUiVersion(installed)) {
|
|
177
|
+
return installed
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const declared = deps[PACKAGE_NAME]
|
|
181
|
+
const major = declaredMajor(declared)
|
|
182
|
+
if (major !== null && major < 5) {
|
|
183
|
+
return declared ?? 'unknown'
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return null
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function detect(cwd: string): Detected {
|
|
190
|
+
const pkg = readPackageJson(cwd)
|
|
191
|
+
const deps = allDeps(pkg)
|
|
192
|
+
const framework = detectFramework(cwd, pkg)
|
|
193
|
+
const entry = detectEntry(cwd, framework)
|
|
194
|
+
const typescript = detectTypeScript(cwd, pkg) || (entry.exists && /\.tsx?$/.test(entry.entry))
|
|
195
|
+
return {
|
|
196
|
+
pkg,
|
|
197
|
+
packageManager: detectPackageManager(cwd),
|
|
198
|
+
framework,
|
|
199
|
+
typescript,
|
|
200
|
+
...entry,
|
|
201
|
+
react: detectReactVersion(cwd, pkg),
|
|
202
|
+
aliasedInstall: detectAliasedInstall(deps),
|
|
203
|
+
legacyInstall: detectLegacyInstall(cwd, deps),
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {mkdtempSync, rmSync, writeFileSync} from 'node:fs'
|
|
2
|
+
import {tmpdir} from 'node:os'
|
|
3
|
+
import {join} from 'node:path'
|
|
4
|
+
|
|
5
|
+
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
|
|
6
|
+
|
|
7
|
+
import * as checks from './checks.js'
|
|
8
|
+
import {runDoctor} from './doctor.js'
|
|
9
|
+
import {writeInstalledUiVersion} from './test-utils.js'
|
|
10
|
+
|
|
11
|
+
describe('runDoctor', () => {
|
|
12
|
+
let dir: string
|
|
13
|
+
let output: string
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
dir = mkdtempSync(join(tmpdir(), 'sui-doctor-'))
|
|
17
|
+
output = ''
|
|
18
|
+
vi.spyOn(process.stdout, 'write').mockImplementation((chunk) => {
|
|
19
|
+
output += String(chunk)
|
|
20
|
+
return true
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
rmSync(dir, {recursive: true, force: true})
|
|
26
|
+
vi.restoreAllMocks()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('warns and skips checks when @sanity/ui is pre-v5', () => {
|
|
30
|
+
writeFileSync(join(dir, 'package.json'), JSON.stringify({dependencies: {}}))
|
|
31
|
+
writeInstalledUiVersion(dir, '3.5.1')
|
|
32
|
+
const collectChecks = vi.spyOn(checks, 'collectChecks')
|
|
33
|
+
|
|
34
|
+
expect(runDoctor({cwd: dir})).toBe(0)
|
|
35
|
+
expect(output).toMatch(/@sanity\/ui@\^?3\.5\.1 \(pre-v5\)/)
|
|
36
|
+
expect(output).toMatch(/doctor checks v5 installs only/)
|
|
37
|
+
expect(collectChecks).not.toHaveBeenCalled()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('warns and skips checks when v5 is installed under an alias', () => {
|
|
41
|
+
writeFileSync(
|
|
42
|
+
join(dir, 'package.json'),
|
|
43
|
+
JSON.stringify({dependencies: {ui5: 'npm:@sanity/ui@alpha'}}),
|
|
44
|
+
)
|
|
45
|
+
const collectChecks = vi.spyOn(checks, 'collectChecks')
|
|
46
|
+
|
|
47
|
+
expect(runDoctor({cwd: dir})).toBe(0)
|
|
48
|
+
expect(output).toMatch(/alias "ui5"/)
|
|
49
|
+
expect(output).toMatch(/doctor checks direct @sanity\/ui v5 installs only/)
|
|
50
|
+
expect(collectChecks).not.toHaveBeenCalled()
|
|
51
|
+
})
|
|
52
|
+
})
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {collectChecks} from './checks.js'
|
|
2
|
+
import {detect, LEGACY_COEXISTENCE_HINT, PACKAGE_NAME} from './detect.js'
|
|
3
|
+
import {color, fail, heading, info, success, warn} from './log.js'
|
|
4
|
+
import type {Check, CliOptions} from './types.js'
|
|
5
|
+
|
|
6
|
+
export function printChecks(checks: Check[]): void {
|
|
7
|
+
for (const c of checks) {
|
|
8
|
+
const label = `${color.bold(c.title)}${c.detail ? ` ${color.dim(`— ${c.detail}`)}` : ''}`
|
|
9
|
+
if (c.status === 'pass') success(label)
|
|
10
|
+
else if (c.status === 'warn') warn(label)
|
|
11
|
+
else fail(label)
|
|
12
|
+
|
|
13
|
+
if (c.status !== 'pass' && c.fix) {
|
|
14
|
+
info(` ${color.dim('fix:')} ${c.fix}`)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Runs every check and prints it. Returns 1 only when something fails; warnings still pass. */
|
|
20
|
+
export function runDoctor({cwd}: Pick<CliOptions, 'cwd'>): number {
|
|
21
|
+
const detected = detect(cwd)
|
|
22
|
+
|
|
23
|
+
heading('Sanity UI doctor')
|
|
24
|
+
|
|
25
|
+
if (detected.aliasedInstall) {
|
|
26
|
+
warn(
|
|
27
|
+
`This project installs v5 under the alias "${detected.aliasedInstall}". doctor checks direct @sanity/ui v5 installs only.`,
|
|
28
|
+
)
|
|
29
|
+
info(` ${color.dim('hint:')} ${LEGACY_COEXISTENCE_HINT}`)
|
|
30
|
+
return 0
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (detected.legacyInstall) {
|
|
34
|
+
warn(
|
|
35
|
+
`This project has ${PACKAGE_NAME}@${detected.legacyInstall} (pre-v5). doctor checks v5 installs only.`,
|
|
36
|
+
)
|
|
37
|
+
info(` ${color.dim('hint:')} ${LEGACY_COEXISTENCE_HINT}`)
|
|
38
|
+
return 0
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const {checks} = collectChecks(cwd)
|
|
42
|
+
info(
|
|
43
|
+
color.dim(
|
|
44
|
+
`framework: ${detected.framework} · package manager: ${detected.packageManager} · entry: ${detected.entry}`,
|
|
45
|
+
),
|
|
46
|
+
)
|
|
47
|
+
info('')
|
|
48
|
+
|
|
49
|
+
printChecks(checks)
|
|
50
|
+
|
|
51
|
+
const failed = checks.filter((c) => c.status === 'fail')
|
|
52
|
+
const warned = checks.filter((c) => c.status === 'warn')
|
|
53
|
+
|
|
54
|
+
info('')
|
|
55
|
+
if (failed.length === 0 && warned.length === 0) {
|
|
56
|
+
success('Everything looks good.')
|
|
57
|
+
return 0
|
|
58
|
+
}
|
|
59
|
+
if (failed.length === 0) {
|
|
60
|
+
warn(`${warned.length} warning${warned.length === 1 ? '' : 's'}, no blocking issues.`)
|
|
61
|
+
return 0
|
|
62
|
+
}
|
|
63
|
+
fail(`${failed.length} issue${failed.length === 1 ? '' : 's'} found. See fixes above.`)
|
|
64
|
+
return 1
|
|
65
|
+
}
|
package/src/cli/index.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import {resolve} from 'node:path'
|
|
2
|
+
import process from 'node:process'
|
|
3
|
+
import {parseArgs} from 'node:util'
|
|
4
|
+
|
|
5
|
+
import {runDoctor} from './doctor.js'
|
|
6
|
+
import {runInit} from './init.js'
|
|
7
|
+
import {color, fail, info} from './log.js'
|
|
8
|
+
import {readSelfPackage} from './selfPackage.js'
|
|
9
|
+
|
|
10
|
+
const HELP = `${color.bold('@sanity/ui')} setup CLI
|
|
11
|
+
|
|
12
|
+
${color.bold('Usage')}
|
|
13
|
+
npx @sanity/ui@alpha <command> [options]
|
|
14
|
+
|
|
15
|
+
${color.bold('Commands')}
|
|
16
|
+
init Scaffold a correct Sanity UI install (deps, stylesheet, tsconfig)
|
|
17
|
+
doctor Check an existing setup and report fixes
|
|
18
|
+
|
|
19
|
+
${color.bold('Options')}
|
|
20
|
+
--dry Print the plan and exit without changing anything (init)
|
|
21
|
+
--yes, -y Accept every prompt with its default (non-interactive)
|
|
22
|
+
--cwd <dir> Run against another directory (default: current directory)
|
|
23
|
+
--version Print the CLI version
|
|
24
|
+
--help, -h Show this help
|
|
25
|
+
`
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parses arguments and dispatches to a command. `parseArgs` runs in strict mode,
|
|
29
|
+
* so an unknown flag throws from here and is caught by the bin wrapper. Exit
|
|
30
|
+
* codes: 0 on success or an explicit `--help`, 1 on misuse (no or unknown command).
|
|
31
|
+
*/
|
|
32
|
+
export async function main(argv: string[]): Promise<number> {
|
|
33
|
+
const {values, positionals} = parseArgs({
|
|
34
|
+
args: argv,
|
|
35
|
+
allowPositionals: true,
|
|
36
|
+
options: {
|
|
37
|
+
dry: {type: 'boolean', default: false},
|
|
38
|
+
yes: {type: 'boolean', short: 'y', default: false},
|
|
39
|
+
cwd: {type: 'string'},
|
|
40
|
+
version: {type: 'boolean', default: false},
|
|
41
|
+
help: {type: 'boolean', short: 'h', default: false},
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
if (values.version) {
|
|
46
|
+
info(readSelfPackage().version ?? 'unknown')
|
|
47
|
+
return 0
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const command = positionals[0]
|
|
51
|
+
|
|
52
|
+
if (values.help) {
|
|
53
|
+
info(HELP)
|
|
54
|
+
return 0
|
|
55
|
+
}
|
|
56
|
+
if (!command) {
|
|
57
|
+
info(HELP)
|
|
58
|
+
return 1
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const options = {
|
|
62
|
+
dry: values.dry ?? false,
|
|
63
|
+
yes: values.yes ?? false,
|
|
64
|
+
cwd: values.cwd ? resolve(process.cwd(), values.cwd) : process.cwd(),
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
switch (command) {
|
|
68
|
+
case 'init':
|
|
69
|
+
return runInit(options)
|
|
70
|
+
case 'doctor':
|
|
71
|
+
return runDoctor(options)
|
|
72
|
+
default:
|
|
73
|
+
fail(`Unknown command: ${command}`)
|
|
74
|
+
info(HELP)
|
|
75
|
+
return 1
|
|
76
|
+
}
|
|
77
|
+
}
|
package/src/cli/init.ts
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import {basename} from 'node:path'
|
|
2
|
+
import process from 'node:process'
|
|
3
|
+
|
|
4
|
+
import {verifyAfterInit} from './checks.js'
|
|
5
|
+
import {buildConfig, CONFIG_FILE, configExists, writeConfig} from './config.js'
|
|
6
|
+
import {detect, LEGACY_COEXISTENCE_HINT, PACKAGE_NAME} from './detect.js'
|
|
7
|
+
import {printChecks} from './doctor.js'
|
|
8
|
+
import {installCommand, resolveInstallSpecs, resolveTypeSpecs, runInstall} from './install.js'
|
|
9
|
+
import {color, confirm, fail, heading, info, step, success, warn} from './log.js'
|
|
10
|
+
import {inspectStylesheet, STYLES_SPECIFIER, wireStylesheet} from './styles.js'
|
|
11
|
+
import type {StylesheetInfo} from './styles.js'
|
|
12
|
+
import {inspectTsconfig, reconcileTsconfig} from './tsconfig.js'
|
|
13
|
+
import type {TsconfigInspection} from './tsconfig.js'
|
|
14
|
+
import type {CliOptions, Detected} from './types.js'
|
|
15
|
+
import {meetsNode, meetsReact, NODE_REQUIREMENT, REACT_MIN} from './versions.js'
|
|
16
|
+
|
|
17
|
+
interface Plan {
|
|
18
|
+
specs: string[]
|
|
19
|
+
typeSpecs: string[]
|
|
20
|
+
install: string
|
|
21
|
+
stylesheet: StylesheetInfo
|
|
22
|
+
tsconfig: TsconfigInspection | null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Prints the React and Node checks. Missing React only warns, since the user may
|
|
27
|
+
* be scaffolding before installing it; an out-of-range React or Node sets
|
|
28
|
+
* `blocking`. `blocking` never hard-stops init. It only triggers a louder warning
|
|
29
|
+
* before the apply confirmation, so the user stays in control either way.
|
|
30
|
+
*/
|
|
31
|
+
function reportPrereqs({react}: Detected): {blocking: boolean} {
|
|
32
|
+
let blocking = false
|
|
33
|
+
|
|
34
|
+
const reactVersion = react.version
|
|
35
|
+
if (!reactVersion) {
|
|
36
|
+
warn(`React not detected. This package needs React >= ${REACT_MIN}.`)
|
|
37
|
+
} else if (meetsReact(reactVersion)) {
|
|
38
|
+
success(`React ${reactVersion} meets the >= ${REACT_MIN} requirement.`)
|
|
39
|
+
} else {
|
|
40
|
+
warn(`React ${reactVersion} is below ${REACT_MIN}. Components may not render correctly.`)
|
|
41
|
+
info(` ${color.dim('fix:')} Upgrade React to >= ${REACT_MIN}`)
|
|
42
|
+
blocking = true
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const nodeVersion = process.versions.node
|
|
46
|
+
if (meetsNode(nodeVersion)) {
|
|
47
|
+
success(`Node ${nodeVersion} meets the ${NODE_REQUIREMENT} requirement.`)
|
|
48
|
+
} else {
|
|
49
|
+
warn(`Node ${nodeVersion} is outside ${NODE_REQUIREMENT}. Install may behave unexpectedly.`)
|
|
50
|
+
info(` ${color.dim('fix:')} Use Node ${NODE_REQUIREMENT}`)
|
|
51
|
+
blocking = true
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {blocking}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parseReactMajor(version: string | null): number {
|
|
58
|
+
const match = typeof version === 'string' ? version.match(/\d+/) : null
|
|
59
|
+
return match ? Number(match[0]) : 19
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function buildPlan(cwd: string, detected: Detected): Plan {
|
|
63
|
+
const specs = resolveInstallSpecs()
|
|
64
|
+
const {command, args} = installCommand(detected.packageManager, specs)
|
|
65
|
+
const reactMajor = parseReactMajor(detected.react.version)
|
|
66
|
+
const typeSpecs = detected.typescript ? resolveTypeSpecs(cwd, {reactMajor}) : []
|
|
67
|
+
const stylesheet = inspectStylesheet(cwd, detected.entry)
|
|
68
|
+
const tsconfig = detected.typescript ? inspectTsconfig(cwd) : null
|
|
69
|
+
|
|
70
|
+
return {specs, typeSpecs, install: `${command} ${args.join(' ')}`, stylesheet, tsconfig}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function printPlan(cwd: string, detected: Detected, plan: Plan): void {
|
|
74
|
+
heading('Plan')
|
|
75
|
+
|
|
76
|
+
step(`Install: ${color.bold(plan.install)}`)
|
|
77
|
+
|
|
78
|
+
if (plan.typeSpecs.length > 0) {
|
|
79
|
+
step(`Dev types: ${color.bold(plan.typeSpecs.join(' '))}`)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (plan.stylesheet.imported) {
|
|
83
|
+
step(`Stylesheet: already imported in ${plan.stylesheet.importedIn ?? detected.entry}`)
|
|
84
|
+
} else if (plan.stylesheet.exists) {
|
|
85
|
+
step(`Stylesheet: add ${color.bold(`import '${STYLES_SPECIFIER}'`)} to ${detected.entry}`)
|
|
86
|
+
} else {
|
|
87
|
+
step(`Stylesheet: entry ${detected.entry} not found, will skip (add the import manually)`)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (!detected.typescript || !plan.tsconfig) {
|
|
91
|
+
step('tsconfig: no TypeScript detected, skipping')
|
|
92
|
+
} else if (!plan.tsconfig.exists) {
|
|
93
|
+
step('tsconfig: create tsconfig.json with required options')
|
|
94
|
+
} else if (plan.tsconfig.changes.length === 0) {
|
|
95
|
+
step('tsconfig: already has required options')
|
|
96
|
+
} else {
|
|
97
|
+
const summary = plan.tsconfig.changes.map((c) => c.key).join(', ')
|
|
98
|
+
step(`tsconfig: update ${basename(plan.tsconfig.file)} (${summary})`)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
step(configExists(cwd) ? `${CONFIG_FILE}: overwrite` : `${CONFIG_FILE}: create`)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The init lifecycle: detect the project, report prerequisites, build and print
|
|
106
|
+
* the plan, confirm, apply each step, then verify. `--dry` stops after the plan;
|
|
107
|
+
* `--yes` accepts the confirmation without prompting.
|
|
108
|
+
*/
|
|
109
|
+
export async function runInit(options: CliOptions): Promise<number> {
|
|
110
|
+
const {cwd, dry, yes} = options
|
|
111
|
+
const detected = detect(cwd)
|
|
112
|
+
|
|
113
|
+
heading('Sanity UI init')
|
|
114
|
+
info(
|
|
115
|
+
color.dim(
|
|
116
|
+
`framework: ${detected.framework} · package manager: ${detected.packageManager} · entry: ${detected.entry} · typescript: ${detected.typescript}`,
|
|
117
|
+
),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
if (detected.aliasedInstall) {
|
|
121
|
+
fail(
|
|
122
|
+
`This project installs v5 under the alias "${detected.aliasedInstall}". init is for new apps only.`,
|
|
123
|
+
)
|
|
124
|
+
info(` ${color.dim('fix:')} ${LEGACY_COEXISTENCE_HINT}`)
|
|
125
|
+
return 1
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (detected.legacyInstall) {
|
|
129
|
+
fail(
|
|
130
|
+
`This project already has ${PACKAGE_NAME}@${detected.legacyInstall} (pre-v5). init is for new apps only.`,
|
|
131
|
+
)
|
|
132
|
+
info(` ${color.dim('fix:')} ${LEGACY_COEXISTENCE_HINT}`)
|
|
133
|
+
return 1
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
heading('Requirements')
|
|
137
|
+
const {blocking} = reportPrereqs(detected)
|
|
138
|
+
|
|
139
|
+
const plan = buildPlan(cwd, detected)
|
|
140
|
+
printPlan(cwd, detected, plan)
|
|
141
|
+
|
|
142
|
+
if (dry) {
|
|
143
|
+
info('')
|
|
144
|
+
info(color.dim('Dry run: no changes made.'))
|
|
145
|
+
return 0
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (blocking) {
|
|
149
|
+
info('')
|
|
150
|
+
warn('Some requirements are unmet (see above). Continuing may not work.')
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Without a TTY there's no one to answer the prompt, so confirm() falls back to
|
|
154
|
+
// the default (apply). Say so explicitly rather than appearing to act silently;
|
|
155
|
+
// --dry is the preview-only path for anyone who doesn't want writes.
|
|
156
|
+
if (!yes && !process.stdin.isTTY) {
|
|
157
|
+
info('')
|
|
158
|
+
info(color.dim('Non-interactive (no TTY): applying automatically. Use --dry to preview.'))
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const proceed = await confirm('\nApply these changes?', {defaultValue: true, assumeYes: yes})
|
|
162
|
+
if (!proceed) {
|
|
163
|
+
info('Aborted. No changes made.')
|
|
164
|
+
return 0
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
heading('Applying')
|
|
168
|
+
|
|
169
|
+
step(`Installing with ${detected.packageManager}…`)
|
|
170
|
+
const install = runInstall(cwd, detected.packageManager, plan.specs)
|
|
171
|
+
if (install.ok) {
|
|
172
|
+
success('Dependencies installed.')
|
|
173
|
+
} else if (install.reason === 'spawn') {
|
|
174
|
+
warn(`Could not run ${detected.packageManager}: ${install.message ?? 'unknown error'}`)
|
|
175
|
+
info(` ${color.dim('fix:')} Install manually: ${install.command} ${install.args.join(' ')}`)
|
|
176
|
+
} else {
|
|
177
|
+
warn(`Install exited with code ${install.status ?? 'unknown'}.`)
|
|
178
|
+
info(
|
|
179
|
+
` ${color.dim('hint:')} This is usually a peer-dependency conflict. Re-run the install and read the conflict, or retry with your package manager's legacy peer-deps flag.`,
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (plan.typeSpecs.length > 0) {
|
|
184
|
+
step('Installing React type definitions…')
|
|
185
|
+
const typeInstall = runInstall(cwd, detected.packageManager, plan.typeSpecs, {dev: true})
|
|
186
|
+
if (typeInstall.ok) success('Type definitions installed.')
|
|
187
|
+
else warn(`Could not install ${plan.typeSpecs.join(' ')}. Add them manually.`)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (plan.stylesheet.exists && !plan.stylesheet.imported) {
|
|
191
|
+
const wired = wireStylesheet(cwd, detected.entry, {dryRun: false})
|
|
192
|
+
if (wired.wrote) success(`Imported the stylesheet in ${detected.entry}.`)
|
|
193
|
+
} else if (plan.stylesheet.imported) {
|
|
194
|
+
success('Stylesheet already imported.')
|
|
195
|
+
} else {
|
|
196
|
+
warn(`Could not find ${detected.entry}. Add \`import '${STYLES_SPECIFIER}'\` manually.`)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (detected.typescript) {
|
|
200
|
+
const result = reconcileTsconfig(cwd, {dryRun: false})
|
|
201
|
+
if (result.created) success('Created tsconfig.json with required options.')
|
|
202
|
+
else if (result.unreadable) warn('tsconfig.json could not be parsed; left untouched.')
|
|
203
|
+
else if (result.changes.length === 0) success('tsconfig already has the required options.')
|
|
204
|
+
else success(`Updated tsconfig: ${result.changes.map((c) => c.key).join(', ')}.`)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const config = buildConfig({framework: detected.framework, entry: detected.entry})
|
|
208
|
+
writeConfig(cwd, config)
|
|
209
|
+
success(`Wrote ${CONFIG_FILE}.`)
|
|
210
|
+
|
|
211
|
+
heading('Verifying')
|
|
212
|
+
const results = verifyAfterInit(cwd)
|
|
213
|
+
printChecks(results)
|
|
214
|
+
|
|
215
|
+
const failed = results.filter((c) => c.status === 'fail')
|
|
216
|
+
info('')
|
|
217
|
+
if (failed.length === 0) {
|
|
218
|
+
success('Setup verified. Import components from @sanity/ui and start building.')
|
|
219
|
+
return 0
|
|
220
|
+
}
|
|
221
|
+
fail(
|
|
222
|
+
`${failed.length} check${failed.length === 1 ? '' : 's'} failed. Run \`npx ${PACKAGE_NAME}@alpha doctor\` for details.`,
|
|
223
|
+
)
|
|
224
|
+
return 1
|
|
225
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {mkdtempSync, rmSync} from 'node:fs'
|
|
2
|
+
import {tmpdir} from 'node:os'
|
|
3
|
+
import {join} from 'node:path'
|
|
4
|
+
|
|
5
|
+
import {afterEach, beforeEach, describe, expect, it} from 'vitest'
|
|
6
|
+
|
|
7
|
+
import {installCommand, resolveInstallSpecs, resolveTypeSpecs} from './install.js'
|
|
8
|
+
|
|
9
|
+
describe('resolveInstallSpecs', () => {
|
|
10
|
+
it('pins the package to its shipped version and icons to the declared version', () => {
|
|
11
|
+
const specs = resolveInstallSpecs()
|
|
12
|
+
expect(specs[0]).toMatch(/^@sanity\/ui@\d+\.\d+\.\d+/)
|
|
13
|
+
expect(specs.some((s) => s.startsWith('@sanity/icons@'))).toBe(true)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('leaves dependencies the app never imports itself to the package manager', () => {
|
|
17
|
+
const specs = resolveInstallSpecs()
|
|
18
|
+
expect(specs.some((s) => s.startsWith('react-refractor'))).toBe(false)
|
|
19
|
+
expect(specs.some((s) => s.startsWith('clsx'))).toBe(false)
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe('installCommand', () => {
|
|
24
|
+
it('uses install for npm and add for others', () => {
|
|
25
|
+
expect(installCommand('npm', ['x']).args).toEqual(['install', 'x'])
|
|
26
|
+
expect(installCommand('pnpm', ['x']).args).toEqual(['add', 'x'])
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('applies the right dev flag per package manager', () => {
|
|
30
|
+
expect(installCommand('npm', ['x'], {dev: true}).args).toEqual(['install', '--save-dev', 'x'])
|
|
31
|
+
expect(installCommand('pnpm', ['x'], {dev: true}).args).toEqual(['add', '-D', 'x'])
|
|
32
|
+
expect(installCommand('yarn', ['x'], {dev: true}).args).toEqual(['add', '-D', 'x'])
|
|
33
|
+
expect(installCommand('bun', ['x'], {dev: true}).args).toEqual(['add', '-d', 'x'])
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe('resolveTypeSpecs', () => {
|
|
38
|
+
let dir: string
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
dir = mkdtempSync(join(tmpdir(), 'sui-types-'))
|
|
41
|
+
})
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
rmSync(dir, {recursive: true, force: true})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('returns React type packages pinned to the major when none are installed', () => {
|
|
47
|
+
expect(resolveTypeSpecs(dir, {reactMajor: 19})).toEqual([
|
|
48
|
+
'@types/react@^19',
|
|
49
|
+
'@types/react-dom@^19',
|
|
50
|
+
])
|
|
51
|
+
})
|
|
52
|
+
})
|