banhaten 0.1.2 → 0.2.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/CHANGELOG.md +45 -0
- package/CONTRIBUTING.md +44 -0
- package/README.md +145 -328
- package/SECURITY.md +15 -0
- package/banhaten.config.example.json +1 -3
- package/docs/design-system/README.md +20 -0
- package/docs/design-system/adoption-quality-remediation.md +87 -0
- package/docs/design-system/agent-usage.md +13 -0
- package/docs/design-system/appearance-presets.md +189 -0
- package/docs/design-system/appearances/default.md +94 -0
- package/docs/design-system/appearances/rounded.md +95 -0
- package/docs/design-system/appearances/sharp.md +95 -0
- package/docs/design-system/component-docs-pattern.md +81 -0
- package/docs/design-system/component-showcase-consistency-report.md +217 -0
- package/docs/design-system/component-token-consistency-audit.md +173 -0
- package/docs/design-system/components/README.md +75 -0
- package/docs/design-system/components/accordion.md +67 -0
- package/docs/design-system/components/activity-feed.md +106 -0
- package/docs/design-system/components/alert-dialog.md +87 -0
- package/docs/design-system/components/alert.md +111 -0
- package/docs/design-system/components/aspect-ratio.md +59 -0
- package/docs/design-system/components/attribute.md +103 -0
- package/docs/design-system/components/autocomplete.md +93 -0
- package/docs/design-system/components/avatar.md +70 -0
- package/docs/design-system/components/badge.md +72 -0
- package/docs/design-system/components/banner.md +99 -0
- package/docs/design-system/components/breadcrumbs.md +188 -0
- package/docs/design-system/components/button-group.md +117 -0
- package/docs/design-system/components/button.md +95 -0
- package/docs/design-system/components/card.md +95 -0
- package/docs/design-system/components/carousel.md +61 -0
- package/docs/design-system/components/catalog-components.md +59 -0
- package/docs/design-system/components/chart.md +58 -0
- package/docs/design-system/components/checkbox.md +68 -0
- package/docs/design-system/components/collapsible.md +63 -0
- package/docs/design-system/components/command.md +75 -0
- package/docs/design-system/components/context-menu.md +59 -0
- package/docs/design-system/components/date-picker.md +97 -0
- package/docs/design-system/components/divider.md +117 -0
- package/docs/design-system/components/empty-state.md +72 -0
- package/docs/design-system/components/field.md +86 -0
- package/docs/design-system/components/file-upload.md +199 -0
- package/docs/design-system/components/hover-card.md +61 -0
- package/docs/design-system/components/icons.md +48 -0
- package/docs/design-system/components/input-group.md +73 -0
- package/docs/design-system/components/input-otp.md +73 -0
- package/docs/design-system/components/input.md +84 -0
- package/docs/design-system/components/item.md +72 -0
- package/docs/design-system/components/kbd.md +62 -0
- package/docs/design-system/components/label.md +64 -0
- package/docs/design-system/components/menu.md +76 -0
- package/docs/design-system/components/menubar.md +60 -0
- package/docs/design-system/components/modal.md +119 -0
- package/docs/design-system/components/native-select.md +70 -0
- package/docs/design-system/components/navigation-menu.md +63 -0
- package/docs/design-system/components/onboarding-step-list-item.md +94 -0
- package/docs/design-system/components/page-header.md +98 -0
- package/docs/design-system/components/pagination.md +71 -0
- package/docs/design-system/components/popover.md +77 -0
- package/docs/design-system/components/progress-slider.md +66 -0
- package/docs/design-system/components/progress.md +92 -0
- package/docs/design-system/components/radio-card.md +65 -0
- package/docs/design-system/components/radio-group.md +72 -0
- package/docs/design-system/components/resizable.md +57 -0
- package/docs/design-system/components/scroll-area.md +60 -0
- package/docs/design-system/components/segmented-control.md +71 -0
- package/docs/design-system/components/select.md +87 -0
- package/docs/design-system/components/sheet.md +82 -0
- package/docs/design-system/components/sidebar.md +83 -0
- package/docs/design-system/components/skeleton.md +91 -0
- package/docs/design-system/components/slideout.md +77 -0
- package/docs/design-system/components/slider.md +81 -0
- package/docs/design-system/components/social-button.md +65 -0
- package/docs/design-system/components/spinner.md +77 -0
- package/docs/design-system/components/steps.md +77 -0
- package/docs/design-system/components/table.md +411 -0
- package/docs/design-system/components/tabs.md +69 -0
- package/docs/design-system/components/tag.md +95 -0
- package/docs/design-system/components/textarea.md +64 -0
- package/docs/design-system/components/timeline.md +95 -0
- package/docs/design-system/components/toast.md +73 -0
- package/docs/design-system/components/toggle.md +96 -0
- package/docs/design-system/components/toolbar.md +105 -0
- package/docs/design-system/components/tooltip.md +107 -0
- package/docs/design-system/components/typography.md +18 -0
- package/docs/design-system/density-and-elevation.md +44 -0
- package/docs/design-system/design-system-test-missing-items.md +368 -0
- package/docs/design-system/direction-and-localization.md +23 -0
- package/docs/design-system/form-recipes.md +73 -0
- package/docs/design-system/gap-promotion-policy.md +21 -0
- package/docs/design-system/gap-promotion-report.md +166 -0
- package/docs/design-system/icons.md +69 -0
- package/docs/design-system/installation.md +68 -0
- package/docs/design-system/launch-comparison-shadcn.md +98 -0
- package/docs/design-system/registry-and-cli.md +64 -0
- package/docs/design-system/release-policy.md +30 -0
- package/docs/design-system/roadmap.md +14 -0
- package/docs/design-system/support-matrix.md +23 -0
- package/docs/design-system/tabs.md +53 -0
- package/docs/design-system/token-governance.md +60 -0
- package/package.json +97 -65
- package/registry/component-contract.json +40 -0
- package/registry/components/alert-dialog.tsx +297 -0
- package/registry/components/alert.tsx +86 -65
- package/registry/components/aspect-ratio.tsx +30 -0
- package/registry/components/autocomplete.tsx +23 -9
- package/registry/components/avatar.tsx +7 -7
- package/registry/components/badge.tsx +1 -1
- package/registry/components/button-group.tsx +321 -4
- package/registry/components/button.tsx +38 -17
- package/registry/components/card.tsx +68 -59
- package/registry/components/carousel.tsx +282 -0
- package/registry/components/chart.tsx +203 -0
- package/registry/components/checkbox.tsx +3 -3
- package/registry/components/collapsible.tsx +69 -0
- package/registry/components/command.tsx +174 -0
- package/registry/components/context-menu.tsx +236 -0
- package/registry/components/date-picker.tsx +246 -105
- package/registry/components/expanded/ActivityFeed.tsx +6 -15
- package/registry/components/expanded/Banner.tsx +3 -3
- package/registry/components/expanded/Breadcrumbs.tsx +5 -5
- package/registry/components/expanded/CatalogComponentsShowcase.tsx +13 -13
- package/registry/components/expanded/CatalogDivider.tsx +1 -1
- package/registry/components/expanded/EmptyState.tsx +1 -1
- package/registry/components/expanded/FileUpload.tsx +9 -2
- package/registry/components/expanded/OnboardingStepListItem.tsx +2 -4
- package/registry/components/expanded/PageHeader.tsx +14 -14
- package/registry/components/expanded/Slideout.tsx +15 -415
- package/registry/components/expanded/Steps.tsx +4 -4
- package/registry/components/expanded/Table.tsx +116 -69
- package/registry/components/expanded/Timeline.tsx +3 -3
- package/registry/components/expanded/activityFeed.css +1 -1
- package/registry/components/expanded/breadcrumbs.css +139 -139
- package/registry/components/expanded/catalogComponentsShowcase.css +83 -83
- package/registry/components/expanded/divider.css +1 -1
- package/registry/components/expanded/emptyState.css +1 -1
- package/registry/components/expanded/fileUpload.css +6 -6
- package/registry/components/expanded/slideout.css +9 -685
- package/registry/components/expanded/steps.css +274 -274
- package/registry/components/expanded/table.css +2 -2
- package/registry/components/expanded/timeline.css +264 -264
- package/registry/components/field.tsx +230 -0
- package/registry/components/hover-card.tsx +48 -0
- package/registry/components/input-content.ts +1 -0
- package/registry/components/input-group.tsx +130 -0
- package/registry/components/input-otp.tsx +13 -3
- package/registry/components/input.tsx +70 -45
- package/registry/components/item.tsx +189 -0
- package/registry/components/kbd.tsx +151 -0
- package/registry/components/label.tsx +78 -0
- package/registry/components/menu.tsx +34 -246
- package/registry/components/menubar.tsx +226 -0
- package/registry/components/modal.tsx +216 -143
- package/registry/components/native-select.tsx +205 -0
- package/registry/components/navigation-menu.tsx +171 -0
- package/registry/components/pagination.tsx +80 -58
- package/registry/components/popover.tsx +162 -664
- package/registry/components/radio-card.tsx +116 -87
- package/registry/components/radio-group.tsx +1 -1
- package/registry/components/resizable.tsx +74 -0
- package/registry/components/scroll-area.tsx +67 -0
- package/registry/components/segmented-control.tsx +87 -0
- package/registry/components/select-content.tsx +2 -2
- package/registry/components/select.tsx +54 -20
- package/registry/components/sheet.tsx +305 -0
- package/registry/components/sidebar.tsx +352 -0
- package/registry/components/skeleton.css +0 -3
- package/registry/components/social-button.tsx +116 -30
- package/registry/components/spinner.tsx +18 -55
- package/registry/components/{expanded/tabs.css → tabs.css} +127 -106
- package/registry/components/tabs.tsx +246 -0
- package/registry/components/tag.tsx +2 -2
- package/registry/components/textarea.tsx +4 -4
- package/registry/components/toast.tsx +136 -0
- package/registry/components/toggle.tsx +2 -2
- package/registry/components/toolbar.tsx +89 -17
- package/registry/components/tooltip.tsx +1 -1
- package/registry/examples/alert-demo.tsx +25 -5
- package/registry/examples/alert-dialog-demo.tsx +42 -0
- package/registry/examples/aspect-ratio-demo.tsx +11 -0
- package/registry/examples/attribute-demo.tsx +1 -1
- package/registry/examples/autocomplete-demo.tsx +3 -3
- package/registry/examples/avatar-demo.tsx +7 -7
- package/registry/examples/badge-demo.tsx +1 -1
- package/registry/examples/button-demo.tsx +6 -6
- package/registry/examples/button-group-demo.tsx +23 -2
- package/registry/examples/card-demo.tsx +46 -8
- package/registry/examples/carousel-demo.tsx +43 -0
- package/registry/examples/chart-demo.tsx +33 -0
- package/registry/examples/checkbox-demo.tsx +5 -5
- package/registry/examples/collapsible-demo.tsx +16 -0
- package/registry/examples/collection-filter-bar-demo.tsx +135 -0
- package/registry/examples/command-demo.tsx +42 -0
- package/registry/examples/{expanded/command-bar-demo.tsx → command-palette-demo.tsx} +5 -15
- package/registry/examples/context-menu-demo.tsx +29 -0
- package/registry/examples/control-density-contract-demo.tsx +61 -0
- package/registry/examples/date-picker-demo.tsx +12 -12
- package/registry/examples/divider-demo.tsx +3 -3
- package/registry/examples/expanded/banner-demo.tsx +1 -1
- package/registry/examples/expanded/breadcrumbs-demo.tsx +1 -1
- package/registry/examples/expanded/empty-state-demo.tsx +1 -1
- package/registry/examples/expanded/file-upload-demo.tsx +1 -1
- package/registry/examples/expanded/onboarding-step-demo.tsx +1 -1
- package/registry/examples/expanded/slideout-demo.tsx +2 -2
- package/registry/examples/expanded/steps-demo.tsx +1 -1
- package/registry/examples/expanded/tabs-demo.tsx +2 -2
- package/registry/examples/expanded/timeline-demo.tsx +1 -1
- package/registry/examples/field-demo.tsx +51 -0
- package/registry/examples/hover-card-demo.tsx +23 -0
- package/registry/examples/input-demo.tsx +6 -6
- package/registry/examples/input-group-demo.tsx +16 -0
- package/registry/examples/input-otp-demo.tsx +4 -4
- package/registry/examples/item-demo.tsx +46 -0
- package/registry/examples/kbd-demo.tsx +11 -0
- package/registry/examples/label-demo.tsx +20 -0
- package/registry/examples/menu-demo.tsx +1 -1
- package/registry/examples/menubar-demo.tsx +34 -0
- package/registry/examples/modal-demo.tsx +67 -9
- package/registry/examples/native-select-demo.tsx +16 -0
- package/registry/examples/navigation-menu-demo.tsx +29 -0
- package/registry/examples/pagination-demo.tsx +6 -3
- package/registry/examples/popover-demo.tsx +19 -19
- package/registry/examples/progress-demo.tsx +1 -1
- package/registry/examples/progress-slider-demo.tsx +1 -1
- package/registry/examples/radio-card-demo.tsx +4 -4
- package/registry/examples/radio-group-demo.tsx +2 -2
- package/registry/examples/resizable-demo.tsx +22 -0
- package/registry/examples/results-toolbar-demo.tsx +44 -0
- package/registry/examples/scroll-area-demo.tsx +15 -0
- package/registry/examples/segmented-control-demo.tsx +39 -0
- package/registry/examples/select-demo.tsx +1 -1
- package/registry/examples/sheet-demo.tsx +47 -0
- package/registry/examples/sidebar-demo.tsx +55 -0
- package/registry/examples/slider-demo.tsx +3 -3
- package/registry/examples/social-button-demo.tsx +18 -8
- package/registry/examples/spinner-demo.tsx +13 -13
- package/registry/examples/tabs-demo.tsx +13 -0
- package/registry/examples/tag-demo.tsx +1 -1
- package/registry/examples/textarea-demo.tsx +4 -4
- package/registry/examples/toast-demo.tsx +35 -0
- package/registry/examples/toggle-demo.tsx +6 -6
- package/registry/examples/tooltip-demo.tsx +4 -4
- package/registry/guidance/USAGE.md +27 -0
- package/registry/hooks/use-direction.ts +193 -13
- package/registry/index.json +6010 -432
- package/registry/styles/globals.css +3112 -4690
- package/registry.json +1582 -0
- package/schema/config.schema.json +51 -0
- package/schema/registry.schema.json +190 -0
- package/schema/tokens.schema.json +163 -0
- package/src/cli/command-line.js +194 -0
- package/src/cli/config-editors.js +9 -0
- package/src/cli/doctor.js +399 -0
- package/src/cli/file-transaction.js +295 -0
- package/src/cli/global-css.js +153 -0
- package/src/cli/index.js +639 -1391
- package/src/cli/install-plan.js +592 -0
- package/src/cli/install-receipt.js +171 -0
- package/src/cli/jsonc.js +203 -0
- package/src/cli/package-install-state.js +254 -0
- package/src/cli/package-manager.js +238 -0
- package/src/cli/package-plan.js +185 -0
- package/src/cli/package-version.js +97 -0
- package/src/cli/registry-query.js +207 -0
- package/src/cli/reporting.js +55 -0
- package/src/cli/schema-validation.js +19 -0
- package/src/cli/source-scanner.js +686 -0
- package/src/cli/vite-config.js +478 -0
- package/tokens/banhaten.tokens.json +4313 -310
- 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 +0 -744
- 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/expanded/BannerBoard.tsx +0 -225
- package/registry/components/expanded/CatalogTag.tsx +0 -85
- package/registry/components/expanded/CommandBar.tsx +0 -386
- package/registry/components/expanded/IconExplorer.tsx +0 -612
- package/registry/components/expanded/Tabs.tsx +0 -86
- package/registry/components/expanded/TimelineShowcase.tsx +0 -158
- package/registry/components/expanded/commandBar.css +0 -470
- package/registry/components/expanded/iconExplorer.css +0 -764
- package/registry/components/expanded/iconPacks.ts +0 -866
- package/registry/components/expanded/tag.css +0 -148
- package/registry/examples/expanded/icons-demo.tsx +0 -5
- 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/src/cli/index.js
CHANGED
|
@@ -1,9 +1,54 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { createHash } from "node:crypto"
|
|
3
4
|
import fs from "node:fs/promises"
|
|
4
5
|
import path from "node:path"
|
|
5
6
|
import process from "node:process"
|
|
6
7
|
import { fileURLToPath } from "node:url"
|
|
8
|
+
import { parseCommandLine } from "./command-line.js"
|
|
9
|
+
import {
|
|
10
|
+
inspectBanhatenProject,
|
|
11
|
+
printBanhatenDoctor,
|
|
12
|
+
} from "./doctor.js"
|
|
13
|
+
import {
|
|
14
|
+
assertFileUnchanged,
|
|
15
|
+
deferUntilApplied,
|
|
16
|
+
formatUnifiedDiff,
|
|
17
|
+
runFileTransaction,
|
|
18
|
+
writeFile,
|
|
19
|
+
} from "./file-transaction.js"
|
|
20
|
+
import {
|
|
21
|
+
applyPackageDependencyPlan,
|
|
22
|
+
createPackageDependencyPlan,
|
|
23
|
+
mergeDependencyRequirements,
|
|
24
|
+
} from "./package-plan.js"
|
|
25
|
+
import { inspectPackageInstallState } from "./package-install-state.js"
|
|
26
|
+
import {
|
|
27
|
+
createInstallPlan,
|
|
28
|
+
resolveInstallTarget,
|
|
29
|
+
} from "./install-plan.js"
|
|
30
|
+
import {
|
|
31
|
+
createInstallPlanReceipt,
|
|
32
|
+
normalizeReceiptComponents,
|
|
33
|
+
normalizeReceiptTarget,
|
|
34
|
+
validateReceiptDestinations,
|
|
35
|
+
} from "./install-receipt.js"
|
|
36
|
+
import {
|
|
37
|
+
ensureGlobalCssImports,
|
|
38
|
+
removeExistingTokenBlock,
|
|
39
|
+
} from "./global-css.js"
|
|
40
|
+
import {
|
|
41
|
+
printInstallHint,
|
|
42
|
+
printResult,
|
|
43
|
+
resolveInstallAction,
|
|
44
|
+
} from "./reporting.js"
|
|
45
|
+
import { assertSchema } from "./schema-validation.js"
|
|
46
|
+
import {
|
|
47
|
+
listComponents,
|
|
48
|
+
searchComponents,
|
|
49
|
+
showComponentDocs,
|
|
50
|
+
viewComponent,
|
|
51
|
+
} from "./registry-query.js"
|
|
7
52
|
|
|
8
53
|
const packageRoot = path.resolve(
|
|
9
54
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
@@ -12,29 +57,24 @@ const packageRoot = path.resolve(
|
|
|
12
57
|
)
|
|
13
58
|
const registryRoot = path.join(packageRoot, "registry")
|
|
14
59
|
const registryIndexPath = path.join(registryRoot, "index.json")
|
|
60
|
+
const packageJsonPath = path.join(packageRoot, "package.json")
|
|
61
|
+
const schemaRoot = path.join(packageRoot, "schema")
|
|
15
62
|
|
|
16
63
|
const CONFIG_FILE = "banhaten.config.json"
|
|
17
|
-
const
|
|
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";'
|
|
64
|
+
const INSTALL_RECEIPT_FILE = ".banhaten/installed.json"
|
|
24
65
|
const SETUP_DEV_DEPENDENCIES = {
|
|
25
66
|
tailwindcss: "^4.0.0",
|
|
26
67
|
}
|
|
27
68
|
const VITE_DEV_DEPENDENCIES = {
|
|
28
69
|
"@tailwindcss/vite": "^4.0.0",
|
|
29
70
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
71
|
+
let runtimeOptions = {
|
|
72
|
+
json: false,
|
|
73
|
+
silent: false,
|
|
74
|
+
}
|
|
33
75
|
|
|
34
76
|
const DEFAULT_CONFIG = {
|
|
35
|
-
$schema: "
|
|
36
|
-
style: "default",
|
|
37
|
-
tsx: true,
|
|
77
|
+
$schema: ".banhaten/schema/config.schema.json",
|
|
38
78
|
tailwind: {
|
|
39
79
|
css: "app/globals.css",
|
|
40
80
|
},
|
|
@@ -45,7 +85,28 @@ const DEFAULT_CONFIG = {
|
|
|
45
85
|
},
|
|
46
86
|
}
|
|
47
87
|
|
|
88
|
+
// Top-level keys from the shadcn-shaped config this CLI grew out of. They are
|
|
89
|
+
// deliberately migrated away rather than rejected, so an existing project can
|
|
90
|
+
// upgrade in place. Every *other* unknown key is a real error: without this
|
|
91
|
+
// list, dropping unknown keys wholesale is indistinguishable from silently
|
|
92
|
+
// destroying a setting the user meant to keep.
|
|
93
|
+
const LEGACY_CONFIG_KEYS = Object.freeze(["style", "tsx", "rsc", "iconLibrary"])
|
|
94
|
+
|
|
95
|
+
function stripLegacyConfigKeys(config) {
|
|
96
|
+
const next = { ...config }
|
|
97
|
+
for (const key of LEGACY_CONFIG_KEYS) delete next[key]
|
|
98
|
+
return next
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function findLegacyConfigKeys(config) {
|
|
102
|
+
return LEGACY_CONFIG_KEYS.filter((key) => key in config)
|
|
103
|
+
}
|
|
104
|
+
|
|
48
105
|
const registry = await readJson(registryIndexPath)
|
|
106
|
+
const packageManifest = await readJson(packageJsonPath)
|
|
107
|
+
const registrySchema = await readJson(path.join(schemaRoot, "registry.schema.json"))
|
|
108
|
+
const configSchema = await readJson(path.join(schemaRoot, "config.schema.json"))
|
|
109
|
+
assertSchema(registry, registrySchema, "registry/index.json")
|
|
49
110
|
|
|
50
111
|
main().catch((error) => {
|
|
51
112
|
console.error(`banhaten: ${error.message}`)
|
|
@@ -53,132 +114,165 @@ main().catch((error) => {
|
|
|
53
114
|
})
|
|
54
115
|
|
|
55
116
|
async function main() {
|
|
56
|
-
const { command, positionals, flags } =
|
|
117
|
+
const { command, positionals, flags } = parseCommandLine(process.argv.slice(2))
|
|
118
|
+
runtimeOptions = {
|
|
119
|
+
json: Boolean(flags.json),
|
|
120
|
+
silent: Boolean(flags.silent),
|
|
121
|
+
}
|
|
57
122
|
|
|
58
|
-
if (
|
|
123
|
+
if (command === "version" || flags.version) {
|
|
124
|
+
console.log(packageManifest.version)
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!command || command === "help" || flags.help) {
|
|
59
129
|
printHelp()
|
|
60
130
|
return
|
|
61
131
|
}
|
|
62
132
|
|
|
63
133
|
if (command === "list") {
|
|
64
|
-
listComponents()
|
|
134
|
+
listComponents(registry, runtimeOptions)
|
|
65
135
|
return
|
|
66
136
|
}
|
|
67
137
|
|
|
68
|
-
if (command === "
|
|
69
|
-
|
|
138
|
+
if (command === "search") {
|
|
139
|
+
searchComponents(registry, positionals, runtimeOptions)
|
|
70
140
|
return
|
|
71
141
|
}
|
|
72
142
|
|
|
73
|
-
if (command === "
|
|
74
|
-
await
|
|
143
|
+
if (command === "docs") {
|
|
144
|
+
await showComponentDocs(registry, packageRoot, positionals, runtimeOptions)
|
|
75
145
|
return
|
|
76
146
|
}
|
|
77
147
|
|
|
78
|
-
if (command === "
|
|
79
|
-
await
|
|
148
|
+
if (command === "view") {
|
|
149
|
+
await viewComponent(
|
|
150
|
+
registry,
|
|
151
|
+
registryRoot,
|
|
152
|
+
positionals,
|
|
153
|
+
flags,
|
|
154
|
+
runtimeOptions
|
|
155
|
+
)
|
|
80
156
|
return
|
|
81
157
|
}
|
|
82
158
|
|
|
83
|
-
if (command === "
|
|
84
|
-
await
|
|
159
|
+
if (command === "doctor") {
|
|
160
|
+
const report = await inspectBanhatenProject({
|
|
161
|
+
cwd: path.resolve(flags.cwd || process.cwd()),
|
|
162
|
+
registryVersion: packageManifest.version,
|
|
163
|
+
})
|
|
164
|
+
printBanhatenDoctor(report, runtimeOptions)
|
|
165
|
+
if (report.summary.errors > 0) process.exitCode = 1
|
|
85
166
|
return
|
|
86
167
|
}
|
|
87
168
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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()
|
|
169
|
+
if (command === "init") {
|
|
170
|
+
await runFileTransaction(() => initProject(flags))
|
|
171
|
+
return
|
|
172
|
+
}
|
|
110
173
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
174
|
+
if (command === "add") {
|
|
175
|
+
await runFileTransaction(() => addComponents(positionals, flags))
|
|
176
|
+
return
|
|
177
|
+
}
|
|
115
178
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
continue
|
|
121
|
-
}
|
|
179
|
+
if (command === "diff") {
|
|
180
|
+
await runFileTransaction(() => diffComponents(positionals, flags))
|
|
181
|
+
return
|
|
182
|
+
}
|
|
122
183
|
|
|
123
|
-
|
|
184
|
+
if (command === "update") {
|
|
185
|
+
await runFileTransaction(() => updateComponents(positionals, flags))
|
|
186
|
+
return
|
|
124
187
|
}
|
|
125
188
|
|
|
126
|
-
|
|
127
|
-
return { command, positionals: rest, flags }
|
|
189
|
+
throw new Error(`unknown command "${command}"`)
|
|
128
190
|
}
|
|
129
191
|
|
|
130
192
|
function printHelp() {
|
|
131
193
|
console.log(`Banhaten design system
|
|
132
194
|
|
|
133
195
|
Usage:
|
|
134
|
-
banhaten init [--cwd <path>] [--force] [--dry-run]
|
|
135
|
-
banhaten add <component...> [--cwd <path>] [--force] [--dry-run]
|
|
136
|
-
banhaten diff [component...] [--cwd <path>] [--
|
|
137
|
-
banhaten update [component...] [--cwd <path>] [--force] [--dry-run]
|
|
138
|
-
banhaten list
|
|
196
|
+
banhaten init [--cwd <path>] [--force] [--dry-run] [--json]
|
|
197
|
+
banhaten add <component...> [--cwd <path>] [--force] [--dry-run] [--json]
|
|
198
|
+
banhaten diff [component...] [--cwd <path>] [--json]
|
|
199
|
+
banhaten update [component...] [--cwd <path>] [--force] [--dry-run] [--json]
|
|
200
|
+
banhaten list [--json]
|
|
201
|
+
banhaten search <query> [--json]
|
|
202
|
+
banhaten docs <component> [--json]
|
|
203
|
+
banhaten view <component> [--file <registry-source>] [--json]
|
|
204
|
+
banhaten doctor [--cwd <path>] [--json]
|
|
205
|
+
banhaten version
|
|
139
206
|
|
|
140
207
|
Examples:
|
|
141
208
|
npx banhaten init
|
|
142
209
|
npx banhaten add button
|
|
143
210
|
npx banhaten diff
|
|
144
211
|
npx banhaten update button
|
|
212
|
+
npx banhaten search input
|
|
213
|
+
npx banhaten docs button
|
|
214
|
+
npx banhaten view button
|
|
215
|
+
npx banhaten doctor
|
|
216
|
+
|
|
217
|
+
Command flags:
|
|
218
|
+
--cwd <path> Run project-aware commands against a project directory.
|
|
219
|
+
--dry-run Preview init, add, or update writes.
|
|
220
|
+
--force Replace locally modified generated files after review.
|
|
221
|
+
--json Print machine-readable output where shown above.
|
|
222
|
+
|
|
223
|
+
Global flags:
|
|
224
|
+
--help, -h Print help.
|
|
225
|
+
--silent, -s Suppress normal command output.
|
|
226
|
+
--yes, -y Accepted for non-interactive scripts.
|
|
227
|
+
--version, -v Print the installed CLI version.
|
|
145
228
|
`)
|
|
146
229
|
}
|
|
147
230
|
|
|
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
231
|
async function initProject(flags) {
|
|
159
|
-
const cwd = path.resolve(
|
|
232
|
+
const cwd = path.resolve(flags.cwd || process.cwd())
|
|
160
233
|
const dryRun = Boolean(flags["dry-run"])
|
|
161
234
|
const force = Boolean(flags.force)
|
|
162
|
-
await warmPathHints(cwd)
|
|
163
235
|
const config = await loadConfig(cwd)
|
|
236
|
+
const installPlan = await createInstallPlan(cwd, config)
|
|
237
|
+
const receipt = await readInstallReceipt(cwd)
|
|
238
|
+
validateReceiptDestinations(installPlan, receipt, registry)
|
|
239
|
+
const managedOptions = { dryRun, force, receipt }
|
|
164
240
|
const writes = []
|
|
165
241
|
|
|
166
242
|
writes.push(
|
|
167
243
|
...(await writeConfig(cwd, config, { dryRun, force })),
|
|
168
|
-
...(await
|
|
169
|
-
...(await
|
|
170
|
-
...(await
|
|
244
|
+
...(await writeProjectSchemaFiles(cwd, { dryRun, force })),
|
|
245
|
+
...(await writeBaseFiles(installPlan, managedOptions)),
|
|
246
|
+
...(await writeGlobalCss(installPlan, managedOptions)),
|
|
247
|
+
...(await writeAliasConfig(installPlan, { dryRun, force }))
|
|
171
248
|
)
|
|
172
249
|
|
|
173
|
-
const
|
|
250
|
+
const packageResult = await updatePackageJson(
|
|
174
251
|
cwd,
|
|
175
|
-
|
|
252
|
+
getPackageChanges(installPlan, registry.base.dependencies || {}),
|
|
176
253
|
{ dryRun }
|
|
177
254
|
)
|
|
178
|
-
writes.push(...
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
255
|
+
writes.push(...packageResult.writes)
|
|
256
|
+
writes.push(...(await writeInstallReceipt(installPlan, [], { dryRun })))
|
|
257
|
+
const installResolution = await resolveInstallAction(
|
|
258
|
+
cwd,
|
|
259
|
+
packageResult.installRequired
|
|
260
|
+
)
|
|
261
|
+
for (const evidence of installResolution.evidence) {
|
|
262
|
+
await assertFileUnchanged(evidence.path, evidence.source)
|
|
263
|
+
}
|
|
264
|
+
const { install } = installResolution
|
|
265
|
+
|
|
266
|
+
deferUntilApplied(() => {
|
|
267
|
+
printResult(
|
|
268
|
+
"Initialized Banhaten design system",
|
|
269
|
+
writes,
|
|
270
|
+
dryRun,
|
|
271
|
+
runtimeOptions,
|
|
272
|
+
install
|
|
273
|
+
)
|
|
274
|
+
printInstallHint(install, dryRun, runtimeOptions)
|
|
275
|
+
})
|
|
182
276
|
}
|
|
183
277
|
|
|
184
278
|
async function addComponents(names, flags) {
|
|
@@ -193,7 +287,7 @@ async function addComponents(names, flags) {
|
|
|
193
287
|
}
|
|
194
288
|
|
|
195
289
|
async function diffComponents(names, flags) {
|
|
196
|
-
await writeComponents(names, { ...flags, "dry-run": true }, {
|
|
290
|
+
await writeComponents(names, { ...flags, "dry-run": true, "show-diff": true }, {
|
|
197
291
|
inferInstalled: true,
|
|
198
292
|
title: names.length
|
|
199
293
|
? `Checked ${names.join(", ")} for updates`
|
|
@@ -211,16 +305,20 @@ async function updateComponents(names, flags) {
|
|
|
211
305
|
}
|
|
212
306
|
|
|
213
307
|
async function writeComponents(requestedNames, flags, options) {
|
|
214
|
-
const cwd = path.resolve(
|
|
308
|
+
const cwd = path.resolve(flags.cwd || process.cwd())
|
|
215
309
|
const dryRun = Boolean(flags["dry-run"])
|
|
216
310
|
const force = Boolean(flags.force)
|
|
217
|
-
|
|
311
|
+
const showDiff = Boolean(flags["show-diff"])
|
|
218
312
|
const config = await loadConfig(cwd)
|
|
313
|
+
const installPlan = await createInstallPlan(cwd, config)
|
|
314
|
+
const receipt = await readInstallReceipt(cwd)
|
|
315
|
+
validateReceiptDestinations(installPlan, receipt, registry)
|
|
316
|
+
const managedOptions = { dryRun, force, receipt, showDiff }
|
|
219
317
|
const selectedNames =
|
|
220
318
|
requestedNames.length > 0
|
|
221
319
|
? requestedNames
|
|
222
320
|
: options.inferInstalled
|
|
223
|
-
? await findInstalledComponents(
|
|
321
|
+
? await findInstalledComponents(installPlan)
|
|
224
322
|
: requestedNames
|
|
225
323
|
|
|
226
324
|
if (selectedNames.length === 0) {
|
|
@@ -232,12 +330,13 @@ async function writeComponents(requestedNames, flags, options) {
|
|
|
232
330
|
const names = resolveRegistryDependencies(selectedNames)
|
|
233
331
|
|
|
234
332
|
const writes = []
|
|
235
|
-
const deps = {
|
|
333
|
+
const deps = {}
|
|
334
|
+
const components = []
|
|
236
335
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
336
|
+
mergeDependencyRequirements(
|
|
337
|
+
deps,
|
|
338
|
+
registry.base.dependencies || {},
|
|
339
|
+
"registry base"
|
|
241
340
|
)
|
|
242
341
|
|
|
243
342
|
for (const name of names) {
|
|
@@ -246,41 +345,200 @@ async function writeComponents(requestedNames, flags, options) {
|
|
|
246
345
|
throw new Error(`unknown component "${name}". Run \`banhaten list\`.`)
|
|
247
346
|
}
|
|
248
347
|
|
|
249
|
-
|
|
348
|
+
mergeDependencyRequirements(
|
|
349
|
+
deps,
|
|
350
|
+
component.dependencies || {},
|
|
351
|
+
`registry component "${name}"`
|
|
352
|
+
)
|
|
353
|
+
components.push({ component, name })
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
writes.push(
|
|
357
|
+
...(await writeConfig(cwd, config, { dryRun, force })),
|
|
358
|
+
...(await writeProjectSchemaFiles(cwd, { dryRun, force })),
|
|
359
|
+
...(await writeBaseFiles(installPlan, managedOptions)),
|
|
360
|
+
...(await writeGlobalCss(installPlan, managedOptions)),
|
|
361
|
+
...(await writeAliasConfig(installPlan, { dryRun, force }))
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
for (const { component, name } of components) {
|
|
250
365
|
writes.push(
|
|
251
|
-
...(await writeRegistryFiles(
|
|
252
|
-
|
|
253
|
-
|
|
366
|
+
...(await writeRegistryFiles(installPlan, component.files || [], {
|
|
367
|
+
...managedOptions,
|
|
368
|
+
owner: name,
|
|
254
369
|
}))
|
|
255
370
|
)
|
|
256
371
|
}
|
|
257
372
|
|
|
258
|
-
|
|
373
|
+
const packageResult = await updatePackageJson(
|
|
259
374
|
cwd,
|
|
260
|
-
|
|
375
|
+
getPackageChanges(installPlan, deps),
|
|
261
376
|
{ dryRun }
|
|
262
|
-
)
|
|
377
|
+
)
|
|
378
|
+
writes.push(...packageResult.writes)
|
|
379
|
+
writes.push(...(await writeInstallReceipt(installPlan, names, { dryRun })))
|
|
380
|
+
const installResolution = await resolveInstallAction(
|
|
381
|
+
cwd,
|
|
382
|
+
packageResult.installRequired
|
|
383
|
+
)
|
|
384
|
+
for (const evidence of installResolution.evidence) {
|
|
385
|
+
await assertFileUnchanged(evidence.path, evidence.source)
|
|
386
|
+
}
|
|
387
|
+
const { install } = installResolution
|
|
263
388
|
|
|
264
|
-
|
|
265
|
-
|
|
389
|
+
deferUntilApplied(() => {
|
|
390
|
+
printResult(options.title, writes, dryRun, runtimeOptions, install)
|
|
391
|
+
printInstallHint(install, dryRun, runtimeOptions)
|
|
392
|
+
})
|
|
266
393
|
}
|
|
267
394
|
|
|
268
|
-
async function
|
|
395
|
+
async function readInstallReceipt(cwd) {
|
|
396
|
+
const receipt = await readJsonIfExists(path.join(cwd, INSTALL_RECEIPT_FILE))
|
|
397
|
+
if (!receipt) return null
|
|
398
|
+
if (receipt.schemaVersion !== 1 || !receipt.components) {
|
|
399
|
+
throw new Error(
|
|
400
|
+
`${INSTALL_RECEIPT_FILE} is invalid. Re-run an explicit \`banhaten add <component> --force\` command to rebuild it.`
|
|
401
|
+
)
|
|
402
|
+
}
|
|
403
|
+
return receipt
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async function findInstalledComponents(installPlan) {
|
|
407
|
+
const { cwd } = installPlan
|
|
408
|
+
const receiptPath = path.join(cwd, INSTALL_RECEIPT_FILE)
|
|
409
|
+
const receipt = await readInstallReceipt(cwd)
|
|
410
|
+
if (receipt) {
|
|
411
|
+
const installed = []
|
|
412
|
+
for (const [name, record] of Object.entries(receipt.components)) {
|
|
413
|
+
if (!registry.components[name] || !Array.isArray(record.files)) continue
|
|
414
|
+
if (
|
|
415
|
+
record.files.length > 0 &&
|
|
416
|
+
(await allPathsExist(
|
|
417
|
+
record.files.map((file) =>
|
|
418
|
+
path.join(cwd, normalizeReceiptTarget(file.target))
|
|
419
|
+
)
|
|
420
|
+
))
|
|
421
|
+
) {
|
|
422
|
+
installed.push(name)
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return installed
|
|
427
|
+
}
|
|
428
|
+
|
|
269
429
|
const installed = []
|
|
270
430
|
|
|
271
431
|
for (const [name, component] of Object.entries(registry.components)) {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
432
|
+
const files = component.files || []
|
|
433
|
+
if (files.length === 0) continue
|
|
434
|
+
|
|
435
|
+
const targetPaths = files.map((file) =>
|
|
436
|
+
path.join(cwd, resolveInstallTarget(installPlan, file.target))
|
|
437
|
+
)
|
|
438
|
+
if (await allPathsExist(targetPaths)) {
|
|
439
|
+
installed.push(name)
|
|
278
440
|
}
|
|
279
441
|
}
|
|
280
442
|
|
|
281
443
|
return installed
|
|
282
444
|
}
|
|
283
445
|
|
|
446
|
+
async function writeInstallReceipt(installPlan, names, options) {
|
|
447
|
+
const { cwd } = installPlan
|
|
448
|
+
const receiptPath = path.join(cwd, INSTALL_RECEIPT_FILE)
|
|
449
|
+
const current = (await readJsonIfExists(receiptPath)) || {
|
|
450
|
+
schemaVersion: 1,
|
|
451
|
+
components: {},
|
|
452
|
+
}
|
|
453
|
+
const components = normalizeReceiptComponents(current.components || {})
|
|
454
|
+
const managedFiles = Object.fromEntries(
|
|
455
|
+
Object.entries(current.managedFiles || {}).map(([target, record]) => [
|
|
456
|
+
normalizeReceiptTarget(target),
|
|
457
|
+
record,
|
|
458
|
+
])
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
await addManagedFileRecords(
|
|
462
|
+
managedFiles,
|
|
463
|
+
installPlan,
|
|
464
|
+
registry.base.files || [],
|
|
465
|
+
"base"
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
for (const name of names) {
|
|
469
|
+
const component = registry.components[name]
|
|
470
|
+
await addManagedFileRecords(
|
|
471
|
+
managedFiles,
|
|
472
|
+
installPlan,
|
|
473
|
+
component.files || [],
|
|
474
|
+
name
|
|
475
|
+
)
|
|
476
|
+
components[name] = {
|
|
477
|
+
files: await Promise.all(
|
|
478
|
+
(component.files || []).map(async (file) => {
|
|
479
|
+
const sourcePath = path.join(registryRoot, file.source)
|
|
480
|
+
const source = await fs.readFile(sourcePath)
|
|
481
|
+
const target = resolveInstallTarget(installPlan, file.target)
|
|
482
|
+
const content = isTextRegistryFile(file.source)
|
|
483
|
+
? applyTemplate(source.toString("utf8"), installPlan, target)
|
|
484
|
+
: source
|
|
485
|
+
|
|
486
|
+
return {
|
|
487
|
+
target,
|
|
488
|
+
sha256: createHash("sha256").update(content).digest("hex"),
|
|
489
|
+
}
|
|
490
|
+
})
|
|
491
|
+
),
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const next = {
|
|
496
|
+
schemaVersion: 1,
|
|
497
|
+
registryVersion: packageManifest.version,
|
|
498
|
+
installPlan: createInstallPlanReceipt(installPlan),
|
|
499
|
+
components: sortObject(components),
|
|
500
|
+
managedFiles: sortObject(managedFiles),
|
|
501
|
+
tokenCss: await createTokenCssReceipt(installPlan),
|
|
502
|
+
}
|
|
503
|
+
const status = await writeFile(
|
|
504
|
+
receiptPath,
|
|
505
|
+
`${JSON.stringify(next, null, 2)}\n`,
|
|
506
|
+
{ dryRun: options.dryRun, force: true }
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
return [`${status} ${INSTALL_RECEIPT_FILE}`]
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
async function addManagedFileRecords(records, installPlan, files, owner) {
|
|
513
|
+
for (const file of files) {
|
|
514
|
+
const source = await fs.readFile(path.join(registryRoot, file.source))
|
|
515
|
+
const target = resolveInstallTarget(installPlan, file.target)
|
|
516
|
+
const content = isTextRegistryFile(file.source)
|
|
517
|
+
? applyTemplate(source.toString("utf8"), installPlan, target)
|
|
518
|
+
: source
|
|
519
|
+
records[target] = {
|
|
520
|
+
owner,
|
|
521
|
+
sha256: hashContent(content),
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
async function createTokenCssReceipt(installPlan) {
|
|
527
|
+
const tokenCss = await fs.readFile(
|
|
528
|
+
path.join(registryRoot, "styles", "globals.css"),
|
|
529
|
+
"utf8"
|
|
530
|
+
)
|
|
531
|
+
return {
|
|
532
|
+
target: installPlan.destinations.css,
|
|
533
|
+
sha256: hashNormalizedText(tokenCss),
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
async function allPathsExist(paths) {
|
|
538
|
+
const results = await Promise.all(paths.map((targetPath) => exists(targetPath)))
|
|
539
|
+
return results.every(Boolean)
|
|
540
|
+
}
|
|
541
|
+
|
|
284
542
|
function resolveRegistryDependencies(names) {
|
|
285
543
|
const resolved = []
|
|
286
544
|
const seen = new Set()
|
|
@@ -336,8 +594,14 @@ async function loadConfig(cwd) {
|
|
|
336
594
|
|
|
337
595
|
const localConfig = await readJsonIfExists(configPath)
|
|
338
596
|
if (localConfig) {
|
|
339
|
-
|
|
340
|
-
|
|
597
|
+
// Validate what the user actually wrote, before it is merged onto the
|
|
598
|
+
// defaults. Validating the merged object instead would apply
|
|
599
|
+
// `additionalProperties: false` to something that structurally cannot carry
|
|
600
|
+
// an unknown top-level key, so a stray key would pass here and then be
|
|
601
|
+
// silently dropped by writeConfig. Known legacy keys are stripped first so
|
|
602
|
+
// they migrate instead of failing.
|
|
603
|
+
assertSchema(stripLegacyConfigKeys(localConfig), configSchema, CONFIG_FILE)
|
|
604
|
+
|
|
341
605
|
if (localConfig.tailwind?.css) hasExplicitCssPath = true
|
|
342
606
|
config.tailwind = { ...config.tailwind, ...(localConfig.tailwind || {}) }
|
|
343
607
|
config.aliases = { ...config.aliases, ...(localConfig.aliases || {}) }
|
|
@@ -347,6 +611,8 @@ async function loadConfig(cwd) {
|
|
|
347
611
|
config.tailwind.css = await detectCssPath(cwd, config.tailwind.css)
|
|
348
612
|
}
|
|
349
613
|
|
|
614
|
+
assertSchema(config, configSchema, CONFIG_FILE)
|
|
615
|
+
|
|
350
616
|
return config
|
|
351
617
|
}
|
|
352
618
|
|
|
@@ -371,6 +637,38 @@ async function detectCssPath(cwd, fallback) {
|
|
|
371
637
|
async function writeConfig(cwd, config, options) {
|
|
372
638
|
const configPath = path.join(cwd, CONFIG_FILE)
|
|
373
639
|
if ((await exists(configPath)) && !options.force) {
|
|
640
|
+
const current = await fs.readFile(configPath, "utf8")
|
|
641
|
+
const currentConfig = JSON.parse(current)
|
|
642
|
+
// Spread the existing file first so any key this version does not manage
|
|
643
|
+
// survives the rewrite. Rebuilding from a fixed whitelist would delete it
|
|
644
|
+
// and still report the write as a plain "updated". Legacy keys are the one
|
|
645
|
+
// intentional exception, and the removal is reported rather than silent.
|
|
646
|
+
const legacyKeys = findLegacyConfigKeys(currentConfig)
|
|
647
|
+
const nextConfig = {
|
|
648
|
+
...stripLegacyConfigKeys(currentConfig),
|
|
649
|
+
$schema: DEFAULT_CONFIG.$schema,
|
|
650
|
+
tailwind: config.tailwind,
|
|
651
|
+
aliases: config.aliases,
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (JSON.stringify(currentConfig) !== JSON.stringify(nextConfig)) {
|
|
655
|
+
const status = await writeFile(
|
|
656
|
+
configPath,
|
|
657
|
+
`${JSON.stringify(nextConfig, null, 2)}\n`,
|
|
658
|
+
{ dryRun: options.dryRun }
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
const notes = [`${status} ${relative(cwd, configPath)}`]
|
|
662
|
+
if (legacyKeys.length > 0) {
|
|
663
|
+
notes.push(
|
|
664
|
+
`removed legacy config ${legacyKeys.length === 1 ? "key" : "keys"} ` +
|
|
665
|
+
`${legacyKeys.map((key) => JSON.stringify(key)).join(", ")} from ` +
|
|
666
|
+
`${relative(cwd, configPath)}`
|
|
667
|
+
)
|
|
668
|
+
}
|
|
669
|
+
return notes
|
|
670
|
+
}
|
|
671
|
+
|
|
374
672
|
return [`kept ${relative(cwd, configPath)}`]
|
|
375
673
|
}
|
|
376
674
|
|
|
@@ -382,30 +680,72 @@ async function writeConfig(cwd, config, options) {
|
|
|
382
680
|
return [`${status} ${relative(cwd, configPath)}`]
|
|
383
681
|
}
|
|
384
682
|
|
|
385
|
-
async function
|
|
386
|
-
|
|
683
|
+
async function writeProjectSchemaFiles(cwd, options) {
|
|
684
|
+
const schemaFiles = [
|
|
685
|
+
"config.schema.json",
|
|
686
|
+
"registry.schema.json",
|
|
687
|
+
"tokens.schema.json",
|
|
688
|
+
]
|
|
689
|
+
const writes = []
|
|
690
|
+
|
|
691
|
+
for (const schemaFile of schemaFiles) {
|
|
692
|
+
const sourcePath = path.join(schemaRoot, schemaFile)
|
|
693
|
+
const targetPath = path.join(cwd, ".banhaten", "schema", schemaFile)
|
|
694
|
+
const source = await fs.readFile(sourcePath, "utf8")
|
|
695
|
+
const status = await writeFile(targetPath, source, options)
|
|
696
|
+
|
|
697
|
+
writes.push(`${status} ${relative(cwd, targetPath)}`)
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
return writes
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
async function writeBaseFiles(installPlan, options) {
|
|
704
|
+
return writeRegistryFiles(installPlan, registry.base.files || [], {
|
|
705
|
+
...options,
|
|
706
|
+
owner: "base",
|
|
707
|
+
})
|
|
387
708
|
}
|
|
388
709
|
|
|
389
|
-
async function writeRegistryFiles(
|
|
710
|
+
async function writeRegistryFiles(installPlan, files, options) {
|
|
711
|
+
const { cwd } = installPlan
|
|
390
712
|
const writes = []
|
|
391
713
|
|
|
392
714
|
for (const file of files) {
|
|
393
715
|
const sourcePath = path.join(registryRoot, file.source)
|
|
394
|
-
const targetPath = path.join(
|
|
716
|
+
const targetPath = path.join(
|
|
717
|
+
cwd,
|
|
718
|
+
resolveInstallTarget(installPlan, file.target)
|
|
719
|
+
)
|
|
395
720
|
const source = await fs.readFile(sourcePath)
|
|
396
721
|
const content = isTextRegistryFile(file.source)
|
|
397
|
-
? applyTemplate(
|
|
722
|
+
? applyTemplate(
|
|
723
|
+
source.toString("utf8"),
|
|
724
|
+
installPlan,
|
|
725
|
+
relative(cwd, targetPath)
|
|
726
|
+
)
|
|
398
727
|
: source
|
|
728
|
+
const target = relative(cwd, targetPath)
|
|
729
|
+
|
|
730
|
+
await assertManagedFileSafe(targetPath, content, target, options)
|
|
399
731
|
|
|
400
732
|
const status = await writeFile(targetPath, content, options)
|
|
401
|
-
writes.push(`${status} ${
|
|
733
|
+
writes.push(`${status} ${target}`)
|
|
734
|
+
if (options.showDiff && status === "would update") {
|
|
735
|
+
const current = await fs.readFile(
|
|
736
|
+
targetPath,
|
|
737
|
+
isTextRegistryFile(file.source) ? "utf8" : undefined
|
|
738
|
+
)
|
|
739
|
+
writes.push(formatUnifiedDiff(target, current, content))
|
|
740
|
+
}
|
|
402
741
|
}
|
|
403
742
|
|
|
404
743
|
return writes
|
|
405
744
|
}
|
|
406
745
|
|
|
407
|
-
async function writeGlobalCss(
|
|
408
|
-
const
|
|
746
|
+
async function writeGlobalCss(installPlan, options) {
|
|
747
|
+
const { cwd } = installPlan
|
|
748
|
+
const cssPath = path.join(cwd, installPlan.destinations.css)
|
|
409
749
|
const sourcePath = path.join(registryRoot, "styles", "globals.css")
|
|
410
750
|
const tokenCss = await fs.readFile(sourcePath, "utf8")
|
|
411
751
|
const current = await readTextIfExists(cssPath)
|
|
@@ -413,1313 +753,244 @@ async function writeGlobalCss(cwd, config, options) {
|
|
|
413
753
|
? removeExistingTokenBlock(current, relative(cwd, cssPath))
|
|
414
754
|
: { content: "", found: false }
|
|
415
755
|
|
|
416
|
-
if (tokenBlock.found && !options.force) {
|
|
417
|
-
return [`kept ${relative(cwd, cssPath)}`]
|
|
418
|
-
}
|
|
419
|
-
|
|
420
756
|
const nextCss = current
|
|
421
757
|
? `${tokenBlock.content.trimEnd()}\n\n${tokenCss}\n`
|
|
422
758
|
: `${tokenCss}\n`
|
|
423
759
|
const next = ensureGlobalCssImports(nextCss)
|
|
424
760
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
761
|
+
if (
|
|
762
|
+
current &&
|
|
763
|
+
tokenBlock.found &&
|
|
764
|
+
next !== current &&
|
|
765
|
+
!options.dryRun &&
|
|
766
|
+
!options.force
|
|
767
|
+
) {
|
|
768
|
+
const expectedHash = options.receipt?.tokenCss?.sha256
|
|
769
|
+
const currentHash = hashNormalizedText(tokenBlock.block)
|
|
770
|
+
if (!expectedHash || currentHash !== expectedHash) {
|
|
771
|
+
throw new Error(
|
|
772
|
+
`${relative(cwd, cssPath)} contains local changes inside the generated Banhaten token block. ` +
|
|
773
|
+
"Run `banhaten diff` to inspect them, move intentional overrides outside the generated block, or re-run with `--force`."
|
|
774
|
+
)
|
|
775
|
+
}
|
|
776
|
+
}
|
|
431
777
|
|
|
432
|
-
const
|
|
433
|
-
writes
|
|
434
|
-
|
|
778
|
+
const status = await writeFile(cssPath, next, options)
|
|
779
|
+
const writes = [`${status} ${relative(cwd, cssPath)}`]
|
|
780
|
+
if (options.showDiff && status === "would update" && current !== null) {
|
|
781
|
+
writes.push(formatUnifiedDiff(relative(cwd, cssPath), current, next))
|
|
782
|
+
}
|
|
435
783
|
return writes
|
|
436
784
|
}
|
|
437
785
|
|
|
438
|
-
function
|
|
439
|
-
|
|
440
|
-
alias === aliasRoot || alias?.startsWith(`${aliasRoot}/`)
|
|
441
|
-
)
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
async function writeTypescriptAliasConfig(cwd, aliasRoot, options) {
|
|
445
|
-
const tsconfigPath = await findTypescriptConfigPath(cwd)
|
|
446
|
-
if (!tsconfigPath) return []
|
|
447
|
-
|
|
448
|
-
const config = await readJsonc(tsconfigPath)
|
|
449
|
-
const compilerOptions = config.compilerOptions || {}
|
|
450
|
-
const paths = compilerOptions.paths || {}
|
|
451
|
-
const sourceRoot = shouldUseSrcDirectory(cwd) ? "./src" : "."
|
|
452
|
-
const aliasPattern = `${aliasRoot}/*`
|
|
453
|
-
const targetPattern = `${sourceRoot}/*`
|
|
454
|
-
|
|
455
|
-
if (Array.isArray(paths[aliasPattern]) && paths[aliasPattern].includes(targetPattern)) {
|
|
456
|
-
return [`kept ${relative(cwd, tsconfigPath)}`]
|
|
457
|
-
}
|
|
786
|
+
async function writeAliasConfig(installPlan, options) {
|
|
787
|
+
const writes = []
|
|
458
788
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
789
|
+
for (const plannedConfig of [installPlan.typescript, installPlan.vite]) {
|
|
790
|
+
if (!plannedConfig) continue
|
|
791
|
+
const status = await writeFile(
|
|
792
|
+
plannedConfig.targetPath,
|
|
793
|
+
plannedConfig.nextSource,
|
|
794
|
+
{
|
|
795
|
+
dryRun: options.dryRun,
|
|
796
|
+
expectedCurrent: plannedConfig.currentSource,
|
|
797
|
+
force: true,
|
|
798
|
+
}
|
|
799
|
+
)
|
|
800
|
+
writes.push(
|
|
801
|
+
`${status} ${relative(installPlan.cwd, plannedConfig.targetPath)}`
|
|
802
|
+
)
|
|
465
803
|
}
|
|
466
804
|
|
|
467
|
-
|
|
468
|
-
dryRun: options.dryRun,
|
|
469
|
-
force: true,
|
|
470
|
-
})
|
|
471
|
-
return [`${status} ${relative(cwd, tsconfigPath)}`]
|
|
805
|
+
return writes
|
|
472
806
|
}
|
|
473
807
|
|
|
474
|
-
|
|
475
|
-
const
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
"jsconfig.json",
|
|
479
|
-
]
|
|
480
|
-
|
|
481
|
-
for (const candidate of candidates) {
|
|
482
|
-
const target = path.join(cwd, candidate)
|
|
483
|
-
if (await exists(target)) return target
|
|
808
|
+
function getPackageChanges(installPlan, dependencies) {
|
|
809
|
+
const devDependencies = { ...SETUP_DEV_DEPENDENCIES }
|
|
810
|
+
if (installPlan.vite) {
|
|
811
|
+
Object.assign(devDependencies, VITE_DEV_DEPENDENCIES)
|
|
484
812
|
}
|
|
485
813
|
|
|
486
|
-
return
|
|
814
|
+
return {
|
|
815
|
+
dependencies,
|
|
816
|
+
devDependencies,
|
|
817
|
+
}
|
|
487
818
|
}
|
|
488
819
|
|
|
489
|
-
async function
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
const
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
try {
|
|
498
|
-
next = updateViteConfigSource(current, { aliasRoot, sourceRoot })
|
|
499
|
-
} catch (error) {
|
|
500
|
-
throw new Error(`${relative(cwd, viteConfigPath)}: ${error.message}`)
|
|
820
|
+
async function updatePackageJson(cwd, changes, options) {
|
|
821
|
+
const packagePath = path.join(cwd, "package.json")
|
|
822
|
+
const currentSource = await readTextIfExists(packagePath)
|
|
823
|
+
const packageExists = currentSource !== null
|
|
824
|
+
const packageJson = currentSource !== null ? JSON.parse(currentSource) : {
|
|
825
|
+
name: path.basename(cwd).toLowerCase(),
|
|
826
|
+
version: "0.0.0",
|
|
827
|
+
private: true,
|
|
501
828
|
}
|
|
829
|
+
const packagePlan = createPackageDependencyPlan(packageJson, changes)
|
|
502
830
|
|
|
503
|
-
if (
|
|
504
|
-
|
|
831
|
+
if (packagePlan.operations.length === 0 && packageExists) {
|
|
832
|
+
await assertFileUnchanged(packagePath, currentSource)
|
|
833
|
+
const installState = await inspectPackageInstallState({
|
|
834
|
+
cwd,
|
|
835
|
+
packageJson,
|
|
836
|
+
requirements: packagePlan.requirements,
|
|
837
|
+
})
|
|
838
|
+
for (const evidence of installState.evidence) {
|
|
839
|
+
await assertFileUnchanged(evidence.path, evidence.source)
|
|
840
|
+
}
|
|
841
|
+
return {
|
|
842
|
+
installRequired: installState.required,
|
|
843
|
+
writes: [`kept ${relative(cwd, packagePath)}`],
|
|
844
|
+
}
|
|
505
845
|
}
|
|
506
846
|
|
|
507
|
-
|
|
847
|
+
applyPackageDependencyPlan(packageJson, packagePlan.operations)
|
|
848
|
+
|
|
849
|
+
const status = await writeFile(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`, {
|
|
508
850
|
dryRun: options.dryRun,
|
|
851
|
+
expectedCurrent: currentSource,
|
|
509
852
|
force: true,
|
|
510
853
|
})
|
|
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
854
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
855
|
+
return {
|
|
856
|
+
installRequired: packagePlan.operations.length > 0,
|
|
857
|
+
writes: [`${status} ${relative(cwd, packagePath)}`],
|
|
525
858
|
}
|
|
526
|
-
|
|
527
|
-
return null
|
|
528
859
|
}
|
|
529
860
|
|
|
530
|
-
function
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
861
|
+
function applyTemplate(source, installPlan, target) {
|
|
862
|
+
const replacements = {
|
|
863
|
+
"@/components/ui": resolveTemplateImport(
|
|
864
|
+
installPlan,
|
|
865
|
+
target,
|
|
866
|
+
"ui"
|
|
867
|
+
),
|
|
868
|
+
"@/lib/utils": resolveTemplateImport(
|
|
869
|
+
installPlan,
|
|
870
|
+
target,
|
|
871
|
+
"utils"
|
|
872
|
+
),
|
|
873
|
+
"@/hooks": resolveTemplateImport(
|
|
874
|
+
installPlan,
|
|
875
|
+
target,
|
|
876
|
+
"hooks"
|
|
877
|
+
),
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
return source.replace(
|
|
881
|
+
/@\/components\/ui|@\/lib\/utils|@\/hooks/g,
|
|
882
|
+
(match) => replacements[match]
|
|
541
883
|
)
|
|
542
884
|
}
|
|
543
885
|
|
|
544
|
-
function
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
[`import ${localName} from "@tailwindcss/vite"`]
|
|
554
|
-
)
|
|
886
|
+
function resolveTemplateImport(installPlan, target, destinationName) {
|
|
887
|
+
const configuredAlias = installPlan.config.aliases[destinationName]
|
|
888
|
+
if (
|
|
889
|
+
configuredAlias === "@" ||
|
|
890
|
+
configuredAlias === "~" ||
|
|
891
|
+
configuredAlias.startsWith("@/") ||
|
|
892
|
+
configuredAlias.startsWith("~/")
|
|
893
|
+
) {
|
|
894
|
+
return configuredAlias.replace(/\/$/, "")
|
|
555
895
|
}
|
|
556
896
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
897
|
+
const targetDirectory = path.posix.dirname(
|
|
898
|
+
normalizeReceiptTarget(target)
|
|
899
|
+
)
|
|
900
|
+
const destination = installPlan.destinations[destinationName]
|
|
901
|
+
const relativeImport = path.posix.relative(targetDirectory, destination)
|
|
562
902
|
|
|
563
|
-
return
|
|
903
|
+
if (!relativeImport) return "."
|
|
904
|
+
return relativeImport.startsWith(".")
|
|
905
|
+
? relativeImport
|
|
906
|
+
: `./${relativeImport}`
|
|
564
907
|
}
|
|
565
908
|
|
|
566
|
-
function
|
|
567
|
-
|
|
568
|
-
let next = addViteResolveAlias(source, aliasRoot, sourceRoot)
|
|
569
|
-
next = addVitePluginCall(next, `${tailwindLocalName}()`, tailwindLocalName)
|
|
570
|
-
next = ensureTailwindViteImport(next, tailwindLocalName)
|
|
571
|
-
next = ensureNodeUrlImport(next)
|
|
572
|
-
return next
|
|
909
|
+
function isTextRegistryFile(source) {
|
|
910
|
+
return !/\.(avif|eot|gif|ico|jpe?g|otf|png|ttf|webp|woff2?)$/i.test(source)
|
|
573
911
|
}
|
|
574
912
|
|
|
575
|
-
function
|
|
576
|
-
|
|
577
|
-
(entry) => entry.moduleSpecifier === "@tailwindcss/vite"
|
|
578
|
-
)
|
|
579
|
-
|
|
580
|
-
if (!tailwindImport) return "tailwindcss"
|
|
581
|
-
if (tailwindImport.defaultLocal) return tailwindImport.defaultLocal
|
|
582
|
-
|
|
583
|
-
throw new Error(
|
|
584
|
-
"unsupported Vite config: @tailwindcss/vite must be imported with a default binding."
|
|
585
|
-
)
|
|
913
|
+
async function readJson(filePath) {
|
|
914
|
+
return JSON.parse(await fs.readFile(filePath, "utf8"))
|
|
586
915
|
}
|
|
587
916
|
|
|
588
|
-
function
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
if (!resolveProperty) {
|
|
596
|
-
return insertObjectProperty(
|
|
597
|
-
source,
|
|
598
|
-
configObject,
|
|
599
|
-
[
|
|
600
|
-
"resolve: {",
|
|
601
|
-
" alias: {",
|
|
602
|
-
` ${aliasEntry},`,
|
|
603
|
-
" },",
|
|
604
|
-
"},",
|
|
605
|
-
].join("\n")
|
|
606
|
-
)
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
const resolveObject = expectObjectLiteralValue(
|
|
610
|
-
source,
|
|
611
|
-
resolveProperty,
|
|
612
|
-
"unsupported Vite config: resolve must be an object literal."
|
|
613
|
-
)
|
|
614
|
-
const aliasProperty = findObjectProperty(source, resolveObject, "alias")
|
|
615
|
-
|
|
616
|
-
if (!aliasProperty) {
|
|
617
|
-
return insertObjectProperty(
|
|
618
|
-
source,
|
|
619
|
-
resolveObject,
|
|
620
|
-
[
|
|
621
|
-
"alias: {",
|
|
622
|
-
` ${aliasEntry},`,
|
|
623
|
-
"},",
|
|
624
|
-
].join("\n")
|
|
625
|
-
)
|
|
917
|
+
async function readJsonIfExists(filePath) {
|
|
918
|
+
try {
|
|
919
|
+
return await readJson(filePath)
|
|
920
|
+
} catch (error) {
|
|
921
|
+
if (error.code === "ENOENT") return null
|
|
922
|
+
throw error
|
|
626
923
|
}
|
|
924
|
+
}
|
|
627
925
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
if (findObjectProperty(source, aliasObject, aliasRoot)) {
|
|
635
|
-
return source
|
|
926
|
+
async function readTextIfExists(filePath) {
|
|
927
|
+
try {
|
|
928
|
+
return await fs.readFile(filePath, "utf8")
|
|
929
|
+
} catch (error) {
|
|
930
|
+
if (error.code === "ENOENT") return null
|
|
931
|
+
throw error
|
|
636
932
|
}
|
|
637
|
-
|
|
638
|
-
return insertObjectProperty(source, aliasObject, `${aliasEntry},`)
|
|
639
933
|
}
|
|
640
934
|
|
|
641
|
-
function
|
|
642
|
-
|
|
643
|
-
const pluginsProperty = findObjectProperty(source, configObject, "plugins")
|
|
644
|
-
|
|
645
|
-
if (!pluginsProperty) {
|
|
646
|
-
return insertObjectProperty(source, configObject, `plugins: [${pluginCall}],`)
|
|
647
|
-
}
|
|
935
|
+
async function assertManagedFileSafe(targetPath, nextContent, target, options) {
|
|
936
|
+
if (options.dryRun || options.force || !(await exists(targetPath))) return
|
|
648
937
|
|
|
649
|
-
const
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
)
|
|
938
|
+
const current = await fs.readFile(targetPath)
|
|
939
|
+
const next = Buffer.isBuffer(nextContent)
|
|
940
|
+
? nextContent
|
|
941
|
+
: Buffer.from(nextContent, "utf8")
|
|
942
|
+
if (current.equals(next)) return
|
|
654
943
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
944
|
+
const expectedHash = findManagedFileHash(options.receipt, target)
|
|
945
|
+
const currentHash = hashContent(current)
|
|
946
|
+
if (expectedHash && currentHash === expectedHash) return
|
|
658
947
|
|
|
659
|
-
|
|
948
|
+
const owner = options.owner ? ` (${options.owner})` : ""
|
|
949
|
+
const reason = expectedHash
|
|
950
|
+
? "has local changes since Banhaten installed it"
|
|
951
|
+
: "already exists without a Banhaten ownership receipt"
|
|
952
|
+
const diffCommand = ["banhaten", "diff", options.owner === "base" ? "" : options.owner]
|
|
953
|
+
.filter(Boolean)
|
|
954
|
+
.join(" ")
|
|
955
|
+
throw new Error(
|
|
956
|
+
`${target}${owner} ${reason}. Run \`${diffCommand}\` to inspect the registry version, ` +
|
|
957
|
+
"or re-run the command with `--force` to replace the file."
|
|
958
|
+
)
|
|
660
959
|
}
|
|
661
960
|
|
|
662
|
-
function
|
|
663
|
-
const
|
|
961
|
+
function findManagedFileHash(receipt, target) {
|
|
962
|
+
const normalizedTarget = normalizeReceiptTarget(target)
|
|
963
|
+
const direct = Object.entries(receipt?.managedFiles || {}).find(
|
|
964
|
+
([recordedTarget]) =>
|
|
965
|
+
normalizeReceiptTarget(recordedTarget) === normalizedTarget
|
|
966
|
+
)?.[1]?.sha256
|
|
967
|
+
if (direct) return direct
|
|
664
968
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
969
|
+
for (const component of Object.values(receipt?.components || {})) {
|
|
970
|
+
const match = component.files?.find(
|
|
971
|
+
(file) => normalizeReceiptTarget(file.target) === normalizedTarget
|
|
668
972
|
)
|
|
973
|
+
if (match?.sha256) return match.sha256
|
|
669
974
|
}
|
|
670
975
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
}
|
|
976
|
+
return null
|
|
977
|
+
}
|
|
674
978
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
"unsupported Vite config: default export must be `defineConfig({ ... })` or an object literal."
|
|
679
|
-
)
|
|
680
|
-
}
|
|
979
|
+
function hashContent(content) {
|
|
980
|
+
return createHash("sha256").update(content).digest("hex")
|
|
981
|
+
}
|
|
681
982
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
}
|
|
983
|
+
function hashNormalizedText(content) {
|
|
984
|
+
return hashContent(String(content).replace(/\r\n/g, "\n").trim())
|
|
985
|
+
}
|
|
686
986
|
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
987
|
+
async function exists(filePath) {
|
|
988
|
+
try {
|
|
989
|
+
await fs.access(filePath)
|
|
990
|
+
return true
|
|
991
|
+
} catch {
|
|
992
|
+
return false
|
|
692
993
|
}
|
|
693
|
-
|
|
694
|
-
return createBracketRange(source, index, "{")
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
function findExportDefaultExpressionStart(source) {
|
|
698
|
-
let index = 0
|
|
699
|
-
|
|
700
|
-
while (index < source.length) {
|
|
701
|
-
const next = skipIgnoredSyntax(source, index)
|
|
702
|
-
if (next !== index) {
|
|
703
|
-
index = next
|
|
704
|
-
continue
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
if (!matchIdentifier(source, index, "export")) {
|
|
708
|
-
index += 1
|
|
709
|
-
continue
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
const defaultStart = skipWhitespaceAndComments(source, index + "export".length)
|
|
713
|
-
if (!matchIdentifier(source, defaultStart, "default")) {
|
|
714
|
-
index += "export".length
|
|
715
|
-
continue
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
return skipWhitespaceAndComments(source, defaultStart + "default".length)
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
return -1
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
function createBracketRange(source, start, open) {
|
|
725
|
-
const close = open === "{" ? "}" : open === "[" ? "]" : ")"
|
|
726
|
-
const end = findMatchingBracket(source, start, open, close)
|
|
727
|
-
return { start, end: end + 1 }
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
function expectObjectLiteralValue(source, property, message) {
|
|
731
|
-
const valueStart = skipWhitespaceAndComments(source, property.valueStart)
|
|
732
|
-
if (source[valueStart] !== "{") throw new Error(message)
|
|
733
|
-
return createBracketRange(source, valueStart, "{")
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
function expectArrayLiteralValue(source, property, message) {
|
|
737
|
-
const valueStart = skipWhitespaceAndComments(source, property.valueStart)
|
|
738
|
-
if (source[valueStart] !== "[") throw new Error(message)
|
|
739
|
-
return createBracketRange(source, valueStart, "[")
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
function findObjectProperty(source, objectRange, propertyName) {
|
|
743
|
-
const bodyEnd = objectRange.end - 1
|
|
744
|
-
let index = objectRange.start + 1
|
|
745
|
-
|
|
746
|
-
while (index < bodyEnd) {
|
|
747
|
-
index = skipWhitespaceCommentsAndCommas(source, index, bodyEnd)
|
|
748
|
-
if (index >= bodyEnd) return null
|
|
749
|
-
|
|
750
|
-
const propertyStart = index
|
|
751
|
-
const name = readPropertyName(source, index)
|
|
752
|
-
const propertyEnd = findTopLevelCommaOrEnd(source, propertyStart, bodyEnd)
|
|
753
|
-
|
|
754
|
-
if (!name) {
|
|
755
|
-
index = propertyEnd + 1
|
|
756
|
-
continue
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
index = skipWhitespaceAndComments(source, name.end)
|
|
760
|
-
if (source[index] !== ":") {
|
|
761
|
-
if (name.name === propertyName) {
|
|
762
|
-
throw new Error(
|
|
763
|
-
`unsupported Vite config: ${propertyName} must be a normal object property.`
|
|
764
|
-
)
|
|
765
|
-
}
|
|
766
|
-
index = propertyEnd + 1
|
|
767
|
-
continue
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
if (name.name === propertyName) {
|
|
771
|
-
return {
|
|
772
|
-
name: name.name,
|
|
773
|
-
start: propertyStart,
|
|
774
|
-
end: propertyEnd,
|
|
775
|
-
valueStart: skipWhitespaceAndComments(source, index + 1),
|
|
776
|
-
valueEnd: propertyEnd,
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
index = propertyEnd + 1
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
return null
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
function readPropertyName(source, index) {
|
|
787
|
-
if (source[index] === "\"" || source[index] === "'") {
|
|
788
|
-
const literal = readStringLiteral(source, index)
|
|
789
|
-
return literal ? { name: literal.value, end: literal.end } : null
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
return readIdentifierAt(source, index)
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
function insertObjectProperty(source, objectRange, propertyText) {
|
|
796
|
-
const insertAt = objectRange.start + 1
|
|
797
|
-
const bodyEnd = objectRange.end - 1
|
|
798
|
-
const body = source.slice(insertAt, bodyEnd)
|
|
799
|
-
const hasContent = body.trim().length > 0
|
|
800
|
-
const propertyIndent = detectObjectChildIndent(source, objectRange)
|
|
801
|
-
const closeIndent = getLineIndent(source, bodyEnd)
|
|
802
|
-
const indentedProperty = indentMultiline(propertyText, propertyIndent)
|
|
803
|
-
|
|
804
|
-
if (!hasContent) {
|
|
805
|
-
return `${source.slice(0, insertAt)}\n${indentedProperty}\n${closeIndent}${source.slice(insertAt)}`
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
const needsTrailingNewline = source[insertAt] !== "\n" && source[insertAt] !== "\r"
|
|
809
|
-
const insertion = `\n${indentedProperty}${needsTrailingNewline ? "\n" : ""}`
|
|
810
|
-
return `${source.slice(0, insertAt)}${insertion}${source.slice(insertAt)}`
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
function detectObjectChildIndent(source, objectRange) {
|
|
814
|
-
const bodyEnd = objectRange.end - 1
|
|
815
|
-
let index = objectRange.start + 1
|
|
816
|
-
|
|
817
|
-
while (index < bodyEnd) {
|
|
818
|
-
if (source[index] === "\n") {
|
|
819
|
-
const next = skipHorizontalWhitespace(source, index + 1)
|
|
820
|
-
if (next < bodyEnd && source[next] !== "\n" && source[next] !== "\r") {
|
|
821
|
-
return source.slice(index + 1, next)
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
index += 1
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
return `${getLineIndent(source, objectRange.start)} `
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
function appendArrayElement(source, arrayRange, elementText) {
|
|
831
|
-
const bodyStart = arrayRange.start + 1
|
|
832
|
-
const bodyEnd = arrayRange.end - 1
|
|
833
|
-
const body = source.slice(bodyStart, bodyEnd)
|
|
834
|
-
|
|
835
|
-
if (body.trim().length === 0) {
|
|
836
|
-
return `${source.slice(0, bodyStart)}${elementText}${source.slice(bodyStart)}`
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
const contentEnd = trimEndIndex(source, bodyStart, bodyEnd)
|
|
840
|
-
const hasNewline = bodyIncludesNewline(source, bodyStart, bodyEnd)
|
|
841
|
-
const hasTrailingComma = previousCodeChar(source, contentEnd) === ","
|
|
842
|
-
|
|
843
|
-
if (!hasNewline) {
|
|
844
|
-
const separator = hasTrailingComma ? " " : ", "
|
|
845
|
-
return `${source.slice(0, contentEnd)}${separator}${elementText}${source.slice(contentEnd)}`
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
const closeIndent = getLineIndent(source, bodyEnd)
|
|
849
|
-
const elementIndent = `${closeIndent} `
|
|
850
|
-
const separator = hasTrailingComma ? "\n" : ",\n"
|
|
851
|
-
return `${source.slice(0, contentEnd)}${separator}${elementIndent}${elementText},${source.slice(contentEnd)}`
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
function arrayHasCallExpression(source, arrayRange, localName) {
|
|
855
|
-
const bodyEnd = arrayRange.end - 1
|
|
856
|
-
let index = arrayRange.start + 1
|
|
857
|
-
|
|
858
|
-
while (index < bodyEnd) {
|
|
859
|
-
index = skipWhitespaceCommentsAndCommas(source, index, bodyEnd)
|
|
860
|
-
if (index >= bodyEnd) return false
|
|
861
|
-
|
|
862
|
-
const elementEnd = findTopLevelCommaOrEnd(source, index, bodyEnd)
|
|
863
|
-
if (startsWithCallExpression(source, index, elementEnd, localName)) {
|
|
864
|
-
return true
|
|
865
|
-
}
|
|
866
|
-
index = elementEnd + 1
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
return false
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
function startsWithCallExpression(source, start, end, localName) {
|
|
873
|
-
const index = skipWhitespaceAndComments(source, start)
|
|
874
|
-
if (!matchIdentifier(source, index, localName)) return false
|
|
875
|
-
|
|
876
|
-
const callStart = skipWhitespaceAndComments(source, index + localName.length)
|
|
877
|
-
if (source[callStart] !== "(") return false
|
|
878
|
-
|
|
879
|
-
const callEnd = createBracketRange(source, callStart, "(").end
|
|
880
|
-
const next = skipWhitespaceAndComments(source, callEnd)
|
|
881
|
-
return next <= end
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
function parseImportDeclarations(source) {
|
|
885
|
-
const declarations = []
|
|
886
|
-
let index = 0
|
|
887
|
-
|
|
888
|
-
while (index < source.length) {
|
|
889
|
-
const next = skipIgnoredSyntax(source, index)
|
|
890
|
-
if (next !== index) {
|
|
891
|
-
index = next
|
|
892
|
-
continue
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
if (!matchIdentifier(source, index, "import")) {
|
|
896
|
-
index += 1
|
|
897
|
-
continue
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
const clauseStart = skipWhitespaceAndComments(source, index + "import".length)
|
|
901
|
-
if (source[clauseStart] === ".") {
|
|
902
|
-
index = clauseStart + 1
|
|
903
|
-
continue
|
|
904
|
-
}
|
|
905
|
-
if (source[clauseStart] === "(") {
|
|
906
|
-
index = clauseStart + 1
|
|
907
|
-
continue
|
|
908
|
-
}
|
|
909
|
-
|
|
910
|
-
const end = findImportDeclarationEnd(source, index)
|
|
911
|
-
const declaration = source.slice(index, end)
|
|
912
|
-
const moduleSpecifier = getImportModuleSpecifier(declaration)
|
|
913
|
-
|
|
914
|
-
if (moduleSpecifier) {
|
|
915
|
-
declarations.push({
|
|
916
|
-
start: index,
|
|
917
|
-
end,
|
|
918
|
-
moduleSpecifier,
|
|
919
|
-
defaultLocal: getDefaultImportLocal(declaration),
|
|
920
|
-
namedLocals: getNamedImportLocals(declaration),
|
|
921
|
-
})
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
index = end
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
return declarations
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
function findImportDeclarationEnd(source, start) {
|
|
931
|
-
const stack = []
|
|
932
|
-
let sawModuleSpecifier = false
|
|
933
|
-
let index = start
|
|
934
|
-
|
|
935
|
-
while (index < source.length) {
|
|
936
|
-
const skipped = skipComment(source, index)
|
|
937
|
-
if (skipped !== index) {
|
|
938
|
-
index = skipped
|
|
939
|
-
continue
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
if (source[index] === "\"" || source[index] === "'") {
|
|
943
|
-
const literal = readStringLiteral(source, index)
|
|
944
|
-
if (!literal) return source.length
|
|
945
|
-
sawModuleSpecifier = true
|
|
946
|
-
index = literal.end
|
|
947
|
-
continue
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
if (isOpeningBracket(source[index])) {
|
|
951
|
-
stack.push(expectedClosingBracket(source[index]))
|
|
952
|
-
index += 1
|
|
953
|
-
continue
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
if (isClosingBracket(source[index])) {
|
|
957
|
-
if (stack.at(-1) === source[index]) stack.pop()
|
|
958
|
-
index += 1
|
|
959
|
-
continue
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
if (stack.length === 0 && source[index] === ";") return index + 1
|
|
963
|
-
if (
|
|
964
|
-
stack.length === 0 &&
|
|
965
|
-
sawModuleSpecifier &&
|
|
966
|
-
(source[index] === "\n" || source[index] === "\r")
|
|
967
|
-
) {
|
|
968
|
-
return index + 1
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
index += 1
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
return source.length
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
function getImportModuleSpecifier(declaration) {
|
|
978
|
-
let moduleSpecifier = null
|
|
979
|
-
let index = 0
|
|
980
|
-
|
|
981
|
-
while (index < declaration.length) {
|
|
982
|
-
const skipped = skipComment(declaration, index)
|
|
983
|
-
if (skipped !== index) {
|
|
984
|
-
index = skipped
|
|
985
|
-
continue
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
if (declaration[index] === "\"" || declaration[index] === "'") {
|
|
989
|
-
const literal = readStringLiteral(declaration, index)
|
|
990
|
-
if (!literal) break
|
|
991
|
-
moduleSpecifier = literal.value
|
|
992
|
-
index = literal.end
|
|
993
|
-
continue
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
index += 1
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
return moduleSpecifier
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
function getDefaultImportLocal(declaration) {
|
|
1003
|
-
let index = skipWhitespaceAndComments(declaration, "import".length)
|
|
1004
|
-
|
|
1005
|
-
if (matchIdentifier(declaration, index, "type")) {
|
|
1006
|
-
index = skipWhitespaceAndComments(declaration, index + "type".length)
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
if (declaration[index] === "\"" || declaration[index] === "{" || declaration[index] === "*") {
|
|
1010
|
-
return null
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
const local = readIdentifierAt(declaration, index)
|
|
1014
|
-
return local?.name ?? null
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
function getNamedImportLocals(declaration) {
|
|
1018
|
-
const open = declaration.indexOf("{")
|
|
1019
|
-
if (open === -1) return []
|
|
1020
|
-
|
|
1021
|
-
const close = findMatchingBracket(declaration, open, "{", "}")
|
|
1022
|
-
const namedImportBody = declaration.slice(open + 1, close)
|
|
1023
|
-
const locals = []
|
|
1024
|
-
|
|
1025
|
-
for (const specifier of splitTopLevelList(namedImportBody)) {
|
|
1026
|
-
const trimmed = specifier.trim()
|
|
1027
|
-
if (!trimmed) continue
|
|
1028
|
-
|
|
1029
|
-
const withoutType = trimmed.startsWith("type ")
|
|
1030
|
-
? trimmed.slice("type ".length).trim()
|
|
1031
|
-
: trimmed
|
|
1032
|
-
const asIndex = withoutType.toLowerCase().lastIndexOf(" as ")
|
|
1033
|
-
const localName = asIndex === -1
|
|
1034
|
-
? withoutType
|
|
1035
|
-
: withoutType.slice(asIndex + " as ".length).trim()
|
|
1036
|
-
const local = readIdentifierAt(localName, 0)
|
|
1037
|
-
|
|
1038
|
-
if (local) locals.push(local.name)
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
return locals
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
function splitTopLevelList(source) {
|
|
1045
|
-
const items = []
|
|
1046
|
-
let start = 0
|
|
1047
|
-
let index = 0
|
|
1048
|
-
|
|
1049
|
-
while (index < source.length) {
|
|
1050
|
-
const skipped = skipIgnoredSyntax(source, index)
|
|
1051
|
-
if (skipped !== index) {
|
|
1052
|
-
index = skipped
|
|
1053
|
-
continue
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
if (source[index] === ",") {
|
|
1057
|
-
items.push(source.slice(start, index))
|
|
1058
|
-
start = index + 1
|
|
1059
|
-
}
|
|
1060
|
-
index += 1
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
items.push(source.slice(start))
|
|
1064
|
-
return items
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
function prependImportLines(source, lines) {
|
|
1068
|
-
if (lines.length === 0) return source
|
|
1069
|
-
|
|
1070
|
-
let insertAt = 0
|
|
1071
|
-
if (source.startsWith("#!")) {
|
|
1072
|
-
const lineEnd = source.indexOf("\n")
|
|
1073
|
-
insertAt = lineEnd === -1 ? source.length : lineEnd + 1
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
const prefix = `${lines.join("\n")}\n`
|
|
1077
|
-
return `${source.slice(0, insertAt)}${prefix}${source.slice(insertAt)}`
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
function findTopLevelCommaOrEnd(source, start, end) {
|
|
1081
|
-
const stack = []
|
|
1082
|
-
let index = start
|
|
1083
|
-
|
|
1084
|
-
while (index < end) {
|
|
1085
|
-
const next = skipIgnoredSyntax(source, index)
|
|
1086
|
-
if (next !== index) {
|
|
1087
|
-
index = next
|
|
1088
|
-
continue
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
if (isOpeningBracket(source[index])) {
|
|
1092
|
-
stack.push(expectedClosingBracket(source[index]))
|
|
1093
|
-
index += 1
|
|
1094
|
-
continue
|
|
1095
|
-
}
|
|
1096
|
-
|
|
1097
|
-
if (isClosingBracket(source[index])) {
|
|
1098
|
-
if (stack.at(-1) === source[index]) stack.pop()
|
|
1099
|
-
index += 1
|
|
1100
|
-
continue
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
if (stack.length === 0 && source[index] === ",") return index
|
|
1104
|
-
index += 1
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
return end
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
function findMatchingBracket(source, start, open, close) {
|
|
1111
|
-
if (source[start] !== open) {
|
|
1112
|
-
throw new Error("unsupported Vite config: malformed JavaScript object.")
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
const stack = []
|
|
1116
|
-
let index = start
|
|
1117
|
-
|
|
1118
|
-
while (index < source.length) {
|
|
1119
|
-
const next = skipIgnoredSyntax(source, index)
|
|
1120
|
-
if (next !== index) {
|
|
1121
|
-
index = next
|
|
1122
|
-
continue
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
if (isOpeningBracket(source[index])) {
|
|
1126
|
-
stack.push(expectedClosingBracket(source[index]))
|
|
1127
|
-
index += 1
|
|
1128
|
-
continue
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
if (isClosingBracket(source[index])) {
|
|
1132
|
-
const expected = stack.pop()
|
|
1133
|
-
if (expected !== source[index]) {
|
|
1134
|
-
throw new Error("unsupported Vite config: malformed JavaScript brackets.")
|
|
1135
|
-
}
|
|
1136
|
-
if (stack.length === 0) return index
|
|
1137
|
-
index += 1
|
|
1138
|
-
continue
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
index += 1
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
throw new Error(`unsupported Vite config: missing closing ${close}.`)
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
function skipIgnoredSyntax(source, index) {
|
|
1148
|
-
const commentEnd = skipComment(source, index)
|
|
1149
|
-
if (commentEnd !== index) return commentEnd
|
|
1150
|
-
|
|
1151
|
-
if (source[index] === "\"" || source[index] === "'" || source[index] === "`") {
|
|
1152
|
-
return skipQuotedText(source, index)
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
return index
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
function skipComment(source, index) {
|
|
1159
|
-
if (source[index] !== "/") return index
|
|
1160
|
-
|
|
1161
|
-
if (source[index + 1] === "/") {
|
|
1162
|
-
const lineEnd = source.indexOf("\n", index + 2)
|
|
1163
|
-
return lineEnd === -1 ? source.length : lineEnd + 1
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
if (source[index + 1] === "*") {
|
|
1167
|
-
const blockEnd = source.indexOf("*/", index + 2)
|
|
1168
|
-
return blockEnd === -1 ? source.length : blockEnd + 2
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
return index
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
function skipQuotedText(source, index) {
|
|
1175
|
-
const quote = source[index]
|
|
1176
|
-
index += 1
|
|
1177
|
-
|
|
1178
|
-
while (index < source.length) {
|
|
1179
|
-
if (source[index] === "\\") {
|
|
1180
|
-
index += 2
|
|
1181
|
-
continue
|
|
1182
|
-
}
|
|
1183
|
-
|
|
1184
|
-
if (source[index] === quote) return index + 1
|
|
1185
|
-
index += 1
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
return source.length
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
function readStringLiteral(source, index) {
|
|
1192
|
-
const quote = source[index]
|
|
1193
|
-
if (quote !== "\"" && quote !== "'") return null
|
|
1194
|
-
|
|
1195
|
-
let value = ""
|
|
1196
|
-
index += 1
|
|
1197
|
-
|
|
1198
|
-
while (index < source.length) {
|
|
1199
|
-
if (source[index] === "\\") {
|
|
1200
|
-
value += source[index + 1] ?? ""
|
|
1201
|
-
index += 2
|
|
1202
|
-
continue
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
if (source[index] === quote) {
|
|
1206
|
-
return { value, end: index + 1 }
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
value += source[index]
|
|
1210
|
-
index += 1
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
return null
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
function readIdentifierAt(source, index) {
|
|
1217
|
-
if (!isIdentifierStart(source[index])) return null
|
|
1218
|
-
|
|
1219
|
-
let end = index + 1
|
|
1220
|
-
while (end < source.length && isIdentifierPart(source[end])) {
|
|
1221
|
-
end += 1
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
return { name: source.slice(index, end), end }
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
function matchIdentifier(source, index, name) {
|
|
1228
|
-
if (source.slice(index, index + name.length) !== name) return false
|
|
1229
|
-
|
|
1230
|
-
const before = source[index - 1]
|
|
1231
|
-
const after = source[index + name.length]
|
|
1232
|
-
return !isIdentifierPart(before) && !isIdentifierPart(after)
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
function isIdentifierStart(char) {
|
|
1236
|
-
return Boolean(char && /[A-Za-z_$]/.test(char))
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
function isIdentifierPart(char) {
|
|
1240
|
-
return Boolean(char && /[A-Za-z0-9_$]/.test(char))
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
function isOpeningBracket(char) {
|
|
1244
|
-
return char === "{" || char === "[" || char === "("
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
function isClosingBracket(char) {
|
|
1248
|
-
return char === "}" || char === "]" || char === ")"
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
|
-
function expectedClosingBracket(char) {
|
|
1252
|
-
if (char === "{") return "}"
|
|
1253
|
-
if (char === "[") return "]"
|
|
1254
|
-
return ")"
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
function skipWhitespaceAndComments(source, index) {
|
|
1258
|
-
while (index < source.length) {
|
|
1259
|
-
const next = skipComment(source, index)
|
|
1260
|
-
if (next !== index) {
|
|
1261
|
-
index = next
|
|
1262
|
-
continue
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
if (!isWhitespace(source[index])) return index
|
|
1266
|
-
index += 1
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
return index
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
|
-
function skipWhitespaceCommentsAndCommas(source, index, end) {
|
|
1273
|
-
while (index < end) {
|
|
1274
|
-
const next = skipWhitespaceAndComments(source, index)
|
|
1275
|
-
if (next !== index) {
|
|
1276
|
-
index = next
|
|
1277
|
-
continue
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
if (source[index] !== ",") return index
|
|
1281
|
-
index += 1
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
return index
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
function skipHorizontalWhitespace(source, index) {
|
|
1288
|
-
while (source[index] === " " || source[index] === "\t") {
|
|
1289
|
-
index += 1
|
|
1290
|
-
}
|
|
1291
|
-
return index
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
function isWhitespace(char) {
|
|
1295
|
-
return char === " " || char === "\t" || char === "\n" || char === "\r"
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
function indentMultiline(text, indent) {
|
|
1299
|
-
return text
|
|
1300
|
-
.split("\n")
|
|
1301
|
-
.map((line) => `${indent}${line}`)
|
|
1302
|
-
.join("\n")
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
function getLineIndent(source, index) {
|
|
1306
|
-
let lineStart = index
|
|
1307
|
-
while (lineStart > 0 && source[lineStart - 1] !== "\n") {
|
|
1308
|
-
lineStart -= 1
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
|
-
return source.slice(lineStart, skipHorizontalWhitespace(source, lineStart))
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
function trimEndIndex(source, start, end) {
|
|
1315
|
-
while (end > start && isWhitespace(source[end - 1])) {
|
|
1316
|
-
end -= 1
|
|
1317
|
-
}
|
|
1318
|
-
return end
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
function previousCodeChar(source, index) {
|
|
1322
|
-
index -= 1
|
|
1323
|
-
while (index >= 0 && isWhitespace(source[index])) {
|
|
1324
|
-
index -= 1
|
|
1325
|
-
}
|
|
1326
|
-
return source[index]
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
function bodyIncludesNewline(source, start, end) {
|
|
1330
|
-
for (let index = start; index < end; index += 1) {
|
|
1331
|
-
if (source[index] === "\n" || source[index] === "\r") return true
|
|
1332
|
-
}
|
|
1333
|
-
return false
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
async function getPackageChanges(cwd, dependencies) {
|
|
1337
|
-
const devDependencies = { ...SETUP_DEV_DEPENDENCIES }
|
|
1338
|
-
if (await findViteConfigPath(cwd)) {
|
|
1339
|
-
Object.assign(devDependencies, VITE_DEV_DEPENDENCIES)
|
|
1340
|
-
}
|
|
1341
|
-
|
|
1342
|
-
return {
|
|
1343
|
-
dependencies,
|
|
1344
|
-
devDependencies,
|
|
1345
|
-
}
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
async function updatePackageJson(cwd, changes, options) {
|
|
1349
|
-
const packagePath = path.join(cwd, "package.json")
|
|
1350
|
-
const dependencies = changes.dependencies || {}
|
|
1351
|
-
const devDependencies = changes.devDependencies || {}
|
|
1352
|
-
const packageJson = (await readJsonIfExists(packagePath)) || {
|
|
1353
|
-
name: path.basename(cwd).toLowerCase(),
|
|
1354
|
-
version: "0.0.0",
|
|
1355
|
-
private: true,
|
|
1356
|
-
}
|
|
1357
|
-
const added = [
|
|
1358
|
-
...addPackageSection(packageJson, "dependencies", dependencies),
|
|
1359
|
-
...addPackageSection(packageJson, "devDependencies", devDependencies),
|
|
1360
|
-
]
|
|
1361
|
-
|
|
1362
|
-
if (added.length === 0 && (await exists(packagePath))) {
|
|
1363
|
-
return [`kept ${relative(cwd, packagePath)}`]
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
const status = await writeFile(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`, {
|
|
1367
|
-
dryRun: options.dryRun,
|
|
1368
|
-
force: true,
|
|
1369
|
-
})
|
|
1370
|
-
|
|
1371
|
-
return [
|
|
1372
|
-
`${status} ${relative(cwd, packagePath)}`,
|
|
1373
|
-
]
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
function addPackageSection(packageJson, section, dependencies) {
|
|
1377
|
-
const added = []
|
|
1378
|
-
const existing = packageJson[section] || {}
|
|
1379
|
-
|
|
1380
|
-
for (const [name, version] of Object.entries(dependencies)) {
|
|
1381
|
-
if (hasPackageDependency(packageJson, name)) continue
|
|
1382
|
-
|
|
1383
|
-
existing[name] = version
|
|
1384
|
-
added.push(name)
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
|
-
if (added.length > 0 || packageJson[section]) {
|
|
1388
|
-
packageJson[section] = sortObject(existing)
|
|
1389
|
-
}
|
|
1390
|
-
|
|
1391
|
-
return added
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
function hasPackageDependency(packageJson, name) {
|
|
1395
|
-
return Boolean(
|
|
1396
|
-
packageJson.dependencies?.[name] ||
|
|
1397
|
-
packageJson.devDependencies?.[name] ||
|
|
1398
|
-
packageJson.peerDependencies?.[name] ||
|
|
1399
|
-
packageJson.optionalDependencies?.[name]
|
|
1400
|
-
)
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
|
-
function applyTemplate(source, config) {
|
|
1404
|
-
return source
|
|
1405
|
-
.replaceAll("@/lib/utils", config.aliases.utils)
|
|
1406
|
-
.replaceAll("@/hooks", config.aliases.hooks)
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
function isTextRegistryFile(source) {
|
|
1410
|
-
return !/\.(avif|eot|gif|ico|jpe?g|otf|png|ttf|webp|woff2?)$/i.test(source)
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
function resolveTarget(cwd, config, target) {
|
|
1414
|
-
const replacements = {
|
|
1415
|
-
"{{ui}}": aliasToPath(cwd, config.aliases.ui),
|
|
1416
|
-
"{{utils}}": stripExtension(aliasToPath(cwd, config.aliases.utils)),
|
|
1417
|
-
"{{hooks}}": aliasToPath(cwd, config.aliases.hooks),
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
|
-
let resolved = target
|
|
1421
|
-
for (const [placeholder, value] of Object.entries(replacements)) {
|
|
1422
|
-
resolved = resolved.replaceAll(placeholder, value)
|
|
1423
|
-
}
|
|
1424
|
-
|
|
1425
|
-
return resolved
|
|
1426
|
-
}
|
|
1427
|
-
|
|
1428
|
-
function aliasToPath(cwd, alias) {
|
|
1429
|
-
if (!alias) return ""
|
|
1430
|
-
if (path.isAbsolute(alias)) return alias
|
|
1431
|
-
if (alias.startsWith("./")) return alias.slice(2)
|
|
1432
|
-
if (alias.startsWith("../")) return alias
|
|
1433
|
-
|
|
1434
|
-
if (alias.startsWith("@/") || alias.startsWith("~/")) {
|
|
1435
|
-
const rest = alias.slice(2)
|
|
1436
|
-
return path.join(shouldUseSrcDirectory(cwd) ? "src" : "", rest)
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
return alias
|
|
1440
|
-
}
|
|
1441
|
-
|
|
1442
|
-
function shouldUseSrcDirectory(cwd) {
|
|
1443
|
-
return pathExistsSyncHint(cwd, "src/app") || pathExistsSyncHint(cwd, "src")
|
|
1444
|
-
}
|
|
1445
|
-
|
|
1446
|
-
function pathExistsSyncHint(cwd, segment) {
|
|
1447
|
-
return Boolean(syncExistsCache.get(path.join(cwd, segment)))
|
|
1448
|
-
}
|
|
1449
|
-
|
|
1450
|
-
async function warmPathHints(cwd) {
|
|
1451
|
-
for (const segment of ["src", "src/app"]) {
|
|
1452
|
-
const target = path.join(cwd, segment)
|
|
1453
|
-
syncExistsCache.set(target, await exists(target))
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
async function writeFile(targetPath, content, options) {
|
|
1458
|
-
const alreadyExists = await exists(targetPath)
|
|
1459
|
-
const isBinary = Buffer.isBuffer(content)
|
|
1460
|
-
|
|
1461
|
-
if (options.dryRun) {
|
|
1462
|
-
if (!alreadyExists) return "would write"
|
|
1463
|
-
|
|
1464
|
-
const current = await fs.readFile(targetPath)
|
|
1465
|
-
const isEqual = isBinary
|
|
1466
|
-
? current.equals(content)
|
|
1467
|
-
: current.toString("utf8") === content
|
|
1468
|
-
if (isEqual) return "would keep"
|
|
1469
|
-
|
|
1470
|
-
return "would update"
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
|
-
if (alreadyExists && !options.force) {
|
|
1474
|
-
const current = await fs.readFile(targetPath)
|
|
1475
|
-
const isEqual = isBinary
|
|
1476
|
-
? current.equals(content)
|
|
1477
|
-
: current.toString("utf8") === content
|
|
1478
|
-
if (isEqual) return "kept"
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
|
-
await fs.mkdir(path.dirname(targetPath), { recursive: true })
|
|
1482
|
-
await fs.writeFile(targetPath, content, isBinary ? undefined : "utf8")
|
|
1483
|
-
return alreadyExists ? "updated" : "wrote"
|
|
1484
|
-
}
|
|
1485
|
-
|
|
1486
|
-
async function readJson(filePath) {
|
|
1487
|
-
return JSON.parse(await fs.readFile(filePath, "utf8"))
|
|
1488
|
-
}
|
|
1489
|
-
|
|
1490
|
-
async function readJsonc(filePath) {
|
|
1491
|
-
const content = await fs.readFile(filePath, "utf8")
|
|
1492
|
-
return JSON.parse(stripJsonComments(content))
|
|
1493
|
-
}
|
|
1494
|
-
|
|
1495
|
-
function stripJsonComments(content) {
|
|
1496
|
-
let output = ""
|
|
1497
|
-
let inString = false
|
|
1498
|
-
let escaped = false
|
|
1499
|
-
|
|
1500
|
-
for (let index = 0; index < content.length; index += 1) {
|
|
1501
|
-
const char = content[index]
|
|
1502
|
-
const next = content[index + 1]
|
|
1503
|
-
|
|
1504
|
-
if (inString) {
|
|
1505
|
-
output += char
|
|
1506
|
-
|
|
1507
|
-
if (escaped) {
|
|
1508
|
-
escaped = false
|
|
1509
|
-
} else if (char === "\\") {
|
|
1510
|
-
escaped = true
|
|
1511
|
-
} else if (char === "\"") {
|
|
1512
|
-
inString = false
|
|
1513
|
-
}
|
|
1514
|
-
|
|
1515
|
-
continue
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
|
-
if (char === "\"") {
|
|
1519
|
-
inString = true
|
|
1520
|
-
output += char
|
|
1521
|
-
continue
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
|
-
if (char === "/" && next === "/") {
|
|
1525
|
-
output += " "
|
|
1526
|
-
index += 2
|
|
1527
|
-
|
|
1528
|
-
while (index < content.length && content[index] !== "\n" && content[index] !== "\r") {
|
|
1529
|
-
output += " "
|
|
1530
|
-
index += 1
|
|
1531
|
-
}
|
|
1532
|
-
|
|
1533
|
-
if (index < content.length) {
|
|
1534
|
-
output += content[index]
|
|
1535
|
-
if (content[index] === "\r" && content[index + 1] === "\n") {
|
|
1536
|
-
index += 1
|
|
1537
|
-
output += content[index]
|
|
1538
|
-
}
|
|
1539
|
-
}
|
|
1540
|
-
|
|
1541
|
-
continue
|
|
1542
|
-
}
|
|
1543
|
-
|
|
1544
|
-
if (char === "/" && next === "*") {
|
|
1545
|
-
output += " "
|
|
1546
|
-
index += 2
|
|
1547
|
-
|
|
1548
|
-
while (index < content.length) {
|
|
1549
|
-
if (content[index] === "*" && content[index + 1] === "/") {
|
|
1550
|
-
output += " "
|
|
1551
|
-
index += 1
|
|
1552
|
-
break
|
|
1553
|
-
}
|
|
1554
|
-
|
|
1555
|
-
output += content[index] === "\n" || content[index] === "\r" ? content[index] : " "
|
|
1556
|
-
index += 1
|
|
1557
|
-
}
|
|
1558
|
-
|
|
1559
|
-
continue
|
|
1560
|
-
}
|
|
1561
|
-
|
|
1562
|
-
output += char
|
|
1563
|
-
}
|
|
1564
|
-
|
|
1565
|
-
return output
|
|
1566
|
-
}
|
|
1567
|
-
|
|
1568
|
-
async function readJsonIfExists(filePath) {
|
|
1569
|
-
try {
|
|
1570
|
-
return await readJson(filePath)
|
|
1571
|
-
} catch (error) {
|
|
1572
|
-
if (error.code === "ENOENT") return null
|
|
1573
|
-
throw error
|
|
1574
|
-
}
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
async function readTextIfExists(filePath) {
|
|
1578
|
-
try {
|
|
1579
|
-
return await fs.readFile(filePath, "utf8")
|
|
1580
|
-
} catch (error) {
|
|
1581
|
-
if (error.code === "ENOENT") return null
|
|
1582
|
-
throw error
|
|
1583
|
-
}
|
|
1584
|
-
}
|
|
1585
|
-
|
|
1586
|
-
async function exists(filePath) {
|
|
1587
|
-
try {
|
|
1588
|
-
await fs.access(filePath)
|
|
1589
|
-
return true
|
|
1590
|
-
} catch {
|
|
1591
|
-
return false
|
|
1592
|
-
}
|
|
1593
|
-
}
|
|
1594
|
-
|
|
1595
|
-
function removeExistingTokenBlock(content, fileLabel = "CSS file") {
|
|
1596
|
-
const lines = splitLinesPreservingEndings(content)
|
|
1597
|
-
const kept = []
|
|
1598
|
-
let found = false
|
|
1599
|
-
let inTokenBlock = false
|
|
1600
|
-
|
|
1601
|
-
for (const line of lines) {
|
|
1602
|
-
const trimmed = line.trimStart()
|
|
1603
|
-
|
|
1604
|
-
if (trimmed.startsWith(CSS_START)) {
|
|
1605
|
-
if (found || inTokenBlock) {
|
|
1606
|
-
throw new Error(`${fileLabel}: multiple Banhaten token blocks found.`)
|
|
1607
|
-
}
|
|
1608
|
-
found = true
|
|
1609
|
-
inTokenBlock = true
|
|
1610
|
-
continue
|
|
1611
|
-
}
|
|
1612
|
-
|
|
1613
|
-
if (trimmed.startsWith(CSS_END)) {
|
|
1614
|
-
if (!inTokenBlock) {
|
|
1615
|
-
throw new Error(`${fileLabel}: Banhaten token end marker has no start marker.`)
|
|
1616
|
-
}
|
|
1617
|
-
inTokenBlock = false
|
|
1618
|
-
continue
|
|
1619
|
-
}
|
|
1620
|
-
|
|
1621
|
-
if (!inTokenBlock) {
|
|
1622
|
-
kept.push(line)
|
|
1623
|
-
}
|
|
1624
|
-
}
|
|
1625
|
-
|
|
1626
|
-
if (inTokenBlock) {
|
|
1627
|
-
throw new Error(`${fileLabel}: Banhaten token block is missing its end marker.`)
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
|
-
return { content: kept.join(""), found }
|
|
1631
|
-
}
|
|
1632
|
-
|
|
1633
|
-
function ensureGlobalCssImports(content) {
|
|
1634
|
-
const cleaned = splitLinesPreservingEndings(content)
|
|
1635
|
-
.filter((line) => {
|
|
1636
|
-
const importSpecifier = readCssImportSpecifier(line)
|
|
1637
|
-
return (
|
|
1638
|
-
importSpecifier !== "tailwindcss" &&
|
|
1639
|
-
!isManagedInterFontImport(importSpecifier)
|
|
1640
|
-
)
|
|
1641
|
-
})
|
|
1642
|
-
.join("")
|
|
1643
|
-
.trimStart()
|
|
1644
|
-
return `${TAILWIND_IMPORT}\n${FONT_IMPORTS.join("\n")}\n${cleaned}`
|
|
1645
|
-
}
|
|
1646
|
-
|
|
1647
|
-
function splitLinesPreservingEndings(content) {
|
|
1648
|
-
const lines = []
|
|
1649
|
-
let lineStart = 0
|
|
1650
|
-
let index = 0
|
|
1651
|
-
|
|
1652
|
-
while (index < content.length) {
|
|
1653
|
-
if (content[index] === "\r" && content[index + 1] === "\n") {
|
|
1654
|
-
lines.push(content.slice(lineStart, index + 2))
|
|
1655
|
-
index += 2
|
|
1656
|
-
lineStart = index
|
|
1657
|
-
continue
|
|
1658
|
-
}
|
|
1659
|
-
|
|
1660
|
-
if (content[index] === "\n") {
|
|
1661
|
-
lines.push(content.slice(lineStart, index + 1))
|
|
1662
|
-
index += 1
|
|
1663
|
-
lineStart = index
|
|
1664
|
-
continue
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
index += 1
|
|
1668
|
-
}
|
|
1669
|
-
|
|
1670
|
-
if (lineStart < content.length) {
|
|
1671
|
-
lines.push(content.slice(lineStart))
|
|
1672
|
-
}
|
|
1673
|
-
|
|
1674
|
-
return lines
|
|
1675
|
-
}
|
|
1676
|
-
|
|
1677
|
-
function readCssImportSpecifier(line) {
|
|
1678
|
-
const trimmed = line.trim()
|
|
1679
|
-
const importPrefix = "@import"
|
|
1680
|
-
if (!trimmed.startsWith(importPrefix)) return null
|
|
1681
|
-
|
|
1682
|
-
let index = importPrefix.length
|
|
1683
|
-
while (trimmed[index] === " " || trimmed[index] === "\t") {
|
|
1684
|
-
index += 1
|
|
1685
|
-
}
|
|
1686
|
-
|
|
1687
|
-
const quote = trimmed[index]
|
|
1688
|
-
if (quote !== "\"" && quote !== "'") return null
|
|
1689
|
-
|
|
1690
|
-
const start = index + 1
|
|
1691
|
-
index = start
|
|
1692
|
-
while (index < trimmed.length && trimmed[index] !== quote) {
|
|
1693
|
-
index += 1
|
|
1694
|
-
}
|
|
1695
|
-
|
|
1696
|
-
if (index >= trimmed.length) return null
|
|
1697
|
-
const specifier = trimmed.slice(start, index)
|
|
1698
|
-
index += 1
|
|
1699
|
-
|
|
1700
|
-
while (trimmed[index] === " " || trimmed[index] === "\t") {
|
|
1701
|
-
index += 1
|
|
1702
|
-
}
|
|
1703
|
-
|
|
1704
|
-
if (trimmed[index] === ";") index += 1
|
|
1705
|
-
|
|
1706
|
-
while (trimmed[index] === " " || trimmed[index] === "\t") {
|
|
1707
|
-
index += 1
|
|
1708
|
-
}
|
|
1709
|
-
|
|
1710
|
-
return index === trimmed.length ? specifier : null
|
|
1711
|
-
}
|
|
1712
|
-
|
|
1713
|
-
function isManagedInterFontImport(specifier) {
|
|
1714
|
-
if (!specifier) return false
|
|
1715
|
-
if (specifier === "@fontsource/inter") return true
|
|
1716
|
-
if (specifier === "@fontsource-variable/inter") return true
|
|
1717
|
-
|
|
1718
|
-
for (const weight of ["400", "500", "600", "700"]) {
|
|
1719
|
-
if (specifier === `@fontsource/inter/${weight}.css`) return true
|
|
1720
|
-
}
|
|
1721
|
-
|
|
1722
|
-
return false
|
|
1723
994
|
}
|
|
1724
995
|
|
|
1725
996
|
function appendAliasSegment(alias, segment) {
|
|
@@ -1727,10 +998,6 @@ function appendAliasSegment(alias, segment) {
|
|
|
1727
998
|
return `${alias.replace(/\/$/, "")}/${segment}`
|
|
1728
999
|
}
|
|
1729
1000
|
|
|
1730
|
-
function stripExtension(filePath) {
|
|
1731
|
-
return filePath.replace(/\.(t|j)sx?$/, "")
|
|
1732
|
-
}
|
|
1733
|
-
|
|
1734
1001
|
function sortObject(value) {
|
|
1735
1002
|
return Object.fromEntries(
|
|
1736
1003
|
Object.entries(value).sort(([left], [right]) => left.localeCompare(right))
|
|
@@ -1740,22 +1007,3 @@ function sortObject(value) {
|
|
|
1740
1007
|
function relative(cwd, targetPath) {
|
|
1741
1008
|
return path.relative(cwd, targetPath).replaceAll("\\", "/")
|
|
1742
1009
|
}
|
|
1743
|
-
|
|
1744
|
-
function printResult(title, writes, dryRun) {
|
|
1745
|
-
console.log(`${dryRun ? "Dry run: " : ""}${title}`)
|
|
1746
|
-
for (const write of writes) {
|
|
1747
|
-
console.log(`- ${write}`)
|
|
1748
|
-
}
|
|
1749
|
-
}
|
|
1750
|
-
|
|
1751
|
-
function printInstallHint(writes, dryRun) {
|
|
1752
|
-
if (dryRun) return
|
|
1753
|
-
|
|
1754
|
-
const packageJsonChanged = writes.some((write) =>
|
|
1755
|
-
/^(wrote|updated) package\.json$/.test(write)
|
|
1756
|
-
)
|
|
1757
|
-
|
|
1758
|
-
if (packageJsonChanged) {
|
|
1759
|
-
console.log("\nNext: run npm install to install the added dependencies.")
|
|
1760
|
-
}
|
|
1761
|
-
}
|