banhaten 0.1.0
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/LICENSE +21 -0
- package/README.md +361 -0
- package/banhaten.config.example.json +13 -0
- package/package.json +59 -0
- package/registry/assets/activity-feed-avatar.png +0 -0
- package/registry/assets/avatars/avatar-01.jpg +0 -0
- package/registry/assets/avatars/avatar-02.jpg +0 -0
- package/registry/assets/avatars/avatar-03.jpg +0 -0
- package/registry/assets/avatars/avatar-04.jpg +0 -0
- package/registry/assets/avatars/avatar-05.jpg +0 -0
- package/registry/assets/avatars/avatar-06.jpg +0 -0
- package/registry/assets/avatars/avatar-07.jpg +0 -0
- package/registry/assets/avatars/avatar-08.jpg +0 -0
- package/registry/assets/avatars/avatar-09.jpg +0 -0
- package/registry/assets/avatars/avatar-10.jpg +0 -0
- package/registry/assets/avatars/avatar-11.jpg +0 -0
- package/registry/assets/avatars/avatar-12.jpg +0 -0
- package/registry/assets/avatars/avatar-13.jpg +0 -0
- package/registry/assets/avatars/avatar-14.jpg +0 -0
- package/registry/assets/avatars/avatar-15.jpg +0 -0
- package/registry/assets/avatars/avatar-16.jpg +0 -0
- package/registry/assets/avatars/avatar-17.jpg +0 -0
- package/registry/assets/avatars/avatar-18.jpg +0 -0
- package/registry/assets/avatars/avatar-19.jpg +0 -0
- package/registry/assets/avatars/avatar-20.jpg +0 -0
- package/registry/assets/avatars/avatar-21.jpg +0 -0
- package/registry/assets/avatars/avatar-22.jpg +0 -0
- package/registry/assets/avatars/avatar-23.jpg +0 -0
- package/registry/assets/avatars/avatar-24.jpg +0 -0
- package/registry/assets/avatars/avatar-25.jpg +0 -0
- package/registry/assets/avatars/avatar-26.jpg +0 -0
- package/registry/assets/avatars/avatar-27.jpg +0 -0
- package/registry/assets/avatars/avatar-28.jpg +0 -0
- package/registry/assets/avatars/avatar-29.jpg +0 -0
- package/registry/assets/avatars/avatar-30.jpg +0 -0
- package/registry/assets/avatars/avatar-31.jpg +0 -0
- package/registry/assets/avatars/avatar-32.jpg +0 -0
- package/registry/assets/avatars/avatar-33.jpg +0 -0
- package/registry/assets/avatars/avatar-34.jpg +0 -0
- package/registry/assets/avatars/avatar-35.jpg +0 -0
- package/registry/assets/image-assets.json +744 -0
- package/registry/assets/images/art-01.jpg +0 -0
- package/registry/assets/images/art-02.jpg +0 -0
- package/registry/assets/images/art-03.jpg +0 -0
- package/registry/assets/images/art-04.jpg +0 -0
- package/registry/assets/images/art-05.jpg +0 -0
- package/registry/assets/images/art-06.jpg +0 -0
- package/registry/assets/images/art-07.jpg +0 -0
- package/registry/assets/images/art-08.jpg +0 -0
- package/registry/assets/images/art-09.jpg +0 -0
- package/registry/assets/images/art-10.jpg +0 -0
- package/registry/assets/images/art-11.jpg +0 -0
- package/registry/assets/images/art-12.jpg +0 -0
- package/registry/assets/images/art-13.jpg +0 -0
- package/registry/assets/images/art-14.jpg +0 -0
- package/registry/assets/images/art-15.jpg +0 -0
- package/registry/assets/images/art-16.jpg +0 -0
- package/registry/assets/images/art-17.jpg +0 -0
- package/registry/assets/images/art-18.jpg +0 -0
- package/registry/assets/images/art-19.jpg +0 -0
- package/registry/assets/images/art-20.jpg +0 -0
- package/registry/assets/images/art-21.jpg +0 -0
- package/registry/assets/images/art-22.jpg +0 -0
- package/registry/assets/images/art-23.jpg +0 -0
- package/registry/assets/images/art-24.jpg +0 -0
- package/registry/assets/images/art-25.jpg +0 -0
- package/registry/assets/images/art-26.jpg +0 -0
- package/registry/assets/images/art-27.jpg +0 -0
- package/registry/assets/images/nature-01.jpg +0 -0
- package/registry/assets/images/nature-02.jpg +0 -0
- package/registry/assets/images/nature-03.jpg +0 -0
- package/registry/assets/images/nature-04.jpg +0 -0
- package/registry/assets/images/nature-05.jpg +0 -0
- package/registry/assets/images/nature-06.jpg +0 -0
- package/registry/assets/images/nature-07.jpg +0 -0
- package/registry/assets/images/nature-08.jpg +0 -0
- package/registry/assets/images/nature-09.jpg +0 -0
- package/registry/assets/images/nature-10.jpg +0 -0
- package/registry/assets/images/nature-11.jpg +0 -0
- package/registry/assets/images/nature-12.jpg +0 -0
- package/registry/assets/images/nature-13.jpg +0 -0
- package/registry/assets/images/nature-14.jpg +0 -0
- package/registry/assets/images/nature-15.jpg +0 -0
- package/registry/assets/images/nature-16.jpg +0 -0
- package/registry/assets/images/nature-17.jpg +0 -0
- package/registry/assets/images/nature-18.jpg +0 -0
- package/registry/assets/images/nature-19.jpg +0 -0
- package/registry/assets/images/nature-20.jpg +0 -0
- package/registry/components/accordion.tsx +119 -0
- package/registry/components/alert.tsx +282 -0
- package/registry/components/attribute.tsx +452 -0
- package/registry/components/avatar.tsx +142 -0
- package/registry/components/badge.tsx +567 -0
- package/registry/components/button-group.tsx +246 -0
- package/registry/components/button.tsx +102 -0
- package/registry/components/card.tsx +613 -0
- package/registry/components/checkbox.tsx +244 -0
- package/registry/components/date-picker.tsx +1143 -0
- package/registry/components/divider.tsx +82 -0
- package/registry/components/expanded/ActivityFeed.tsx +226 -0
- package/registry/components/expanded/Banner.tsx +145 -0
- package/registry/components/expanded/BannerBoard.tsx +225 -0
- package/registry/components/expanded/Breadcrumbs.tsx +156 -0
- package/registry/components/expanded/CatalogComponentsShowcase.tsx +211 -0
- package/registry/components/expanded/CatalogDivider.tsx +48 -0
- package/registry/components/expanded/CatalogTag.tsx +92 -0
- package/registry/components/expanded/CommandBar.tsx +406 -0
- package/registry/components/expanded/FileUpload.tsx +231 -0
- package/registry/components/expanded/IconExplorer.tsx +612 -0
- package/registry/components/expanded/OnboardingStepListItem.tsx +67 -0
- package/registry/components/expanded/PageHeader.tsx +184 -0
- package/registry/components/expanded/Slideout.tsx +514 -0
- package/registry/components/expanded/Steps.tsx +266 -0
- package/registry/components/expanded/Table.tsx +1014 -0
- package/registry/components/expanded/Tabs.tsx +86 -0
- package/registry/components/expanded/Timeline.tsx +235 -0
- package/registry/components/expanded/TimelineShowcase.tsx +158 -0
- package/registry/components/expanded/activityFeed.css +292 -0
- package/registry/components/expanded/banner.css +312 -0
- package/registry/components/expanded/breadcrumbs.css +140 -0
- package/registry/components/expanded/catalogComponentsShowcase.css +87 -0
- package/registry/components/expanded/commandBar.css +473 -0
- package/registry/components/expanded/divider.css +75 -0
- package/registry/components/expanded/fileUpload.css +228 -0
- package/registry/components/expanded/iconExplorer.css +764 -0
- package/registry/components/expanded/iconPacks.ts +866 -0
- package/registry/components/expanded/onboardingStepListItem.css +126 -0
- package/registry/components/expanded/pageHeader.css +287 -0
- package/registry/components/expanded/slideout.css +955 -0
- package/registry/components/expanded/steps.css +329 -0
- package/registry/components/expanded/table.css +607 -0
- package/registry/components/expanded/tabs.css +197 -0
- package/registry/components/expanded/tag.css +148 -0
- package/registry/components/expanded/timeline.css +282 -0
- package/registry/components/input-content.ts +106 -0
- package/registry/components/input.tsx +866 -0
- package/registry/components/menu.tsx +758 -0
- package/registry/components/modal.tsx +799 -0
- package/registry/components/pagination.tsx +543 -0
- package/registry/components/progress-slider.tsx +216 -0
- package/registry/components/progress.tsx +367 -0
- package/registry/components/radio-card.tsx +654 -0
- package/registry/components/radio-group.tsx +570 -0
- package/registry/components/select-content.tsx +313 -0
- package/registry/components/select.tsx +871 -0
- package/registry/components/slider.tsx +380 -0
- package/registry/components/social-button.tsx +360 -0
- package/registry/components/spinner.tsx +31 -0
- package/registry/components/tag.tsx +423 -0
- package/registry/components/textarea.tsx +625 -0
- package/registry/components/toggle.tsx +272 -0
- package/registry/components/toolbar.tsx +467 -0
- package/registry/components/tooltip.tsx +427 -0
- package/registry/examples/accordion-demo.tsx +34 -0
- package/registry/examples/alert-demo.tsx +14 -0
- package/registry/examples/attribute-demo.tsx +65 -0
- package/registry/examples/avatar-demo.tsx +74 -0
- package/registry/examples/badge-demo.tsx +53 -0
- package/registry/examples/button-demo.tsx +83 -0
- package/registry/examples/button-group-demo.tsx +42 -0
- package/registry/examples/card-demo.tsx +48 -0
- package/registry/examples/checkbox-demo.tsx +67 -0
- package/registry/examples/date-picker-demo.tsx +74 -0
- package/registry/examples/divider-demo.tsx +17 -0
- package/registry/examples/expanded/activity-feed-demo.tsx +22 -0
- package/registry/examples/expanded/banner-demo.tsx +23 -0
- package/registry/examples/expanded/catalog-components-demo.tsx +5 -0
- package/registry/examples/expanded/command-bar-demo.tsx +10 -0
- package/registry/examples/expanded/icons-demo.tsx +5 -0
- package/registry/examples/expanded/onboarding-step-demo.tsx +11 -0
- package/registry/examples/expanded/page-header-demo.tsx +19 -0
- package/registry/examples/expanded/slideout-demo.tsx +15 -0
- package/registry/examples/expanded/steps-demo.tsx +18 -0
- package/registry/examples/expanded/tabs-demo.tsx +13 -0
- package/registry/examples/expanded/timeline-demo.tsx +18 -0
- package/registry/examples/input-demo.tsx +87 -0
- package/registry/examples/menu-demo.tsx +109 -0
- package/registry/examples/modal-demo.tsx +16 -0
- package/registry/examples/pagination-demo.tsx +17 -0
- package/registry/examples/progress-demo.tsx +37 -0
- package/registry/examples/progress-slider-demo.tsx +29 -0
- package/registry/examples/radio-card-demo.tsx +51 -0
- package/registry/examples/radio-group-demo.tsx +62 -0
- package/registry/examples/select-demo.tsx +73 -0
- package/registry/examples/slider-demo.tsx +31 -0
- package/registry/examples/social-button-demo.tsx +51 -0
- package/registry/examples/tag-demo.tsx +29 -0
- package/registry/examples/textarea-demo.tsx +79 -0
- package/registry/examples/toggle-demo.tsx +59 -0
- package/registry/examples/toolbar-demo.tsx +80 -0
- package/registry/examples/tooltip-demo.tsx +115 -0
- package/registry/hooks/use-direction.ts +27 -0
- package/registry/index.json +1213 -0
- package/registry/styles/globals.css +4600 -0
- package/registry/utils/cn.ts +6 -0
- package/src/cli/index.js +826 -0
- package/tokens/Color mode.zip +0 -0
- package/tokens/Numbers.zip +0 -0
- package/tokens/Radius.zip +0 -0
- package/tokens/Theme.zip +0 -0
- package/tokens/banhaten.tokens.json +5525 -0
package/src/cli/index.js
ADDED
|
@@ -0,0 +1,826 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs/promises"
|
|
4
|
+
import path from "node:path"
|
|
5
|
+
import process from "node:process"
|
|
6
|
+
import { fileURLToPath } from "node:url"
|
|
7
|
+
|
|
8
|
+
const packageRoot = path.resolve(
|
|
9
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
10
|
+
"..",
|
|
11
|
+
".."
|
|
12
|
+
)
|
|
13
|
+
const registryRoot = path.join(packageRoot, "registry")
|
|
14
|
+
const registryIndexPath = path.join(registryRoot, "index.json")
|
|
15
|
+
|
|
16
|
+
const CONFIG_FILE = "banhaten.config.json"
|
|
17
|
+
const FONT_IMPORTS = [
|
|
18
|
+
'@import "@fontsource/inter/400.css";',
|
|
19
|
+
'@import "@fontsource/inter/500.css";',
|
|
20
|
+
'@import "@fontsource/inter/600.css";',
|
|
21
|
+
'@import "@fontsource/inter/700.css";',
|
|
22
|
+
]
|
|
23
|
+
const TAILWIND_IMPORT = '@import "tailwindcss";'
|
|
24
|
+
const SETUP_DEV_DEPENDENCIES = {
|
|
25
|
+
tailwindcss: "^4.0.0",
|
|
26
|
+
}
|
|
27
|
+
const VITE_DEV_DEPENDENCIES = {
|
|
28
|
+
"@tailwindcss/vite": "^4.0.0",
|
|
29
|
+
}
|
|
30
|
+
const CSS_START = "/* Banhaten design system tokens:"
|
|
31
|
+
const CSS_END = "/* End Banhaten design system tokens. */"
|
|
32
|
+
const syncExistsCache = new Map()
|
|
33
|
+
|
|
34
|
+
const DEFAULT_CONFIG = {
|
|
35
|
+
$schema: "https://banhaten.dev/schema.json",
|
|
36
|
+
style: "default",
|
|
37
|
+
tsx: true,
|
|
38
|
+
tailwind: {
|
|
39
|
+
css: "app/globals.css",
|
|
40
|
+
},
|
|
41
|
+
aliases: {
|
|
42
|
+
ui: "@/components/ui",
|
|
43
|
+
utils: "@/lib/utils",
|
|
44
|
+
hooks: "@/hooks",
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const registry = await readJson(registryIndexPath)
|
|
49
|
+
|
|
50
|
+
main().catch((error) => {
|
|
51
|
+
console.error(`banhaten: ${error.message}`)
|
|
52
|
+
process.exitCode = 1
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
async function main() {
|
|
56
|
+
const { command, positionals, flags } = parseArgs(process.argv.slice(2))
|
|
57
|
+
|
|
58
|
+
if (!command || command === "help" || flags.help || flags.h) {
|
|
59
|
+
printHelp()
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (command === "list") {
|
|
64
|
+
listComponents()
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (command === "init") {
|
|
69
|
+
await initProject(flags)
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (command === "add") {
|
|
74
|
+
await addComponents(positionals, flags)
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (command === "diff") {
|
|
79
|
+
await diffComponents(positionals, flags)
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (command === "update") {
|
|
84
|
+
await updateComponents(positionals, flags)
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
throw new Error(`unknown command "${command}"`)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function parseArgs(argv) {
|
|
92
|
+
const flags = {}
|
|
93
|
+
const positionals = []
|
|
94
|
+
|
|
95
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
96
|
+
const arg = argv[index]
|
|
97
|
+
|
|
98
|
+
if (arg === "-h") {
|
|
99
|
+
flags.h = true
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!arg.startsWith("--")) {
|
|
104
|
+
positionals.push(arg)
|
|
105
|
+
continue
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const [rawKey, inlineValue] = arg.slice(2).split("=")
|
|
109
|
+
const key = rawKey.trim()
|
|
110
|
+
|
|
111
|
+
if (inlineValue !== undefined) {
|
|
112
|
+
flags[key] = inlineValue
|
|
113
|
+
continue
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const next = argv[index + 1]
|
|
117
|
+
if ((key === "cwd" || key === "out") && next && !next.startsWith("-")) {
|
|
118
|
+
flags[key] = next
|
|
119
|
+
index += 1
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
flags[key] = true
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const [command, ...rest] = positionals
|
|
127
|
+
return { command, positionals: rest, flags }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function printHelp() {
|
|
131
|
+
console.log(`Banhaten design system
|
|
132
|
+
|
|
133
|
+
Usage:
|
|
134
|
+
banhaten init [--cwd <path>] [--force] [--dry-run]
|
|
135
|
+
banhaten add <component...> [--cwd <path>] [--force] [--dry-run]
|
|
136
|
+
banhaten diff [component...] [--cwd <path>] [--force]
|
|
137
|
+
banhaten update [component...] [--cwd <path>] [--force] [--dry-run]
|
|
138
|
+
banhaten list
|
|
139
|
+
|
|
140
|
+
Examples:
|
|
141
|
+
npx banhaten init
|
|
142
|
+
npx banhaten add button
|
|
143
|
+
npx banhaten diff
|
|
144
|
+
npx banhaten update button
|
|
145
|
+
`)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function listComponents() {
|
|
149
|
+
const names = Object.keys(registry.components)
|
|
150
|
+
|
|
151
|
+
console.log("Available Banhaten design system components:")
|
|
152
|
+
for (const name of names) {
|
|
153
|
+
const component = registry.components[name]
|
|
154
|
+
console.log(`- ${name}: ${component.description}`)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function initProject(flags) {
|
|
159
|
+
const cwd = path.resolve(String(flags.cwd || process.cwd()))
|
|
160
|
+
const dryRun = Boolean(flags["dry-run"])
|
|
161
|
+
const force = Boolean(flags.force)
|
|
162
|
+
await warmPathHints(cwd)
|
|
163
|
+
const config = await loadConfig(cwd)
|
|
164
|
+
const writes = []
|
|
165
|
+
|
|
166
|
+
writes.push(
|
|
167
|
+
...(await writeConfig(cwd, config, { dryRun, force })),
|
|
168
|
+
...(await writeBaseFiles(cwd, config, { dryRun, force })),
|
|
169
|
+
...(await writeGlobalCss(cwd, config, { dryRun, force })),
|
|
170
|
+
...(await writeAliasConfig(cwd, config, { dryRun, force }))
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
const dependencyWrites = await updatePackageJson(
|
|
174
|
+
cwd,
|
|
175
|
+
await getPackageChanges(cwd, registry.base.dependencies || {}),
|
|
176
|
+
{ dryRun }
|
|
177
|
+
)
|
|
178
|
+
writes.push(...dependencyWrites)
|
|
179
|
+
|
|
180
|
+
printResult("Initialized Banhaten design system", writes, dryRun)
|
|
181
|
+
printInstallHint(writes, dryRun)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function addComponents(names, flags) {
|
|
185
|
+
if (names.length === 0) {
|
|
186
|
+
throw new Error("missing component name. Try `banhaten add button`.")
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
await writeComponents(names, flags, {
|
|
190
|
+
inferInstalled: false,
|
|
191
|
+
title: `Added ${names.join(", ")}`,
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function diffComponents(names, flags) {
|
|
196
|
+
await writeComponents(names, { ...flags, "dry-run": true }, {
|
|
197
|
+
inferInstalled: true,
|
|
198
|
+
title: names.length
|
|
199
|
+
? `Checked ${names.join(", ")} for updates`
|
|
200
|
+
: "Checked installed components for updates",
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function updateComponents(names, flags) {
|
|
205
|
+
await writeComponents(names, flags, {
|
|
206
|
+
inferInstalled: true,
|
|
207
|
+
title: names.length
|
|
208
|
+
? `Updated ${names.join(", ")}`
|
|
209
|
+
: "Updated installed components",
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function writeComponents(requestedNames, flags, options) {
|
|
214
|
+
const cwd = path.resolve(String(flags.cwd || process.cwd()))
|
|
215
|
+
const dryRun = Boolean(flags["dry-run"])
|
|
216
|
+
const force = Boolean(flags.force)
|
|
217
|
+
await warmPathHints(cwd)
|
|
218
|
+
const config = await loadConfig(cwd)
|
|
219
|
+
const selectedNames =
|
|
220
|
+
requestedNames.length > 0
|
|
221
|
+
? requestedNames
|
|
222
|
+
: options.inferInstalled
|
|
223
|
+
? await findInstalledComponents(cwd, config)
|
|
224
|
+
: requestedNames
|
|
225
|
+
|
|
226
|
+
if (selectedNames.length === 0) {
|
|
227
|
+
throw new Error(
|
|
228
|
+
"no installed components found. Run `banhaten add button` first, or pass component names."
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const names = resolveRegistryDependencies(selectedNames)
|
|
233
|
+
|
|
234
|
+
const writes = []
|
|
235
|
+
const deps = { ...(registry.base.dependencies || {}) }
|
|
236
|
+
|
|
237
|
+
writes.push(
|
|
238
|
+
...(await writeConfig(cwd, config, { dryRun, force })),
|
|
239
|
+
...(await writeBaseFiles(cwd, config, { dryRun, force })),
|
|
240
|
+
...(await writeAliasConfig(cwd, config, { dryRun, force }))
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
for (const name of names) {
|
|
244
|
+
const component = registry.components[name]
|
|
245
|
+
if (!component) {
|
|
246
|
+
throw new Error(`unknown component "${name}". Run \`banhaten list\`.`)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
Object.assign(deps, component.dependencies || {})
|
|
250
|
+
writes.push(
|
|
251
|
+
...(await writeRegistryFiles(cwd, config, component.files || [], {
|
|
252
|
+
dryRun,
|
|
253
|
+
force,
|
|
254
|
+
}))
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
writes.push(...(await updatePackageJson(
|
|
259
|
+
cwd,
|
|
260
|
+
await getPackageChanges(cwd, deps),
|
|
261
|
+
{ dryRun }
|
|
262
|
+
)))
|
|
263
|
+
|
|
264
|
+
printResult(options.title, writes, dryRun)
|
|
265
|
+
printInstallHint(writes, dryRun)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function findInstalledComponents(cwd, config) {
|
|
269
|
+
const installed = []
|
|
270
|
+
|
|
271
|
+
for (const [name, component] of Object.entries(registry.components)) {
|
|
272
|
+
for (const file of component.files || []) {
|
|
273
|
+
const targetPath = path.join(cwd, resolveTarget(cwd, config, file.target))
|
|
274
|
+
if (await exists(targetPath)) {
|
|
275
|
+
installed.push(name)
|
|
276
|
+
break
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return installed
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function resolveRegistryDependencies(names) {
|
|
285
|
+
const resolved = []
|
|
286
|
+
const seen = new Set()
|
|
287
|
+
const visiting = new Set()
|
|
288
|
+
|
|
289
|
+
for (const name of names) {
|
|
290
|
+
visit(name)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return resolved
|
|
294
|
+
|
|
295
|
+
function visit(name) {
|
|
296
|
+
const component = registry.components[name]
|
|
297
|
+
if (!component) {
|
|
298
|
+
throw new Error(`unknown component "${name}". Run \`banhaten list\`.`)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (seen.has(name)) return
|
|
302
|
+
if (visiting.has(name)) {
|
|
303
|
+
throw new Error(`circular registry dependency detected at "${name}".`)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
visiting.add(name)
|
|
307
|
+
for (const dependency of component.registryDependencies || []) {
|
|
308
|
+
visit(dependency)
|
|
309
|
+
}
|
|
310
|
+
visiting.delete(name)
|
|
311
|
+
|
|
312
|
+
seen.add(name)
|
|
313
|
+
resolved.push(name)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function loadConfig(cwd) {
|
|
318
|
+
const configPath = path.join(cwd, CONFIG_FILE)
|
|
319
|
+
const componentsPath = path.join(cwd, "components.json")
|
|
320
|
+
const config = structuredClone(DEFAULT_CONFIG)
|
|
321
|
+
|
|
322
|
+
const componentsConfig = await readJsonIfExists(componentsPath)
|
|
323
|
+
let hasExplicitCssPath = false
|
|
324
|
+
if (componentsConfig) {
|
|
325
|
+
if (componentsConfig.tailwind?.css) {
|
|
326
|
+
config.tailwind.css = componentsConfig.tailwind.css
|
|
327
|
+
hasExplicitCssPath = true
|
|
328
|
+
}
|
|
329
|
+
config.aliases.utils = componentsConfig.aliases?.utils || config.aliases.utils
|
|
330
|
+
config.aliases.hooks = componentsConfig.aliases?.hooks || config.aliases.hooks
|
|
331
|
+
config.aliases.ui =
|
|
332
|
+
componentsConfig.aliases?.ui ||
|
|
333
|
+
appendAliasSegment(componentsConfig.aliases?.components, "ui") ||
|
|
334
|
+
config.aliases.ui
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const localConfig = await readJsonIfExists(configPath)
|
|
338
|
+
if (localConfig) {
|
|
339
|
+
config.style = localConfig.style || config.style
|
|
340
|
+
config.tsx = localConfig.tsx ?? config.tsx
|
|
341
|
+
if (localConfig.tailwind?.css) hasExplicitCssPath = true
|
|
342
|
+
config.tailwind = { ...config.tailwind, ...(localConfig.tailwind || {}) }
|
|
343
|
+
config.aliases = { ...config.aliases, ...(localConfig.aliases || {}) }
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (!hasExplicitCssPath) {
|
|
347
|
+
config.tailwind.css = await detectCssPath(cwd, config.tailwind.css)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return config
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function detectCssPath(cwd, fallback) {
|
|
354
|
+
const candidates = [
|
|
355
|
+
"app/globals.css",
|
|
356
|
+
"src/app/globals.css",
|
|
357
|
+
"src/index.css",
|
|
358
|
+
"src/globals.css",
|
|
359
|
+
"styles/globals.css",
|
|
360
|
+
]
|
|
361
|
+
|
|
362
|
+
for (const candidate of candidates) {
|
|
363
|
+
if (await exists(path.join(cwd, candidate))) return candidate
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (await exists(path.join(cwd, "src", "app"))) return "src/app/globals.css"
|
|
367
|
+
|
|
368
|
+
return fallback
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function writeConfig(cwd, config, options) {
|
|
372
|
+
const configPath = path.join(cwd, CONFIG_FILE)
|
|
373
|
+
if ((await exists(configPath)) && !options.force) {
|
|
374
|
+
return [`kept ${relative(cwd, configPath)}`]
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const status = await writeFile(
|
|
378
|
+
configPath,
|
|
379
|
+
`${JSON.stringify(config, null, 2)}\n`,
|
|
380
|
+
options
|
|
381
|
+
)
|
|
382
|
+
return [`${status} ${relative(cwd, configPath)}`]
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async function writeBaseFiles(cwd, config, options) {
|
|
386
|
+
return writeRegistryFiles(cwd, config, registry.base.files || [], options)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async function writeRegistryFiles(cwd, config, files, options) {
|
|
390
|
+
const writes = []
|
|
391
|
+
|
|
392
|
+
for (const file of files) {
|
|
393
|
+
const sourcePath = path.join(registryRoot, file.source)
|
|
394
|
+
const targetPath = path.join(cwd, resolveTarget(cwd, config, file.target))
|
|
395
|
+
const source = await fs.readFile(sourcePath)
|
|
396
|
+
const content = isTextRegistryFile(file.source)
|
|
397
|
+
? applyTemplate(source.toString("utf8"), config)
|
|
398
|
+
: source
|
|
399
|
+
|
|
400
|
+
const status = await writeFile(targetPath, content, options)
|
|
401
|
+
writes.push(`${status} ${relative(cwd, targetPath)}`)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return writes
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async function writeGlobalCss(cwd, config, options) {
|
|
408
|
+
const cssPath = path.join(cwd, config.tailwind.css)
|
|
409
|
+
const sourcePath = path.join(registryRoot, "styles", "globals.css")
|
|
410
|
+
const tokenCss = await fs.readFile(sourcePath, "utf8")
|
|
411
|
+
const current = await readTextIfExists(cssPath)
|
|
412
|
+
|
|
413
|
+
if (current?.includes(CSS_START) && !options.force) {
|
|
414
|
+
return [`kept ${relative(cwd, cssPath)}`]
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const nextCss = current
|
|
418
|
+
? `${removeExistingTokenBlock(current).trimEnd()}\n\n${tokenCss}\n`
|
|
419
|
+
: `${tokenCss}\n`
|
|
420
|
+
const next = ensureGlobalCssImports(nextCss)
|
|
421
|
+
|
|
422
|
+
const status = await writeFile(cssPath, next, { ...options, force: true })
|
|
423
|
+
return [`${status} ${relative(cwd, cssPath)}`]
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async function writeAliasConfig(cwd, config, options) {
|
|
427
|
+
if (!usesRootAlias(config, "@")) return []
|
|
428
|
+
|
|
429
|
+
const writes = []
|
|
430
|
+
writes.push(...(await writeTypescriptAliasConfig(cwd, "@", options)))
|
|
431
|
+
writes.push(...(await writeViteAliasConfig(cwd, "@", options)))
|
|
432
|
+
return writes
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function usesRootAlias(config, aliasRoot) {
|
|
436
|
+
return Object.values(config.aliases || {}).some((alias) =>
|
|
437
|
+
alias === aliasRoot || alias?.startsWith(`${aliasRoot}/`)
|
|
438
|
+
)
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async function writeTypescriptAliasConfig(cwd, aliasRoot, options) {
|
|
442
|
+
const tsconfigPath = await findTypescriptConfigPath(cwd)
|
|
443
|
+
if (!tsconfigPath) return []
|
|
444
|
+
|
|
445
|
+
const config = await readJsonc(tsconfigPath)
|
|
446
|
+
const compilerOptions = config.compilerOptions || {}
|
|
447
|
+
const paths = compilerOptions.paths || {}
|
|
448
|
+
const sourceRoot = shouldUseSrcDirectory(cwd) ? "./src" : "."
|
|
449
|
+
const aliasPattern = `${aliasRoot}/*`
|
|
450
|
+
const targetPattern = `${sourceRoot}/*`
|
|
451
|
+
|
|
452
|
+
if (Array.isArray(paths[aliasPattern]) && paths[aliasPattern].includes(targetPattern)) {
|
|
453
|
+
return [`kept ${relative(cwd, tsconfigPath)}`]
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
config.compilerOptions = {
|
|
457
|
+
...compilerOptions,
|
|
458
|
+
paths: {
|
|
459
|
+
...paths,
|
|
460
|
+
[aliasPattern]: [targetPattern],
|
|
461
|
+
},
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const status = await writeFile(tsconfigPath, `${JSON.stringify(config, null, 2)}\n`, {
|
|
465
|
+
dryRun: options.dryRun,
|
|
466
|
+
force: true,
|
|
467
|
+
})
|
|
468
|
+
return [`${status} ${relative(cwd, tsconfigPath)}`]
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async function findTypescriptConfigPath(cwd) {
|
|
472
|
+
const candidates = [
|
|
473
|
+
"tsconfig.app.json",
|
|
474
|
+
"tsconfig.json",
|
|
475
|
+
"jsconfig.json",
|
|
476
|
+
]
|
|
477
|
+
|
|
478
|
+
for (const candidate of candidates) {
|
|
479
|
+
const target = path.join(cwd, candidate)
|
|
480
|
+
if (await exists(target)) return target
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return null
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async function writeViteAliasConfig(cwd, aliasRoot, options) {
|
|
487
|
+
const viteConfigPath = await findViteConfigPath(cwd)
|
|
488
|
+
if (!viteConfigPath) return []
|
|
489
|
+
|
|
490
|
+
const current = await fs.readFile(viteConfigPath, "utf8")
|
|
491
|
+
const sourceRoot = shouldUseSrcDirectory(cwd) ? "./src" : "."
|
|
492
|
+
const aliasLine = `"${aliasRoot}": fileURLToPath(new URL("${sourceRoot}", import.meta.url))`
|
|
493
|
+
|
|
494
|
+
if (
|
|
495
|
+
current.includes(aliasLine) &&
|
|
496
|
+
current.includes("@tailwindcss/vite") &&
|
|
497
|
+
current.includes("tailwindcss()")
|
|
498
|
+
) {
|
|
499
|
+
return [`kept ${relative(cwd, viteConfigPath)}`]
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
let next = ensureNodeUrlImport(current)
|
|
503
|
+
next = ensureTailwindViteImport(next)
|
|
504
|
+
next = addViteResolveAlias(next, aliasRoot, sourceRoot)
|
|
505
|
+
next = addVitePluginCall(next, "tailwindcss()")
|
|
506
|
+
|
|
507
|
+
const status = await writeFile(viteConfigPath, next, {
|
|
508
|
+
dryRun: options.dryRun,
|
|
509
|
+
force: true,
|
|
510
|
+
})
|
|
511
|
+
return [`${status} ${relative(cwd, viteConfigPath)}`]
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async function findViteConfigPath(cwd) {
|
|
515
|
+
const candidates = [
|
|
516
|
+
"vite.config.ts",
|
|
517
|
+
"vite.config.mts",
|
|
518
|
+
"vite.config.js",
|
|
519
|
+
"vite.config.mjs",
|
|
520
|
+
]
|
|
521
|
+
|
|
522
|
+
for (const candidate of candidates) {
|
|
523
|
+
const target = path.join(cwd, candidate)
|
|
524
|
+
if (await exists(target)) return target
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return null
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function ensureNodeUrlImport(source) {
|
|
531
|
+
if (source.includes("fileURLToPath") && source.includes("node:url")) return source
|
|
532
|
+
|
|
533
|
+
return `import { fileURLToPath, URL } from "node:url"\n${source}`
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function ensureTailwindViteImport(source) {
|
|
537
|
+
if (source.includes("@tailwindcss/vite")) return source
|
|
538
|
+
|
|
539
|
+
return `import tailwindcss from "@tailwindcss/vite"\n${source}`
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function addViteResolveAlias(source, aliasRoot, sourceRoot) {
|
|
543
|
+
const aliasLine = `"${aliasRoot}": fileURLToPath(new URL("${sourceRoot}", import.meta.url))`
|
|
544
|
+
if (source.includes(aliasLine)) return source
|
|
545
|
+
if (!source.includes("defineConfig({")) return source
|
|
546
|
+
|
|
547
|
+
const resolveBlock = [
|
|
548
|
+
" resolve: {",
|
|
549
|
+
" alias: {",
|
|
550
|
+
` "${aliasRoot}": fileURLToPath(new URL("${sourceRoot}", import.meta.url)),`,
|
|
551
|
+
" },",
|
|
552
|
+
" },",
|
|
553
|
+
].join("\n")
|
|
554
|
+
|
|
555
|
+
return source.replace("defineConfig({", `defineConfig({\n${resolveBlock}`)
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function addVitePluginCall(source, pluginCall) {
|
|
559
|
+
if (source.includes(pluginCall)) return source
|
|
560
|
+
|
|
561
|
+
const pluginsPattern = /plugins:\s*\[([\s\S]*?)\]/
|
|
562
|
+
if (pluginsPattern.test(source)) {
|
|
563
|
+
return source.replace(pluginsPattern, (_match, plugins) => {
|
|
564
|
+
const currentPlugins = plugins.trim().replace(/,\s*$/, "")
|
|
565
|
+
const nextPlugins = currentPlugins
|
|
566
|
+
? `${currentPlugins}, ${pluginCall}`
|
|
567
|
+
: pluginCall
|
|
568
|
+
return `plugins: [${nextPlugins}]`
|
|
569
|
+
})
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (!source.includes("defineConfig({")) return source
|
|
573
|
+
|
|
574
|
+
return source.replace("defineConfig({", `defineConfig({\n plugins: [${pluginCall}],`)
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
async function getPackageChanges(cwd, dependencies) {
|
|
578
|
+
const devDependencies = { ...SETUP_DEV_DEPENDENCIES }
|
|
579
|
+
if (await findViteConfigPath(cwd)) {
|
|
580
|
+
Object.assign(devDependencies, VITE_DEV_DEPENDENCIES)
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return {
|
|
584
|
+
dependencies,
|
|
585
|
+
devDependencies,
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
async function updatePackageJson(cwd, changes, options) {
|
|
590
|
+
const packagePath = path.join(cwd, "package.json")
|
|
591
|
+
const dependencies = changes.dependencies || {}
|
|
592
|
+
const devDependencies = changes.devDependencies || {}
|
|
593
|
+
const packageJson = (await readJsonIfExists(packagePath)) || {
|
|
594
|
+
name: path.basename(cwd).toLowerCase(),
|
|
595
|
+
version: "0.0.0",
|
|
596
|
+
private: true,
|
|
597
|
+
}
|
|
598
|
+
const added = [
|
|
599
|
+
...addPackageSection(packageJson, "dependencies", dependencies),
|
|
600
|
+
...addPackageSection(packageJson, "devDependencies", devDependencies),
|
|
601
|
+
]
|
|
602
|
+
|
|
603
|
+
if (added.length === 0 && (await exists(packagePath))) {
|
|
604
|
+
return [`kept ${relative(cwd, packagePath)}`]
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const status = await writeFile(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`, {
|
|
608
|
+
dryRun: options.dryRun,
|
|
609
|
+
force: true,
|
|
610
|
+
})
|
|
611
|
+
|
|
612
|
+
return [
|
|
613
|
+
`${status} ${relative(cwd, packagePath)}`,
|
|
614
|
+
]
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function addPackageSection(packageJson, section, dependencies) {
|
|
618
|
+
const added = []
|
|
619
|
+
const existing = packageJson[section] || {}
|
|
620
|
+
|
|
621
|
+
for (const [name, version] of Object.entries(dependencies)) {
|
|
622
|
+
if (hasPackageDependency(packageJson, name)) continue
|
|
623
|
+
|
|
624
|
+
existing[name] = version
|
|
625
|
+
added.push(name)
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (added.length > 0 || packageJson[section]) {
|
|
629
|
+
packageJson[section] = sortObject(existing)
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return added
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function hasPackageDependency(packageJson, name) {
|
|
636
|
+
return Boolean(
|
|
637
|
+
packageJson.dependencies?.[name] ||
|
|
638
|
+
packageJson.devDependencies?.[name] ||
|
|
639
|
+
packageJson.peerDependencies?.[name] ||
|
|
640
|
+
packageJson.optionalDependencies?.[name]
|
|
641
|
+
)
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function applyTemplate(source, config) {
|
|
645
|
+
return source
|
|
646
|
+
.replaceAll("@/lib/utils", config.aliases.utils)
|
|
647
|
+
.replaceAll("@/hooks", config.aliases.hooks)
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function isTextRegistryFile(source) {
|
|
651
|
+
return !/\.(avif|eot|gif|ico|jpe?g|otf|png|ttf|webp|woff2?)$/i.test(source)
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function resolveTarget(cwd, config, target) {
|
|
655
|
+
const replacements = {
|
|
656
|
+
"{{ui}}": aliasToPath(cwd, config.aliases.ui),
|
|
657
|
+
"{{utils}}": stripExtension(aliasToPath(cwd, config.aliases.utils)),
|
|
658
|
+
"{{hooks}}": aliasToPath(cwd, config.aliases.hooks),
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
let resolved = target
|
|
662
|
+
for (const [placeholder, value] of Object.entries(replacements)) {
|
|
663
|
+
resolved = resolved.replaceAll(placeholder, value)
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
return resolved
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function aliasToPath(cwd, alias) {
|
|
670
|
+
if (!alias) return ""
|
|
671
|
+
if (path.isAbsolute(alias)) return alias
|
|
672
|
+
if (alias.startsWith("./")) return alias.slice(2)
|
|
673
|
+
if (alias.startsWith("../")) return alias
|
|
674
|
+
|
|
675
|
+
if (alias.startsWith("@/") || alias.startsWith("~/")) {
|
|
676
|
+
const rest = alias.slice(2)
|
|
677
|
+
return path.join(shouldUseSrcDirectory(cwd) ? "src" : "", rest)
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
return alias
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function shouldUseSrcDirectory(cwd) {
|
|
684
|
+
return pathExistsSyncHint(cwd, "src/app") || pathExistsSyncHint(cwd, "src")
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function pathExistsSyncHint(cwd, segment) {
|
|
688
|
+
return Boolean(syncExistsCache.get(path.join(cwd, segment)))
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
async function warmPathHints(cwd) {
|
|
692
|
+
for (const segment of ["src", "src/app"]) {
|
|
693
|
+
const target = path.join(cwd, segment)
|
|
694
|
+
syncExistsCache.set(target, await exists(target))
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
async function writeFile(targetPath, content, options) {
|
|
699
|
+
const alreadyExists = await exists(targetPath)
|
|
700
|
+
const isBinary = Buffer.isBuffer(content)
|
|
701
|
+
|
|
702
|
+
if (options.dryRun) {
|
|
703
|
+
if (!alreadyExists) return "would write"
|
|
704
|
+
|
|
705
|
+
const current = await fs.readFile(targetPath)
|
|
706
|
+
const isEqual = isBinary
|
|
707
|
+
? current.equals(content)
|
|
708
|
+
: current.toString("utf8") === content
|
|
709
|
+
if (isEqual) return "would keep"
|
|
710
|
+
|
|
711
|
+
return "would update"
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
if (alreadyExists && !options.force) {
|
|
715
|
+
const current = await fs.readFile(targetPath)
|
|
716
|
+
const isEqual = isBinary
|
|
717
|
+
? current.equals(content)
|
|
718
|
+
: current.toString("utf8") === content
|
|
719
|
+
if (isEqual) return "kept"
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true })
|
|
723
|
+
await fs.writeFile(targetPath, content, isBinary ? undefined : "utf8")
|
|
724
|
+
return alreadyExists ? "updated" : "wrote"
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
async function readJson(filePath) {
|
|
728
|
+
return JSON.parse(await fs.readFile(filePath, "utf8"))
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
async function readJsonc(filePath) {
|
|
732
|
+
const content = await fs.readFile(filePath, "utf8")
|
|
733
|
+
return JSON.parse(stripJsonComments(content))
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function stripJsonComments(content) {
|
|
737
|
+
return content
|
|
738
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
739
|
+
.replace(/(^|[^:])\/\/.*$/gm, "$1")
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
async function readJsonIfExists(filePath) {
|
|
743
|
+
try {
|
|
744
|
+
return await readJson(filePath)
|
|
745
|
+
} catch (error) {
|
|
746
|
+
if (error.code === "ENOENT") return null
|
|
747
|
+
throw error
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
async function readTextIfExists(filePath) {
|
|
752
|
+
try {
|
|
753
|
+
return await fs.readFile(filePath, "utf8")
|
|
754
|
+
} catch (error) {
|
|
755
|
+
if (error.code === "ENOENT") return null
|
|
756
|
+
throw error
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
async function exists(filePath) {
|
|
761
|
+
try {
|
|
762
|
+
await fs.access(filePath)
|
|
763
|
+
return true
|
|
764
|
+
} catch {
|
|
765
|
+
return false
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function removeExistingTokenBlock(content) {
|
|
770
|
+
const start = content.indexOf(CSS_START)
|
|
771
|
+
if (start === -1) return content
|
|
772
|
+
|
|
773
|
+
const end = content.indexOf(CSS_END, start)
|
|
774
|
+
if (end === -1) return content
|
|
775
|
+
|
|
776
|
+
return `${content.slice(0, start)}${content.slice(end + CSS_END.length)}`
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function ensureGlobalCssImports(content) {
|
|
780
|
+
const fontImportPattern =
|
|
781
|
+
/^\s*@import\s+["']@fontsource(?:-variable)?\/inter(?:\/(?:400|500|600|700)\.css)?["'];\s*/gm
|
|
782
|
+
const tailwindImportPattern = /^\s*@import\s+["']tailwindcss["'];\s*/gm
|
|
783
|
+
const cleaned = content
|
|
784
|
+
.replace(fontImportPattern, "")
|
|
785
|
+
.replace(tailwindImportPattern, "")
|
|
786
|
+
.trimStart()
|
|
787
|
+
return `${TAILWIND_IMPORT}\n${FONT_IMPORTS.join("\n")}\n${cleaned}`
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function appendAliasSegment(alias, segment) {
|
|
791
|
+
if (!alias) return null
|
|
792
|
+
return `${alias.replace(/\/$/, "")}/${segment}`
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function stripExtension(filePath) {
|
|
796
|
+
return filePath.replace(/\.(t|j)sx?$/, "")
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function sortObject(value) {
|
|
800
|
+
return Object.fromEntries(
|
|
801
|
+
Object.entries(value).sort(([left], [right]) => left.localeCompare(right))
|
|
802
|
+
)
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function relative(cwd, targetPath) {
|
|
806
|
+
return path.relative(cwd, targetPath).replaceAll("\\", "/")
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function printResult(title, writes, dryRun) {
|
|
810
|
+
console.log(`${dryRun ? "Dry run: " : ""}${title}`)
|
|
811
|
+
for (const write of writes) {
|
|
812
|
+
console.log(`- ${write}`)
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function printInstallHint(writes, dryRun) {
|
|
817
|
+
if (dryRun) return
|
|
818
|
+
|
|
819
|
+
const packageJsonChanged = writes.some((write) =>
|
|
820
|
+
/^(wrote|updated) package\.json$/.test(write)
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
if (packageJsonChanged) {
|
|
824
|
+
console.log("\nNext: run npm install to install the added dependencies.")
|
|
825
|
+
}
|
|
826
|
+
}
|