banhaten 0.1.3 → 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 +65 -13
- package/SECURITY.md +15 -0
- package/banhaten.config.example.json +0 -2
- package/docs/design-system/README.md +9 -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 +6 -1
- package/docs/design-system/component-docs-pattern.md +81 -0
- package/docs/design-system/component-token-consistency-audit.md +13 -3
- package/docs/design-system/components/README.md +3 -2
- package/docs/design-system/components/accordion.md +16 -0
- package/docs/design-system/components/activity-feed.md +15 -1
- package/docs/design-system/components/alert-dialog.md +17 -0
- package/docs/design-system/components/alert.md +46 -14
- package/docs/design-system/components/aspect-ratio.md +15 -0
- package/docs/design-system/components/attribute.md +16 -0
- package/docs/design-system/components/autocomplete.md +19 -0
- package/docs/design-system/components/avatar.md +18 -0
- package/docs/design-system/components/badge.md +19 -0
- package/docs/design-system/components/banner.md +15 -1
- package/docs/design-system/components/breadcrumbs.md +14 -0
- package/docs/design-system/components/button-group.md +34 -0
- package/docs/design-system/components/button.md +18 -0
- package/docs/design-system/components/card.md +18 -1
- package/docs/design-system/components/carousel.md +20 -3
- package/docs/design-system/components/catalog-components.md +15 -1
- package/docs/design-system/components/chart.md +15 -0
- package/docs/design-system/components/checkbox.md +16 -0
- package/docs/design-system/components/collapsible.md +15 -0
- package/docs/design-system/components/command.md +15 -0
- package/docs/design-system/components/context-menu.md +15 -0
- package/docs/design-system/components/date-picker.md +21 -1
- package/docs/design-system/components/divider.md +16 -0
- package/docs/design-system/components/empty-state.md +17 -0
- package/docs/design-system/components/field.md +17 -0
- package/docs/design-system/components/file-upload.md +14 -0
- package/docs/design-system/components/hover-card.md +15 -0
- package/docs/design-system/components/input-group.md +17 -0
- package/docs/design-system/components/input-otp.md +18 -0
- package/docs/design-system/components/input.md +43 -7
- package/docs/design-system/components/item.md +72 -0
- package/docs/design-system/components/kbd.md +18 -0
- package/docs/design-system/components/label.md +16 -0
- package/docs/design-system/components/menu.md +17 -0
- package/docs/design-system/components/menubar.md +15 -0
- package/docs/design-system/components/modal.md +25 -4
- package/docs/design-system/components/native-select.md +18 -0
- package/docs/design-system/components/navigation-menu.md +15 -0
- package/docs/design-system/components/onboarding-step-list-item.md +15 -1
- package/docs/design-system/components/page-header.md +15 -1
- package/docs/design-system/components/pagination.md +30 -8
- package/docs/design-system/components/popover.md +19 -0
- package/docs/design-system/components/progress-slider.md +18 -0
- package/docs/design-system/components/progress.md +17 -0
- package/docs/design-system/components/radio-card.md +16 -0
- package/docs/design-system/components/radio-group.md +17 -0
- package/docs/design-system/components/resizable.md +15 -0
- package/docs/design-system/components/scroll-area.md +15 -0
- package/docs/design-system/components/segmented-control.md +71 -0
- package/docs/design-system/components/select.md +44 -7
- package/docs/design-system/components/sheet.md +17 -0
- package/docs/design-system/components/sidebar.md +15 -0
- package/docs/design-system/components/skeleton.md +18 -0
- package/docs/design-system/components/slideout.md +14 -0
- package/docs/design-system/components/slider.md +20 -0
- package/docs/design-system/components/social-button.md +21 -3
- package/docs/design-system/components/spinner.md +16 -0
- package/docs/design-system/components/steps.md +14 -0
- package/docs/design-system/components/table.md +15 -1
- package/docs/design-system/components/tabs.md +17 -0
- package/docs/design-system/components/tag.md +17 -0
- package/docs/design-system/components/textarea.md +16 -0
- package/docs/design-system/components/timeline.md +14 -0
- package/docs/design-system/components/toast.md +17 -0
- package/docs/design-system/components/toggle.md +17 -0
- package/docs/design-system/components/toolbar.md +21 -1
- package/docs/design-system/components/tooltip.md +18 -1
- package/docs/design-system/density-and-elevation.md +44 -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/installation.md +68 -0
- package/docs/design-system/launch-comparison-shadcn.md +98 -0
- package/docs/design-system/registry-and-cli.md +30 -7
- 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 +1 -1
- package/docs/design-system/token-governance.md +23 -1
- package/package.json +29 -15
- package/registry/component-contract.json +40 -0
- package/registry/components/alert-dialog.tsx +1 -1
- package/registry/components/alert.tsx +86 -65
- 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 +53 -5
- package/registry/components/chart.tsx +36 -3
- package/registry/components/checkbox.tsx +3 -3
- package/registry/components/collapsible.tsx +1 -1
- package/registry/components/command.tsx +1 -1
- package/registry/components/context-menu.tsx +1 -1
- package/registry/components/date-picker.tsx +245 -104
- 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 +13 -13
- 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 +1 -1
- 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 +3 -3
- package/registry/components/expanded/table.css +2 -2
- package/registry/components/expanded/timeline.css +2 -2
- package/registry/components/input-content.ts +1 -0
- package/registry/components/input-otp.tsx +13 -3
- package/registry/components/input.tsx +68 -43
- package/registry/components/item.tsx +189 -0
- package/registry/components/kbd.tsx +111 -4
- package/registry/components/menu.tsx +31 -245
- package/registry/components/menubar.tsx +1 -1
- package/registry/components/modal.tsx +142 -102
- package/registry/components/native-select.tsx +2 -2
- 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/segmented-control.tsx +87 -0
- package/registry/components/select-content.tsx +2 -2
- package/registry/components/select.tsx +52 -16
- package/registry/components/sheet.tsx +1 -1
- package/registry/components/sidebar.tsx +1 -1
- package/registry/components/skeleton.css +0 -3
- package/registry/components/social-button.tsx +47 -25
- package/registry/components/spinner.tsx +18 -55
- package/registry/components/tabs.css +1 -1
- package/registry/components/tabs.tsx +8 -4
- package/registry/components/tag.tsx +2 -2
- package/registry/components/textarea.tsx +3 -3
- package/registry/components/toast.tsx +16 -11
- 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/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 +20 -2
- package/registry/examples/chart-demo.tsx +1 -1
- package/registry/examples/checkbox-demo.tsx +5 -5
- package/registry/examples/collection-filter-bar-demo.tsx +135 -0
- package/registry/examples/command-demo.tsx +3 -3
- package/registry/examples/{expanded/command-bar-demo.tsx → command-palette-demo.tsx} +5 -15
- package/registry/examples/context-menu-demo.tsx +1 -1
- 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 +1 -1
- package/registry/examples/expanded/timeline-demo.tsx +1 -1
- package/registry/examples/hover-card-demo.tsx +2 -2
- package/registry/examples/input-demo.tsx +6 -6
- package/registry/examples/input-otp-demo.tsx +4 -4
- package/registry/examples/item-demo.tsx +46 -0
- package/registry/examples/kbd-demo.tsx +1 -1
- package/registry/examples/label-demo.tsx +1 -1
- package/registry/examples/menu-demo.tsx +1 -1
- package/registry/examples/modal-demo.tsx +67 -9
- package/registry/examples/navigation-menu-demo.tsx +1 -1
- 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 +1 -1
- package/registry/examples/results-toolbar-demo.tsx +44 -0
- package/registry/examples/scroll-area-demo.tsx +3 -3
- package/registry/examples/segmented-control-demo.tsx +39 -0
- package/registry/examples/select-demo.tsx +1 -1
- package/registry/examples/sidebar-demo.tsx +1 -1
- 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 +1 -1
- package/registry/examples/tag-demo.tsx +1 -1
- package/registry/examples/textarea-demo.tsx +4 -4
- package/registry/examples/toast-demo.tsx +1 -1
- 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 +5311 -377
- package/registry/styles/globals.css +2440 -4061
- package/registry.json +119 -149
- package/schema/config.schema.json +11 -8
- package/schema/registry.schema.json +150 -45
- package/schema/tokens.schema.json +134 -34
- 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 +567 -1613
- 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 +4312 -309
- package/docs/design-system/components/command-bar.md +0 -57
- 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/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)),
|
|
@@ -16,23 +61,13 @@ const packageJsonPath = path.join(packageRoot, "package.json")
|
|
|
16
61
|
const schemaRoot = path.join(packageRoot, "schema")
|
|
17
62
|
|
|
18
63
|
const CONFIG_FILE = "banhaten.config.json"
|
|
19
|
-
const
|
|
20
|
-
'@import "@fontsource/inter/400.css";',
|
|
21
|
-
'@import "@fontsource/inter/500.css";',
|
|
22
|
-
'@import "@fontsource/inter/600.css";',
|
|
23
|
-
'@import "@fontsource/inter/700.css";',
|
|
24
|
-
]
|
|
25
|
-
const TAILWIND_IMPORT = '@import "tailwindcss";'
|
|
64
|
+
const INSTALL_RECEIPT_FILE = ".banhaten/installed.json"
|
|
26
65
|
const SETUP_DEV_DEPENDENCIES = {
|
|
27
66
|
tailwindcss: "^4.0.0",
|
|
28
67
|
}
|
|
29
68
|
const VITE_DEV_DEPENDENCIES = {
|
|
30
69
|
"@tailwindcss/vite": "^4.0.0",
|
|
31
70
|
}
|
|
32
|
-
const CSS_START = "/* Banhaten design system tokens:"
|
|
33
|
-
const CSS_END = "/* End Banhaten design system tokens. */"
|
|
34
|
-
const syncExistsCache = new Map()
|
|
35
|
-
const VALUE_FLAGS = new Set(["cwd", "file", "format"])
|
|
36
71
|
let runtimeOptions = {
|
|
37
72
|
json: false,
|
|
38
73
|
silent: false,
|
|
@@ -40,8 +75,6 @@ let runtimeOptions = {
|
|
|
40
75
|
|
|
41
76
|
const DEFAULT_CONFIG = {
|
|
42
77
|
$schema: ".banhaten/schema/config.schema.json",
|
|
43
|
-
style: "default",
|
|
44
|
-
tsx: true,
|
|
45
78
|
tailwind: {
|
|
46
79
|
css: "app/globals.css",
|
|
47
80
|
},
|
|
@@ -52,8 +85,28 @@ const DEFAULT_CONFIG = {
|
|
|
52
85
|
},
|
|
53
86
|
}
|
|
54
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
|
+
|
|
55
105
|
const registry = await readJson(registryIndexPath)
|
|
56
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")
|
|
57
110
|
|
|
58
111
|
main().catch((error) => {
|
|
59
112
|
console.error(`banhaten: ${error.message}`)
|
|
@@ -61,131 +114,94 @@ main().catch((error) => {
|
|
|
61
114
|
})
|
|
62
115
|
|
|
63
116
|
async function main() {
|
|
64
|
-
const { command, positionals, flags } =
|
|
117
|
+
const { command, positionals, flags } = parseCommandLine(process.argv.slice(2))
|
|
65
118
|
runtimeOptions = {
|
|
66
119
|
json: Boolean(flags.json),
|
|
67
|
-
silent: Boolean(flags.silent
|
|
120
|
+
silent: Boolean(flags.silent),
|
|
68
121
|
}
|
|
69
122
|
|
|
70
|
-
if (command === "version" || flags.version
|
|
123
|
+
if (command === "version" || flags.version) {
|
|
71
124
|
console.log(packageManifest.version)
|
|
72
125
|
return
|
|
73
126
|
}
|
|
74
127
|
|
|
75
|
-
if (!command || command === "help" || flags.help
|
|
128
|
+
if (!command || command === "help" || flags.help) {
|
|
76
129
|
printHelp()
|
|
77
130
|
return
|
|
78
131
|
}
|
|
79
132
|
|
|
80
133
|
if (command === "list") {
|
|
81
|
-
listComponents(
|
|
134
|
+
listComponents(registry, runtimeOptions)
|
|
82
135
|
return
|
|
83
136
|
}
|
|
84
137
|
|
|
85
138
|
if (command === "search") {
|
|
86
|
-
searchComponents(positionals,
|
|
139
|
+
searchComponents(registry, positionals, runtimeOptions)
|
|
87
140
|
return
|
|
88
141
|
}
|
|
89
142
|
|
|
90
143
|
if (command === "docs") {
|
|
91
|
-
showComponentDocs(positionals,
|
|
144
|
+
await showComponentDocs(registry, packageRoot, positionals, runtimeOptions)
|
|
92
145
|
return
|
|
93
146
|
}
|
|
94
147
|
|
|
95
148
|
if (command === "view") {
|
|
96
|
-
await viewComponent(
|
|
149
|
+
await viewComponent(
|
|
150
|
+
registry,
|
|
151
|
+
registryRoot,
|
|
152
|
+
positionals,
|
|
153
|
+
flags,
|
|
154
|
+
runtimeOptions
|
|
155
|
+
)
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
|
|
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
|
|
97
166
|
return
|
|
98
167
|
}
|
|
99
168
|
|
|
100
169
|
if (command === "init") {
|
|
101
|
-
await initProject(flags)
|
|
170
|
+
await runFileTransaction(() => initProject(flags))
|
|
102
171
|
return
|
|
103
172
|
}
|
|
104
173
|
|
|
105
174
|
if (command === "add") {
|
|
106
|
-
await addComponents(positionals, flags)
|
|
175
|
+
await runFileTransaction(() => addComponents(positionals, flags))
|
|
107
176
|
return
|
|
108
177
|
}
|
|
109
178
|
|
|
110
179
|
if (command === "diff") {
|
|
111
|
-
await diffComponents(positionals, flags)
|
|
180
|
+
await runFileTransaction(() => diffComponents(positionals, flags))
|
|
112
181
|
return
|
|
113
182
|
}
|
|
114
183
|
|
|
115
184
|
if (command === "update") {
|
|
116
|
-
await updateComponents(positionals, flags)
|
|
185
|
+
await runFileTransaction(() => updateComponents(positionals, flags))
|
|
117
186
|
return
|
|
118
187
|
}
|
|
119
188
|
|
|
120
189
|
throw new Error(`unknown command "${command}"`)
|
|
121
190
|
}
|
|
122
191
|
|
|
123
|
-
function parseArgs(argv) {
|
|
124
|
-
const flags = {}
|
|
125
|
-
const positionals = []
|
|
126
|
-
|
|
127
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
128
|
-
const arg = argv[index]
|
|
129
|
-
|
|
130
|
-
if (arg === "-h") {
|
|
131
|
-
flags.h = true
|
|
132
|
-
continue
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (arg === "-s") {
|
|
136
|
-
flags.s = true
|
|
137
|
-
continue
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (arg === "-v") {
|
|
141
|
-
flags.v = true
|
|
142
|
-
continue
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (arg === "-y") {
|
|
146
|
-
flags.yes = true
|
|
147
|
-
continue
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (!arg.startsWith("--")) {
|
|
151
|
-
positionals.push(arg)
|
|
152
|
-
continue
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const [rawKey, inlineValue] = arg.slice(2).split("=")
|
|
156
|
-
const key = rawKey.trim()
|
|
157
|
-
|
|
158
|
-
if (inlineValue !== undefined) {
|
|
159
|
-
flags[key] = inlineValue
|
|
160
|
-
continue
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const next = argv[index + 1]
|
|
164
|
-
if (VALUE_FLAGS.has(key) && next && !next.startsWith("-")) {
|
|
165
|
-
flags[key] = next
|
|
166
|
-
index += 1
|
|
167
|
-
continue
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
flags[key] = true
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const [command, ...rest] = positionals
|
|
174
|
-
return { command, positionals: rest, flags }
|
|
175
|
-
}
|
|
176
|
-
|
|
177
192
|
function printHelp() {
|
|
178
193
|
console.log(`Banhaten design system
|
|
179
194
|
|
|
180
195
|
Usage:
|
|
181
|
-
banhaten init [--cwd <path>] [--force] [--dry-run]
|
|
182
|
-
banhaten add <component...> [--cwd <path>] [--force] [--dry-run]
|
|
183
|
-
banhaten diff [component...] [--cwd <path>] [--
|
|
184
|
-
banhaten update [component...] [--cwd <path>] [--force] [--dry-run]
|
|
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]
|
|
185
200
|
banhaten list [--json]
|
|
186
201
|
banhaten search <query> [--json]
|
|
187
202
|
banhaten docs <component> [--json]
|
|
188
203
|
banhaten view <component> [--file <registry-source>] [--json]
|
|
204
|
+
banhaten doctor [--cwd <path>] [--json]
|
|
189
205
|
banhaten version
|
|
190
206
|
|
|
191
207
|
Examples:
|
|
@@ -196,209 +212,67 @@ Examples:
|
|
|
196
212
|
npx banhaten search input
|
|
197
213
|
npx banhaten docs button
|
|
198
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.
|
|
199
222
|
|
|
200
223
|
Global flags:
|
|
201
|
-
--
|
|
202
|
-
--dry-run Preview writes without changing files.
|
|
203
|
-
--force Rewrite generated files even when already current.
|
|
204
|
-
--json Print machine-readable output where supported.
|
|
224
|
+
--help, -h Print help.
|
|
205
225
|
--silent, -s Suppress normal command output.
|
|
206
226
|
--yes, -y Accepted for non-interactive scripts.
|
|
207
227
|
--version, -v Print the installed CLI version.
|
|
208
228
|
`)
|
|
209
229
|
}
|
|
210
230
|
|
|
211
|
-
function listComponents() {
|
|
212
|
-
const components = Object.entries(registry.components).map(([name, component]) => ({
|
|
213
|
-
name,
|
|
214
|
-
description: component.description || "",
|
|
215
|
-
dependencies: Object.keys(component.dependencies || {}),
|
|
216
|
-
registryDependencies: component.registryDependencies || [],
|
|
217
|
-
}))
|
|
218
|
-
|
|
219
|
-
if (runtimeOptions.json) {
|
|
220
|
-
writeJson({ components })
|
|
221
|
-
return
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
if (runtimeOptions.silent) return
|
|
225
|
-
|
|
226
|
-
console.log("Available Banhaten design system components:")
|
|
227
|
-
for (const { name, description } of components) {
|
|
228
|
-
console.log(`- ${name}: ${description}`)
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function searchComponents(positionals) {
|
|
233
|
-
const query = positionals.join(" ").trim().toLowerCase()
|
|
234
|
-
if (!query) {
|
|
235
|
-
throw new Error("missing search query. Try `banhaten search input`.")
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const matches = Object.entries(registry.components)
|
|
239
|
-
.filter(([name, component]) => {
|
|
240
|
-
const haystack = [
|
|
241
|
-
name,
|
|
242
|
-
component.description,
|
|
243
|
-
...(component.parts || []),
|
|
244
|
-
...(component.registryDependencies || []),
|
|
245
|
-
]
|
|
246
|
-
.filter(Boolean)
|
|
247
|
-
.join(" ")
|
|
248
|
-
.toLowerCase()
|
|
249
|
-
|
|
250
|
-
return haystack.includes(query)
|
|
251
|
-
})
|
|
252
|
-
.map(([name, component]) => ({
|
|
253
|
-
name,
|
|
254
|
-
description: component.description || "",
|
|
255
|
-
parts: component.parts || [],
|
|
256
|
-
}))
|
|
257
|
-
|
|
258
|
-
if (runtimeOptions.json) {
|
|
259
|
-
writeJson({ query, matches })
|
|
260
|
-
return
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
if (runtimeOptions.silent) return
|
|
264
|
-
|
|
265
|
-
if (matches.length === 0) {
|
|
266
|
-
console.log(`No Banhaten components matched "${query}".`)
|
|
267
|
-
return
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
console.log(`Banhaten components matching "${query}":`)
|
|
271
|
-
for (const match of matches) {
|
|
272
|
-
console.log(`- ${match.name}: ${match.description}`)
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function showComponentDocs(positionals) {
|
|
277
|
-
const [name] = positionals
|
|
278
|
-
const component = getComponentOrThrow(name)
|
|
279
|
-
const docsPath = getComponentDocPath(name)
|
|
280
|
-
const result = {
|
|
281
|
-
name,
|
|
282
|
-
description: component.description || "",
|
|
283
|
-
install: `npx banhaten add ${name}`,
|
|
284
|
-
docsPath,
|
|
285
|
-
files: component.files || [],
|
|
286
|
-
examples: component.examples || [],
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
if (runtimeOptions.json) {
|
|
290
|
-
writeJson(result)
|
|
291
|
-
return
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
if (runtimeOptions.silent) return
|
|
295
|
-
|
|
296
|
-
console.log(`${name}: ${result.description}`)
|
|
297
|
-
console.log(`Install: ${result.install}`)
|
|
298
|
-
console.log(`Docs: ${docsPath}`)
|
|
299
|
-
|
|
300
|
-
if (result.files.length > 0) {
|
|
301
|
-
console.log("Files:")
|
|
302
|
-
for (const file of result.files) {
|
|
303
|
-
console.log(`- ${file.source} -> ${file.target}`)
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
if (result.examples.length > 0) {
|
|
308
|
-
console.log("Examples:")
|
|
309
|
-
for (const example of result.examples) {
|
|
310
|
-
console.log(`- ${example}`)
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
async function viewComponent(positionals, flags) {
|
|
316
|
-
const [name] = positionals
|
|
317
|
-
const component = getComponentOrThrow(name)
|
|
318
|
-
const files = component.files || []
|
|
319
|
-
const requestedFile = typeof flags.file === "string" ? flags.file : null
|
|
320
|
-
const file =
|
|
321
|
-
(requestedFile
|
|
322
|
-
? files.find((entry) => entry.source === requestedFile)
|
|
323
|
-
: files.find((entry) => /\.(ts|tsx|css)$/.test(entry.source)) || files[0]) ||
|
|
324
|
-
null
|
|
325
|
-
|
|
326
|
-
if (!file) {
|
|
327
|
-
throw new Error(`component "${name}" has no viewable registry files.`)
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const sourcePath = path.join(registryRoot, file.source)
|
|
331
|
-
const content = await fs.readFile(
|
|
332
|
-
sourcePath,
|
|
333
|
-
isTextRegistryFile(file.source) ? "utf8" : undefined
|
|
334
|
-
)
|
|
335
|
-
|
|
336
|
-
if (runtimeOptions.json) {
|
|
337
|
-
writeJson({
|
|
338
|
-
name,
|
|
339
|
-
file,
|
|
340
|
-
content: Buffer.isBuffer(content) ? content.toString("base64") : content,
|
|
341
|
-
encoding: Buffer.isBuffer(content) ? "base64" : "utf8",
|
|
342
|
-
})
|
|
343
|
-
return
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
if (runtimeOptions.silent) return
|
|
347
|
-
|
|
348
|
-
console.log(Buffer.isBuffer(content) ? content.toString("base64") : content)
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function getComponentOrThrow(name) {
|
|
352
|
-
if (!name) {
|
|
353
|
-
throw new Error("missing component name. Try `banhaten docs button`.")
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
const component = registry.components[name]
|
|
357
|
-
if (!component) {
|
|
358
|
-
throw new Error(`unknown component "${name}". Run \`banhaten list\`.`)
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
return component
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function getComponentDocPath(name) {
|
|
365
|
-
const docAliases = {
|
|
366
|
-
"onboarding-step": "onboarding-step-list-item",
|
|
367
|
-
}
|
|
368
|
-
const docId = docAliases[name] || name
|
|
369
|
-
|
|
370
|
-
return `docs/design-system/components/${docId}.md`
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function writeJson(value) {
|
|
374
|
-
console.log(JSON.stringify(value, null, 2))
|
|
375
|
-
}
|
|
376
|
-
|
|
377
231
|
async function initProject(flags) {
|
|
378
|
-
const cwd = path.resolve(
|
|
232
|
+
const cwd = path.resolve(flags.cwd || process.cwd())
|
|
379
233
|
const dryRun = Boolean(flags["dry-run"])
|
|
380
234
|
const force = Boolean(flags.force)
|
|
381
|
-
await warmPathHints(cwd)
|
|
382
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 }
|
|
383
240
|
const writes = []
|
|
384
241
|
|
|
385
242
|
writes.push(
|
|
386
243
|
...(await writeConfig(cwd, config, { dryRun, force })),
|
|
387
244
|
...(await writeProjectSchemaFiles(cwd, { dryRun, force })),
|
|
388
|
-
...(await writeBaseFiles(
|
|
389
|
-
...(await writeGlobalCss(
|
|
390
|
-
...(await writeAliasConfig(
|
|
245
|
+
...(await writeBaseFiles(installPlan, managedOptions)),
|
|
246
|
+
...(await writeGlobalCss(installPlan, managedOptions)),
|
|
247
|
+
...(await writeAliasConfig(installPlan, { dryRun, force }))
|
|
391
248
|
)
|
|
392
249
|
|
|
393
|
-
const
|
|
250
|
+
const packageResult = await updatePackageJson(
|
|
394
251
|
cwd,
|
|
395
|
-
|
|
252
|
+
getPackageChanges(installPlan, registry.base.dependencies || {}),
|
|
396
253
|
{ dryRun }
|
|
397
254
|
)
|
|
398
|
-
writes.push(...
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
+
})
|
|
402
276
|
}
|
|
403
277
|
|
|
404
278
|
async function addComponents(names, flags) {
|
|
@@ -413,7 +287,7 @@ async function addComponents(names, flags) {
|
|
|
413
287
|
}
|
|
414
288
|
|
|
415
289
|
async function diffComponents(names, flags) {
|
|
416
|
-
await writeComponents(names, { ...flags, "dry-run": true }, {
|
|
290
|
+
await writeComponents(names, { ...flags, "dry-run": true, "show-diff": true }, {
|
|
417
291
|
inferInstalled: true,
|
|
418
292
|
title: names.length
|
|
419
293
|
? `Checked ${names.join(", ")} for updates`
|
|
@@ -431,16 +305,20 @@ async function updateComponents(names, flags) {
|
|
|
431
305
|
}
|
|
432
306
|
|
|
433
307
|
async function writeComponents(requestedNames, flags, options) {
|
|
434
|
-
const cwd = path.resolve(
|
|
308
|
+
const cwd = path.resolve(flags.cwd || process.cwd())
|
|
435
309
|
const dryRun = Boolean(flags["dry-run"])
|
|
436
310
|
const force = Boolean(flags.force)
|
|
437
|
-
|
|
311
|
+
const showDiff = Boolean(flags["show-diff"])
|
|
438
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 }
|
|
439
317
|
const selectedNames =
|
|
440
318
|
requestedNames.length > 0
|
|
441
319
|
? requestedNames
|
|
442
320
|
: options.inferInstalled
|
|
443
|
-
? await findInstalledComponents(
|
|
321
|
+
? await findInstalledComponents(installPlan)
|
|
444
322
|
: requestedNames
|
|
445
323
|
|
|
446
324
|
if (selectedNames.length === 0) {
|
|
@@ -452,14 +330,13 @@ async function writeComponents(requestedNames, flags, options) {
|
|
|
452
330
|
const names = resolveRegistryDependencies(selectedNames)
|
|
453
331
|
|
|
454
332
|
const writes = []
|
|
455
|
-
const deps = {
|
|
333
|
+
const deps = {}
|
|
334
|
+
const components = []
|
|
456
335
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
...(await writeGlobalCss(cwd, config, { dryRun, force })),
|
|
462
|
-
...(await writeAliasConfig(cwd, config, { dryRun, force }))
|
|
336
|
+
mergeDependencyRequirements(
|
|
337
|
+
deps,
|
|
338
|
+
registry.base.dependencies || {},
|
|
339
|
+
"registry base"
|
|
463
340
|
)
|
|
464
341
|
|
|
465
342
|
for (const name of names) {
|
|
@@ -468,41 +345,200 @@ async function writeComponents(requestedNames, flags, options) {
|
|
|
468
345
|
throw new Error(`unknown component "${name}". Run \`banhaten list\`.`)
|
|
469
346
|
}
|
|
470
347
|
|
|
471
|
-
|
|
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) {
|
|
472
365
|
writes.push(
|
|
473
|
-
...(await writeRegistryFiles(
|
|
474
|
-
|
|
475
|
-
|
|
366
|
+
...(await writeRegistryFiles(installPlan, component.files || [], {
|
|
367
|
+
...managedOptions,
|
|
368
|
+
owner: name,
|
|
476
369
|
}))
|
|
477
370
|
)
|
|
478
371
|
}
|
|
479
372
|
|
|
480
|
-
|
|
373
|
+
const packageResult = await updatePackageJson(
|
|
481
374
|
cwd,
|
|
482
|
-
|
|
375
|
+
getPackageChanges(installPlan, deps),
|
|
483
376
|
{ dryRun }
|
|
484
|
-
)
|
|
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
|
|
485
388
|
|
|
486
|
-
|
|
487
|
-
|
|
389
|
+
deferUntilApplied(() => {
|
|
390
|
+
printResult(options.title, writes, dryRun, runtimeOptions, install)
|
|
391
|
+
printInstallHint(install, dryRun, runtimeOptions)
|
|
392
|
+
})
|
|
488
393
|
}
|
|
489
394
|
|
|
490
|
-
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
|
+
|
|
491
429
|
const installed = []
|
|
492
430
|
|
|
493
431
|
for (const [name, component] of Object.entries(registry.components)) {
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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)
|
|
500
440
|
}
|
|
501
441
|
}
|
|
502
442
|
|
|
503
443
|
return installed
|
|
504
444
|
}
|
|
505
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
|
+
|
|
506
542
|
function resolveRegistryDependencies(names) {
|
|
507
543
|
const resolved = []
|
|
508
544
|
const seen = new Set()
|
|
@@ -558,8 +594,14 @@ async function loadConfig(cwd) {
|
|
|
558
594
|
|
|
559
595
|
const localConfig = await readJsonIfExists(configPath)
|
|
560
596
|
if (localConfig) {
|
|
561
|
-
|
|
562
|
-
|
|
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
|
+
|
|
563
605
|
if (localConfig.tailwind?.css) hasExplicitCssPath = true
|
|
564
606
|
config.tailwind = { ...config.tailwind, ...(localConfig.tailwind || {}) }
|
|
565
607
|
config.aliases = { ...config.aliases, ...(localConfig.aliases || {}) }
|
|
@@ -569,6 +611,8 @@ async function loadConfig(cwd) {
|
|
|
569
611
|
config.tailwind.css = await detectCssPath(cwd, config.tailwind.css)
|
|
570
612
|
}
|
|
571
613
|
|
|
614
|
+
assertSchema(config, configSchema, CONFIG_FILE)
|
|
615
|
+
|
|
572
616
|
return config
|
|
573
617
|
}
|
|
574
618
|
|
|
@@ -595,20 +639,34 @@ async function writeConfig(cwd, config, options) {
|
|
|
595
639
|
if ((await exists(configPath)) && !options.force) {
|
|
596
640
|
const current = await fs.readFile(configPath, "utf8")
|
|
597
641
|
const currentConfig = JSON.parse(current)
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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)) {
|
|
605
655
|
const status = await writeFile(
|
|
606
656
|
configPath,
|
|
607
657
|
`${JSON.stringify(nextConfig, null, 2)}\n`,
|
|
608
658
|
{ dryRun: options.dryRun }
|
|
609
659
|
)
|
|
610
660
|
|
|
611
|
-
|
|
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
|
|
612
670
|
}
|
|
613
671
|
|
|
614
672
|
return [`kept ${relative(cwd, configPath)}`]
|
|
@@ -642,30 +700,52 @@ async function writeProjectSchemaFiles(cwd, options) {
|
|
|
642
700
|
return writes
|
|
643
701
|
}
|
|
644
702
|
|
|
645
|
-
async function writeBaseFiles(
|
|
646
|
-
return writeRegistryFiles(
|
|
703
|
+
async function writeBaseFiles(installPlan, options) {
|
|
704
|
+
return writeRegistryFiles(installPlan, registry.base.files || [], {
|
|
705
|
+
...options,
|
|
706
|
+
owner: "base",
|
|
707
|
+
})
|
|
647
708
|
}
|
|
648
709
|
|
|
649
|
-
async function writeRegistryFiles(
|
|
710
|
+
async function writeRegistryFiles(installPlan, files, options) {
|
|
711
|
+
const { cwd } = installPlan
|
|
650
712
|
const writes = []
|
|
651
713
|
|
|
652
714
|
for (const file of files) {
|
|
653
715
|
const sourcePath = path.join(registryRoot, file.source)
|
|
654
|
-
const targetPath = path.join(
|
|
716
|
+
const targetPath = path.join(
|
|
717
|
+
cwd,
|
|
718
|
+
resolveInstallTarget(installPlan, file.target)
|
|
719
|
+
)
|
|
655
720
|
const source = await fs.readFile(sourcePath)
|
|
656
721
|
const content = isTextRegistryFile(file.source)
|
|
657
|
-
? applyTemplate(
|
|
722
|
+
? applyTemplate(
|
|
723
|
+
source.toString("utf8"),
|
|
724
|
+
installPlan,
|
|
725
|
+
relative(cwd, targetPath)
|
|
726
|
+
)
|
|
658
727
|
: source
|
|
728
|
+
const target = relative(cwd, targetPath)
|
|
729
|
+
|
|
730
|
+
await assertManagedFileSafe(targetPath, content, target, options)
|
|
659
731
|
|
|
660
732
|
const status = await writeFile(targetPath, content, options)
|
|
661
|
-
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
|
+
}
|
|
662
741
|
}
|
|
663
742
|
|
|
664
743
|
return writes
|
|
665
744
|
}
|
|
666
745
|
|
|
667
|
-
async function writeGlobalCss(
|
|
668
|
-
const
|
|
746
|
+
async function writeGlobalCss(installPlan, options) {
|
|
747
|
+
const { cwd } = installPlan
|
|
748
|
+
const cssPath = path.join(cwd, installPlan.destinations.css)
|
|
669
749
|
const sourcePath = path.join(registryRoot, "styles", "globals.css")
|
|
670
750
|
const tokenCss = await fs.readFile(sourcePath, "utf8")
|
|
671
751
|
const current = await readTextIfExists(cssPath)
|
|
@@ -678,1165 +758,230 @@ async function writeGlobalCss(cwd, config, options) {
|
|
|
678
758
|
: `${tokenCss}\n`
|
|
679
759
|
const next = ensureGlobalCssImports(nextCss)
|
|
680
760
|
|
|
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
|
+
}
|
|
777
|
+
|
|
681
778
|
const status = await writeFile(cssPath, next, options)
|
|
682
|
-
|
|
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
|
+
}
|
|
783
|
+
return writes
|
|
683
784
|
}
|
|
684
785
|
|
|
685
|
-
async function writeAliasConfig(
|
|
686
|
-
if (!usesRootAlias(config, "@")) return []
|
|
687
|
-
|
|
786
|
+
async function writeAliasConfig(installPlan, options) {
|
|
688
787
|
const writes = []
|
|
689
|
-
writes.push(...(await writeTypescriptAliasConfig(cwd, "@", options)))
|
|
690
|
-
writes.push(...(await writeViteAliasConfig(cwd, "@", options)))
|
|
691
|
-
return writes
|
|
692
|
-
}
|
|
693
788
|
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
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
|
+
)
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
return writes
|
|
698
806
|
}
|
|
699
807
|
|
|
700
|
-
|
|
701
|
-
const
|
|
702
|
-
if (
|
|
808
|
+
function getPackageChanges(installPlan, dependencies) {
|
|
809
|
+
const devDependencies = { ...SETUP_DEV_DEPENDENCIES }
|
|
810
|
+
if (installPlan.vite) {
|
|
811
|
+
Object.assign(devDependencies, VITE_DEV_DEPENDENCIES)
|
|
812
|
+
}
|
|
703
813
|
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
const targetPattern = `${sourceRoot}/*`
|
|
814
|
+
return {
|
|
815
|
+
dependencies,
|
|
816
|
+
devDependencies,
|
|
817
|
+
}
|
|
818
|
+
}
|
|
710
819
|
|
|
711
|
-
|
|
712
|
-
|
|
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,
|
|
713
828
|
}
|
|
829
|
+
const packagePlan = createPackageDependencyPlan(packageJson, changes)
|
|
714
830
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
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
|
+
}
|
|
721
845
|
}
|
|
722
846
|
|
|
723
|
-
|
|
847
|
+
applyPackageDependencyPlan(packageJson, packagePlan.operations)
|
|
848
|
+
|
|
849
|
+
const status = await writeFile(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`, {
|
|
724
850
|
dryRun: options.dryRun,
|
|
851
|
+
expectedCurrent: currentSource,
|
|
725
852
|
force: true,
|
|
726
853
|
})
|
|
727
|
-
|
|
854
|
+
|
|
855
|
+
return {
|
|
856
|
+
installRequired: packagePlan.operations.length > 0,
|
|
857
|
+
writes: [`${status} ${relative(cwd, packagePath)}`],
|
|
858
|
+
}
|
|
728
859
|
}
|
|
729
860
|
|
|
730
|
-
|
|
731
|
-
const
|
|
732
|
-
"
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
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]
|
|
883
|
+
)
|
|
884
|
+
}
|
|
736
885
|
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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(/\/$/, "")
|
|
740
895
|
}
|
|
741
896
|
|
|
742
|
-
|
|
897
|
+
const targetDirectory = path.posix.dirname(
|
|
898
|
+
normalizeReceiptTarget(target)
|
|
899
|
+
)
|
|
900
|
+
const destination = installPlan.destinations[destinationName]
|
|
901
|
+
const relativeImport = path.posix.relative(targetDirectory, destination)
|
|
902
|
+
|
|
903
|
+
if (!relativeImport) return "."
|
|
904
|
+
return relativeImport.startsWith(".")
|
|
905
|
+
? relativeImport
|
|
906
|
+
: `./${relativeImport}`
|
|
743
907
|
}
|
|
744
908
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
909
|
+
function isTextRegistryFile(source) {
|
|
910
|
+
return !/\.(avif|eot|gif|ico|jpe?g|otf|png|ttf|webp|woff2?)$/i.test(source)
|
|
911
|
+
}
|
|
748
912
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
913
|
+
async function readJson(filePath) {
|
|
914
|
+
return JSON.parse(await fs.readFile(filePath, "utf8"))
|
|
915
|
+
}
|
|
752
916
|
|
|
917
|
+
async function readJsonIfExists(filePath) {
|
|
753
918
|
try {
|
|
754
|
-
|
|
919
|
+
return await readJson(filePath)
|
|
755
920
|
} catch (error) {
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
if (next === current) {
|
|
760
|
-
return [`kept ${relative(cwd, viteConfigPath)}`]
|
|
921
|
+
if (error.code === "ENOENT") return null
|
|
922
|
+
throw error
|
|
761
923
|
}
|
|
762
|
-
|
|
763
|
-
const status = await writeFile(viteConfigPath, next, {
|
|
764
|
-
dryRun: options.dryRun,
|
|
765
|
-
force: true,
|
|
766
|
-
})
|
|
767
|
-
return [`${status} ${relative(cwd, viteConfigPath)}`]
|
|
768
924
|
}
|
|
769
925
|
|
|
770
|
-
async function
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
]
|
|
777
|
-
|
|
778
|
-
for (const candidate of candidates) {
|
|
779
|
-
const target = path.join(cwd, candidate)
|
|
780
|
-
if (await exists(target)) return target
|
|
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
|
|
781
932
|
}
|
|
782
|
-
|
|
783
|
-
return null
|
|
784
933
|
}
|
|
785
934
|
|
|
786
|
-
function
|
|
787
|
-
|
|
788
|
-
const nodeUrlImports = imports.filter((entry) => entry.moduleSpecifier === "node:url")
|
|
789
|
-
const importedLocals = new Set(nodeUrlImports.flatMap((entry) => entry.namedLocals))
|
|
790
|
-
const missing = ["fileURLToPath", "URL"].filter((name) => !importedLocals.has(name))
|
|
935
|
+
async function assertManagedFileSafe(targetPath, nextContent, target, options) {
|
|
936
|
+
if (options.dryRun || options.force || !(await exists(targetPath))) return
|
|
791
937
|
|
|
792
|
-
|
|
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
|
|
793
943
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
)
|
|
798
|
-
}
|
|
944
|
+
const expectedHash = findManagedFileHash(options.receipt, target)
|
|
945
|
+
const currentHash = hashContent(current)
|
|
946
|
+
if (expectedHash && currentHash === expectedHash) return
|
|
799
947
|
|
|
800
|
-
|
|
801
|
-
const
|
|
802
|
-
|
|
803
|
-
|
|
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."
|
|
804
958
|
)
|
|
959
|
+
}
|
|
805
960
|
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
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
|
|
812
968
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
969
|
+
for (const component of Object.values(receipt?.components || {})) {
|
|
970
|
+
const match = component.files?.find(
|
|
971
|
+
(file) => normalizeReceiptTarget(file.target) === normalizedTarget
|
|
816
972
|
)
|
|
973
|
+
if (match?.sha256) return match.sha256
|
|
817
974
|
}
|
|
818
975
|
|
|
819
|
-
return
|
|
976
|
+
return null
|
|
820
977
|
}
|
|
821
978
|
|
|
822
|
-
function
|
|
823
|
-
|
|
824
|
-
let next = addViteResolveAlias(source, aliasRoot, sourceRoot)
|
|
825
|
-
next = addVitePluginCall(next, `${tailwindLocalName}()`, tailwindLocalName)
|
|
826
|
-
next = ensureTailwindViteImport(next, tailwindLocalName)
|
|
827
|
-
next = ensureNodeUrlImport(next)
|
|
828
|
-
return next
|
|
979
|
+
function hashContent(content) {
|
|
980
|
+
return createHash("sha256").update(content).digest("hex")
|
|
829
981
|
}
|
|
830
982
|
|
|
831
|
-
function
|
|
832
|
-
|
|
833
|
-
(entry) => entry.moduleSpecifier === "@tailwindcss/vite"
|
|
834
|
-
)
|
|
835
|
-
|
|
836
|
-
if (!tailwindImport) return "tailwindcss"
|
|
837
|
-
if (tailwindImport.defaultLocal) return tailwindImport.defaultLocal
|
|
838
|
-
|
|
839
|
-
throw new Error(
|
|
840
|
-
"unsupported Vite config: @tailwindcss/vite must be imported with a default binding."
|
|
841
|
-
)
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
function addViteResolveAlias(source, aliasRoot, sourceRoot) {
|
|
845
|
-
const configObject = findViteConfigObject(source)
|
|
846
|
-
const resolveProperty = findObjectProperty(source, configObject, "resolve")
|
|
847
|
-
const aliasEntry = `${JSON.stringify(aliasRoot)}: fileURLToPath(new URL(${JSON.stringify(
|
|
848
|
-
sourceRoot
|
|
849
|
-
)}, import.meta.url))`
|
|
850
|
-
|
|
851
|
-
if (!resolveProperty) {
|
|
852
|
-
return insertObjectProperty(
|
|
853
|
-
source,
|
|
854
|
-
configObject,
|
|
855
|
-
[
|
|
856
|
-
"resolve: {",
|
|
857
|
-
" alias: {",
|
|
858
|
-
` ${aliasEntry},`,
|
|
859
|
-
" },",
|
|
860
|
-
"},",
|
|
861
|
-
].join("\n")
|
|
862
|
-
)
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
const resolveObject = expectObjectLiteralValue(
|
|
866
|
-
source,
|
|
867
|
-
resolveProperty,
|
|
868
|
-
"unsupported Vite config: resolve must be an object literal."
|
|
869
|
-
)
|
|
870
|
-
const aliasProperty = findObjectProperty(source, resolveObject, "alias")
|
|
871
|
-
|
|
872
|
-
if (!aliasProperty) {
|
|
873
|
-
return insertObjectProperty(
|
|
874
|
-
source,
|
|
875
|
-
resolveObject,
|
|
876
|
-
[
|
|
877
|
-
"alias: {",
|
|
878
|
-
` ${aliasEntry},`,
|
|
879
|
-
"},",
|
|
880
|
-
].join("\n")
|
|
881
|
-
)
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
const aliasObject = expectObjectLiteralValue(
|
|
885
|
-
source,
|
|
886
|
-
aliasProperty,
|
|
887
|
-
"unsupported Vite config: resolve.alias must be an object literal."
|
|
888
|
-
)
|
|
889
|
-
|
|
890
|
-
if (findObjectProperty(source, aliasObject, aliasRoot)) {
|
|
891
|
-
return source
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
return insertObjectProperty(source, aliasObject, `${aliasEntry},`)
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
function addVitePluginCall(source, pluginCall, pluginLocalName) {
|
|
898
|
-
const configObject = findViteConfigObject(source)
|
|
899
|
-
const pluginsProperty = findObjectProperty(source, configObject, "plugins")
|
|
900
|
-
|
|
901
|
-
if (!pluginsProperty) {
|
|
902
|
-
return insertObjectProperty(source, configObject, `plugins: [${pluginCall}],`)
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
const pluginsArray = expectArrayLiteralValue(
|
|
906
|
-
source,
|
|
907
|
-
pluginsProperty,
|
|
908
|
-
"unsupported Vite config: plugins must be an array literal."
|
|
909
|
-
)
|
|
910
|
-
|
|
911
|
-
if (arrayHasCallExpression(source, pluginsArray, pluginLocalName)) {
|
|
912
|
-
return source
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
return appendArrayElement(source, pluginsArray, pluginCall)
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
function findViteConfigObject(source) {
|
|
919
|
-
const expressionStart = findExportDefaultExpressionStart(source)
|
|
920
|
-
|
|
921
|
-
if (expressionStart === -1) {
|
|
922
|
-
throw new Error(
|
|
923
|
-
"unsupported Vite config: expected `export default defineConfig({ ... })` or `export default { ... }`."
|
|
924
|
-
)
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
if (source[expressionStart] === "{") {
|
|
928
|
-
return createBracketRange(source, expressionStart, "{")
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
const callee = readIdentifierAt(source, expressionStart)
|
|
932
|
-
if (callee?.name !== "defineConfig") {
|
|
933
|
-
throw new Error(
|
|
934
|
-
"unsupported Vite config: default export must be `defineConfig({ ... })` or an object literal."
|
|
935
|
-
)
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
let index = skipWhitespaceAndComments(source, callee.end)
|
|
939
|
-
if (source[index] !== "(") {
|
|
940
|
-
throw new Error("unsupported Vite config: malformed defineConfig call.")
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
index = skipWhitespaceAndComments(source, index + 1)
|
|
944
|
-
if (source[index] !== "{") {
|
|
945
|
-
throw new Error(
|
|
946
|
-
"unsupported Vite config: defineConfig must receive a config object literal."
|
|
947
|
-
)
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
return createBracketRange(source, index, "{")
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
function findExportDefaultExpressionStart(source) {
|
|
954
|
-
let index = 0
|
|
955
|
-
|
|
956
|
-
while (index < source.length) {
|
|
957
|
-
const next = skipIgnoredSyntax(source, index)
|
|
958
|
-
if (next !== index) {
|
|
959
|
-
index = next
|
|
960
|
-
continue
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
if (!matchIdentifier(source, index, "export")) {
|
|
964
|
-
index += 1
|
|
965
|
-
continue
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
const defaultStart = skipWhitespaceAndComments(source, index + "export".length)
|
|
969
|
-
if (!matchIdentifier(source, defaultStart, "default")) {
|
|
970
|
-
index += "export".length
|
|
971
|
-
continue
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
return skipWhitespaceAndComments(source, defaultStart + "default".length)
|
|
975
|
-
}
|
|
976
|
-
|
|
977
|
-
return -1
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
function createBracketRange(source, start, open) {
|
|
981
|
-
const close = open === "{" ? "}" : open === "[" ? "]" : ")"
|
|
982
|
-
const end = findMatchingBracket(source, start, open, close)
|
|
983
|
-
return { start, end: end + 1 }
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
function expectObjectLiteralValue(source, property, message) {
|
|
987
|
-
const valueStart = skipWhitespaceAndComments(source, property.valueStart)
|
|
988
|
-
if (source[valueStart] !== "{") throw new Error(message)
|
|
989
|
-
return createBracketRange(source, valueStart, "{")
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
function expectArrayLiteralValue(source, property, message) {
|
|
993
|
-
const valueStart = skipWhitespaceAndComments(source, property.valueStart)
|
|
994
|
-
if (source[valueStart] !== "[") throw new Error(message)
|
|
995
|
-
return createBracketRange(source, valueStart, "[")
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
function findObjectProperty(source, objectRange, propertyName) {
|
|
999
|
-
const bodyEnd = objectRange.end - 1
|
|
1000
|
-
let index = objectRange.start + 1
|
|
1001
|
-
|
|
1002
|
-
while (index < bodyEnd) {
|
|
1003
|
-
index = skipWhitespaceCommentsAndCommas(source, index, bodyEnd)
|
|
1004
|
-
if (index >= bodyEnd) return null
|
|
1005
|
-
|
|
1006
|
-
const propertyStart = index
|
|
1007
|
-
const name = readPropertyName(source, index)
|
|
1008
|
-
const propertyEnd = findTopLevelCommaOrEnd(source, propertyStart, bodyEnd)
|
|
1009
|
-
|
|
1010
|
-
if (!name) {
|
|
1011
|
-
index = propertyEnd + 1
|
|
1012
|
-
continue
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
index = skipWhitespaceAndComments(source, name.end)
|
|
1016
|
-
if (source[index] !== ":") {
|
|
1017
|
-
if (name.name === propertyName) {
|
|
1018
|
-
throw new Error(
|
|
1019
|
-
`unsupported Vite config: ${propertyName} must be a normal object property.`
|
|
1020
|
-
)
|
|
1021
|
-
}
|
|
1022
|
-
index = propertyEnd + 1
|
|
1023
|
-
continue
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
if (name.name === propertyName) {
|
|
1027
|
-
return {
|
|
1028
|
-
name: name.name,
|
|
1029
|
-
start: propertyStart,
|
|
1030
|
-
end: propertyEnd,
|
|
1031
|
-
valueStart: skipWhitespaceAndComments(source, index + 1),
|
|
1032
|
-
valueEnd: propertyEnd,
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
index = propertyEnd + 1
|
|
1037
|
-
}
|
|
1038
|
-
|
|
1039
|
-
return null
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
function readPropertyName(source, index) {
|
|
1043
|
-
if (source[index] === "\"" || source[index] === "'") {
|
|
1044
|
-
const literal = readStringLiteral(source, index)
|
|
1045
|
-
return literal ? { name: literal.value, end: literal.end } : null
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
return readIdentifierAt(source, index)
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
function insertObjectProperty(source, objectRange, propertyText) {
|
|
1052
|
-
const insertAt = objectRange.start + 1
|
|
1053
|
-
const bodyEnd = objectRange.end - 1
|
|
1054
|
-
const body = source.slice(insertAt, bodyEnd)
|
|
1055
|
-
const hasContent = body.trim().length > 0
|
|
1056
|
-
const propertyIndent = detectObjectChildIndent(source, objectRange)
|
|
1057
|
-
const closeIndent = getLineIndent(source, bodyEnd)
|
|
1058
|
-
const indentedProperty = indentMultiline(propertyText, propertyIndent)
|
|
1059
|
-
|
|
1060
|
-
if (!hasContent) {
|
|
1061
|
-
return `${source.slice(0, insertAt)}\n${indentedProperty}\n${closeIndent}${source.slice(insertAt)}`
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
const needsTrailingNewline = source[insertAt] !== "\n" && source[insertAt] !== "\r"
|
|
1065
|
-
const insertion = `\n${indentedProperty}${needsTrailingNewline ? "\n" : ""}`
|
|
1066
|
-
return `${source.slice(0, insertAt)}${insertion}${source.slice(insertAt)}`
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
function detectObjectChildIndent(source, objectRange) {
|
|
1070
|
-
const bodyEnd = objectRange.end - 1
|
|
1071
|
-
let index = objectRange.start + 1
|
|
1072
|
-
|
|
1073
|
-
while (index < bodyEnd) {
|
|
1074
|
-
if (source[index] === "\n") {
|
|
1075
|
-
const next = skipHorizontalWhitespace(source, index + 1)
|
|
1076
|
-
if (next < bodyEnd && source[next] !== "\n" && source[next] !== "\r") {
|
|
1077
|
-
return source.slice(index + 1, next)
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
index += 1
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
return `${getLineIndent(source, objectRange.start)} `
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
function appendArrayElement(source, arrayRange, elementText) {
|
|
1087
|
-
const bodyStart = arrayRange.start + 1
|
|
1088
|
-
const bodyEnd = arrayRange.end - 1
|
|
1089
|
-
const body = source.slice(bodyStart, bodyEnd)
|
|
1090
|
-
|
|
1091
|
-
if (body.trim().length === 0) {
|
|
1092
|
-
return `${source.slice(0, bodyStart)}${elementText}${source.slice(bodyStart)}`
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
const contentEnd = trimEndIndex(source, bodyStart, bodyEnd)
|
|
1096
|
-
const hasNewline = bodyIncludesNewline(source, bodyStart, bodyEnd)
|
|
1097
|
-
const hasTrailingComma = previousCodeChar(source, contentEnd) === ","
|
|
1098
|
-
|
|
1099
|
-
if (!hasNewline) {
|
|
1100
|
-
const separator = hasTrailingComma ? " " : ", "
|
|
1101
|
-
return `${source.slice(0, contentEnd)}${separator}${elementText}${source.slice(contentEnd)}`
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
|
-
const closeIndent = getLineIndent(source, bodyEnd)
|
|
1105
|
-
const elementIndent = `${closeIndent} `
|
|
1106
|
-
const separator = hasTrailingComma ? "\n" : ",\n"
|
|
1107
|
-
return `${source.slice(0, contentEnd)}${separator}${elementIndent}${elementText},${source.slice(contentEnd)}`
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
function arrayHasCallExpression(source, arrayRange, localName) {
|
|
1111
|
-
const bodyEnd = arrayRange.end - 1
|
|
1112
|
-
let index = arrayRange.start + 1
|
|
1113
|
-
|
|
1114
|
-
while (index < bodyEnd) {
|
|
1115
|
-
index = skipWhitespaceCommentsAndCommas(source, index, bodyEnd)
|
|
1116
|
-
if (index >= bodyEnd) return false
|
|
1117
|
-
|
|
1118
|
-
const elementEnd = findTopLevelCommaOrEnd(source, index, bodyEnd)
|
|
1119
|
-
if (startsWithCallExpression(source, index, elementEnd, localName)) {
|
|
1120
|
-
return true
|
|
1121
|
-
}
|
|
1122
|
-
index = elementEnd + 1
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
return false
|
|
1126
|
-
}
|
|
1127
|
-
|
|
1128
|
-
function startsWithCallExpression(source, start, end, localName) {
|
|
1129
|
-
const index = skipWhitespaceAndComments(source, start)
|
|
1130
|
-
if (!matchIdentifier(source, index, localName)) return false
|
|
1131
|
-
|
|
1132
|
-
const callStart = skipWhitespaceAndComments(source, index + localName.length)
|
|
1133
|
-
if (source[callStart] !== "(") return false
|
|
1134
|
-
|
|
1135
|
-
const callEnd = createBracketRange(source, callStart, "(").end
|
|
1136
|
-
const next = skipWhitespaceAndComments(source, callEnd)
|
|
1137
|
-
return next <= end
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
function parseImportDeclarations(source) {
|
|
1141
|
-
const declarations = []
|
|
1142
|
-
let index = 0
|
|
1143
|
-
|
|
1144
|
-
while (index < source.length) {
|
|
1145
|
-
const next = skipIgnoredSyntax(source, index)
|
|
1146
|
-
if (next !== index) {
|
|
1147
|
-
index = next
|
|
1148
|
-
continue
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
if (!matchIdentifier(source, index, "import")) {
|
|
1152
|
-
index += 1
|
|
1153
|
-
continue
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
const clauseStart = skipWhitespaceAndComments(source, index + "import".length)
|
|
1157
|
-
if (source[clauseStart] === ".") {
|
|
1158
|
-
index = clauseStart + 1
|
|
1159
|
-
continue
|
|
1160
|
-
}
|
|
1161
|
-
if (source[clauseStart] === "(") {
|
|
1162
|
-
index = clauseStart + 1
|
|
1163
|
-
continue
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
const end = findImportDeclarationEnd(source, index)
|
|
1167
|
-
const declaration = source.slice(index, end)
|
|
1168
|
-
const moduleSpecifier = getImportModuleSpecifier(declaration)
|
|
1169
|
-
|
|
1170
|
-
if (moduleSpecifier) {
|
|
1171
|
-
declarations.push({
|
|
1172
|
-
start: index,
|
|
1173
|
-
end,
|
|
1174
|
-
moduleSpecifier,
|
|
1175
|
-
defaultLocal: getDefaultImportLocal(declaration),
|
|
1176
|
-
namedLocals: getNamedImportLocals(declaration),
|
|
1177
|
-
})
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
index = end
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
return declarations
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
function findImportDeclarationEnd(source, start) {
|
|
1187
|
-
const stack = []
|
|
1188
|
-
let sawModuleSpecifier = false
|
|
1189
|
-
let index = start
|
|
1190
|
-
|
|
1191
|
-
while (index < source.length) {
|
|
1192
|
-
const skipped = skipComment(source, index)
|
|
1193
|
-
if (skipped !== index) {
|
|
1194
|
-
index = skipped
|
|
1195
|
-
continue
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
if (source[index] === "\"" || source[index] === "'") {
|
|
1199
|
-
const literal = readStringLiteral(source, index)
|
|
1200
|
-
if (!literal) return source.length
|
|
1201
|
-
sawModuleSpecifier = true
|
|
1202
|
-
index = literal.end
|
|
1203
|
-
continue
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
if (isOpeningBracket(source[index])) {
|
|
1207
|
-
stack.push(expectedClosingBracket(source[index]))
|
|
1208
|
-
index += 1
|
|
1209
|
-
continue
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
if (isClosingBracket(source[index])) {
|
|
1213
|
-
if (stack.at(-1) === source[index]) stack.pop()
|
|
1214
|
-
index += 1
|
|
1215
|
-
continue
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
if (stack.length === 0 && source[index] === ";") return index + 1
|
|
1219
|
-
if (
|
|
1220
|
-
stack.length === 0 &&
|
|
1221
|
-
sawModuleSpecifier &&
|
|
1222
|
-
(source[index] === "\n" || source[index] === "\r")
|
|
1223
|
-
) {
|
|
1224
|
-
return index + 1
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
index += 1
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
return source.length
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
function getImportModuleSpecifier(declaration) {
|
|
1234
|
-
let moduleSpecifier = null
|
|
1235
|
-
let index = 0
|
|
1236
|
-
|
|
1237
|
-
while (index < declaration.length) {
|
|
1238
|
-
const skipped = skipComment(declaration, index)
|
|
1239
|
-
if (skipped !== index) {
|
|
1240
|
-
index = skipped
|
|
1241
|
-
continue
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
if (declaration[index] === "\"" || declaration[index] === "'") {
|
|
1245
|
-
const literal = readStringLiteral(declaration, index)
|
|
1246
|
-
if (!literal) break
|
|
1247
|
-
moduleSpecifier = literal.value
|
|
1248
|
-
index = literal.end
|
|
1249
|
-
continue
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
index += 1
|
|
1253
|
-
}
|
|
1254
|
-
|
|
1255
|
-
return moduleSpecifier
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
function getDefaultImportLocal(declaration) {
|
|
1259
|
-
let index = skipWhitespaceAndComments(declaration, "import".length)
|
|
1260
|
-
|
|
1261
|
-
if (matchIdentifier(declaration, index, "type")) {
|
|
1262
|
-
index = skipWhitespaceAndComments(declaration, index + "type".length)
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
if (declaration[index] === "\"" || declaration[index] === "{" || declaration[index] === "*") {
|
|
1266
|
-
return null
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
const local = readIdentifierAt(declaration, index)
|
|
1270
|
-
return local?.name ?? null
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
function getNamedImportLocals(declaration) {
|
|
1274
|
-
const open = declaration.indexOf("{")
|
|
1275
|
-
if (open === -1) return []
|
|
1276
|
-
|
|
1277
|
-
const close = findMatchingBracket(declaration, open, "{", "}")
|
|
1278
|
-
const namedImportBody = declaration.slice(open + 1, close)
|
|
1279
|
-
const locals = []
|
|
1280
|
-
|
|
1281
|
-
for (const specifier of splitTopLevelList(namedImportBody)) {
|
|
1282
|
-
const trimmed = specifier.trim()
|
|
1283
|
-
if (!trimmed) continue
|
|
1284
|
-
|
|
1285
|
-
const withoutType = trimmed.startsWith("type ")
|
|
1286
|
-
? trimmed.slice("type ".length).trim()
|
|
1287
|
-
: trimmed
|
|
1288
|
-
const asIndex = withoutType.toLowerCase().lastIndexOf(" as ")
|
|
1289
|
-
const localName = asIndex === -1
|
|
1290
|
-
? withoutType
|
|
1291
|
-
: withoutType.slice(asIndex + " as ".length).trim()
|
|
1292
|
-
const local = readIdentifierAt(localName, 0)
|
|
1293
|
-
|
|
1294
|
-
if (local) locals.push(local.name)
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
return locals
|
|
1298
|
-
}
|
|
1299
|
-
|
|
1300
|
-
function splitTopLevelList(source) {
|
|
1301
|
-
const items = []
|
|
1302
|
-
let start = 0
|
|
1303
|
-
let index = 0
|
|
1304
|
-
|
|
1305
|
-
while (index < source.length) {
|
|
1306
|
-
const skipped = skipIgnoredSyntax(source, index)
|
|
1307
|
-
if (skipped !== index) {
|
|
1308
|
-
index = skipped
|
|
1309
|
-
continue
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
if (source[index] === ",") {
|
|
1313
|
-
items.push(source.slice(start, index))
|
|
1314
|
-
start = index + 1
|
|
1315
|
-
}
|
|
1316
|
-
index += 1
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
|
-
items.push(source.slice(start))
|
|
1320
|
-
return items
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
function prependImportLines(source, lines) {
|
|
1324
|
-
if (lines.length === 0) return source
|
|
1325
|
-
|
|
1326
|
-
let insertAt = 0
|
|
1327
|
-
if (source.startsWith("#!")) {
|
|
1328
|
-
const lineEnd = source.indexOf("\n")
|
|
1329
|
-
insertAt = lineEnd === -1 ? source.length : lineEnd + 1
|
|
1330
|
-
}
|
|
1331
|
-
|
|
1332
|
-
const prefix = `${lines.join("\n")}\n`
|
|
1333
|
-
return `${source.slice(0, insertAt)}${prefix}${source.slice(insertAt)}`
|
|
1334
|
-
}
|
|
1335
|
-
|
|
1336
|
-
function findTopLevelCommaOrEnd(source, start, end) {
|
|
1337
|
-
const stack = []
|
|
1338
|
-
let index = start
|
|
1339
|
-
|
|
1340
|
-
while (index < end) {
|
|
1341
|
-
const next = skipIgnoredSyntax(source, index)
|
|
1342
|
-
if (next !== index) {
|
|
1343
|
-
index = next
|
|
1344
|
-
continue
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
if (isOpeningBracket(source[index])) {
|
|
1348
|
-
stack.push(expectedClosingBracket(source[index]))
|
|
1349
|
-
index += 1
|
|
1350
|
-
continue
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
if (isClosingBracket(source[index])) {
|
|
1354
|
-
if (stack.at(-1) === source[index]) stack.pop()
|
|
1355
|
-
index += 1
|
|
1356
|
-
continue
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1359
|
-
if (stack.length === 0 && source[index] === ",") return index
|
|
1360
|
-
index += 1
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
return end
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
function findMatchingBracket(source, start, open, close) {
|
|
1367
|
-
if (source[start] !== open) {
|
|
1368
|
-
throw new Error("unsupported Vite config: malformed JavaScript object.")
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
const stack = []
|
|
1372
|
-
let index = start
|
|
1373
|
-
|
|
1374
|
-
while (index < source.length) {
|
|
1375
|
-
const next = skipIgnoredSyntax(source, index)
|
|
1376
|
-
if (next !== index) {
|
|
1377
|
-
index = next
|
|
1378
|
-
continue
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
if (isOpeningBracket(source[index])) {
|
|
1382
|
-
stack.push(expectedClosingBracket(source[index]))
|
|
1383
|
-
index += 1
|
|
1384
|
-
continue
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
|
-
if (isClosingBracket(source[index])) {
|
|
1388
|
-
const expected = stack.pop()
|
|
1389
|
-
if (expected !== source[index]) {
|
|
1390
|
-
throw new Error("unsupported Vite config: malformed JavaScript brackets.")
|
|
1391
|
-
}
|
|
1392
|
-
if (stack.length === 0) return index
|
|
1393
|
-
index += 1
|
|
1394
|
-
continue
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
index += 1
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
|
-
throw new Error(`unsupported Vite config: missing closing ${close}.`)
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
|
-
function skipIgnoredSyntax(source, index) {
|
|
1404
|
-
const commentEnd = skipComment(source, index)
|
|
1405
|
-
if (commentEnd !== index) return commentEnd
|
|
1406
|
-
|
|
1407
|
-
if (source[index] === "\"" || source[index] === "'" || source[index] === "`") {
|
|
1408
|
-
return skipQuotedText(source, index)
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
|
-
return index
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
function skipComment(source, index) {
|
|
1415
|
-
if (source[index] !== "/") return index
|
|
1416
|
-
|
|
1417
|
-
if (source[index + 1] === "/") {
|
|
1418
|
-
const lineEnd = source.indexOf("\n", index + 2)
|
|
1419
|
-
return lineEnd === -1 ? source.length : lineEnd + 1
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
if (source[index + 1] === "*") {
|
|
1423
|
-
const blockEnd = source.indexOf("*/", index + 2)
|
|
1424
|
-
return blockEnd === -1 ? source.length : blockEnd + 2
|
|
1425
|
-
}
|
|
1426
|
-
|
|
1427
|
-
return index
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
function skipQuotedText(source, index) {
|
|
1431
|
-
const quote = source[index]
|
|
1432
|
-
index += 1
|
|
1433
|
-
|
|
1434
|
-
while (index < source.length) {
|
|
1435
|
-
if (source[index] === "\\") {
|
|
1436
|
-
index += 2
|
|
1437
|
-
continue
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
if (source[index] === quote) return index + 1
|
|
1441
|
-
index += 1
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
return source.length
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
function readStringLiteral(source, index) {
|
|
1448
|
-
const quote = source[index]
|
|
1449
|
-
if (quote !== "\"" && quote !== "'") return null
|
|
1450
|
-
|
|
1451
|
-
let value = ""
|
|
1452
|
-
index += 1
|
|
1453
|
-
|
|
1454
|
-
while (index < source.length) {
|
|
1455
|
-
if (source[index] === "\\") {
|
|
1456
|
-
value += source[index + 1] ?? ""
|
|
1457
|
-
index += 2
|
|
1458
|
-
continue
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
|
-
if (source[index] === quote) {
|
|
1462
|
-
return { value, end: index + 1 }
|
|
1463
|
-
}
|
|
1464
|
-
|
|
1465
|
-
value += source[index]
|
|
1466
|
-
index += 1
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
return null
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
|
-
function readIdentifierAt(source, index) {
|
|
1473
|
-
if (!isIdentifierStart(source[index])) return null
|
|
1474
|
-
|
|
1475
|
-
let end = index + 1
|
|
1476
|
-
while (end < source.length && isIdentifierPart(source[end])) {
|
|
1477
|
-
end += 1
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
return { name: source.slice(index, end), end }
|
|
1481
|
-
}
|
|
1482
|
-
|
|
1483
|
-
function matchIdentifier(source, index, name) {
|
|
1484
|
-
if (source.slice(index, index + name.length) !== name) return false
|
|
1485
|
-
|
|
1486
|
-
const before = source[index - 1]
|
|
1487
|
-
const after = source[index + name.length]
|
|
1488
|
-
return !isIdentifierPart(before) && !isIdentifierPart(after)
|
|
1489
|
-
}
|
|
1490
|
-
|
|
1491
|
-
function isIdentifierStart(char) {
|
|
1492
|
-
return Boolean(char && /[A-Za-z_$]/.test(char))
|
|
1493
|
-
}
|
|
1494
|
-
|
|
1495
|
-
function isIdentifierPart(char) {
|
|
1496
|
-
return Boolean(char && /[A-Za-z0-9_$]/.test(char))
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
function isOpeningBracket(char) {
|
|
1500
|
-
return char === "{" || char === "[" || char === "("
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
|
-
function isClosingBracket(char) {
|
|
1504
|
-
return char === "}" || char === "]" || char === ")"
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
|
-
function expectedClosingBracket(char) {
|
|
1508
|
-
if (char === "{") return "}"
|
|
1509
|
-
if (char === "[") return "]"
|
|
1510
|
-
return ")"
|
|
1511
|
-
}
|
|
1512
|
-
|
|
1513
|
-
function skipWhitespaceAndComments(source, index) {
|
|
1514
|
-
while (index < source.length) {
|
|
1515
|
-
const next = skipComment(source, index)
|
|
1516
|
-
if (next !== index) {
|
|
1517
|
-
index = next
|
|
1518
|
-
continue
|
|
1519
|
-
}
|
|
1520
|
-
|
|
1521
|
-
if (!isWhitespace(source[index])) return index
|
|
1522
|
-
index += 1
|
|
1523
|
-
}
|
|
1524
|
-
|
|
1525
|
-
return index
|
|
1526
|
-
}
|
|
1527
|
-
|
|
1528
|
-
function skipWhitespaceCommentsAndCommas(source, index, end) {
|
|
1529
|
-
while (index < end) {
|
|
1530
|
-
const next = skipWhitespaceAndComments(source, index)
|
|
1531
|
-
if (next !== index) {
|
|
1532
|
-
index = next
|
|
1533
|
-
continue
|
|
1534
|
-
}
|
|
1535
|
-
|
|
1536
|
-
if (source[index] !== ",") return index
|
|
1537
|
-
index += 1
|
|
1538
|
-
}
|
|
1539
|
-
|
|
1540
|
-
return index
|
|
1541
|
-
}
|
|
1542
|
-
|
|
1543
|
-
function skipHorizontalWhitespace(source, index) {
|
|
1544
|
-
while (source[index] === " " || source[index] === "\t") {
|
|
1545
|
-
index += 1
|
|
1546
|
-
}
|
|
1547
|
-
return index
|
|
1548
|
-
}
|
|
1549
|
-
|
|
1550
|
-
function isWhitespace(char) {
|
|
1551
|
-
return char === " " || char === "\t" || char === "\n" || char === "\r"
|
|
1552
|
-
}
|
|
1553
|
-
|
|
1554
|
-
function indentMultiline(text, indent) {
|
|
1555
|
-
return text
|
|
1556
|
-
.split("\n")
|
|
1557
|
-
.map((line) => `${indent}${line}`)
|
|
1558
|
-
.join("\n")
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
|
-
function getLineIndent(source, index) {
|
|
1562
|
-
let lineStart = index
|
|
1563
|
-
while (lineStart > 0 && source[lineStart - 1] !== "\n") {
|
|
1564
|
-
lineStart -= 1
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
|
-
return source.slice(lineStart, skipHorizontalWhitespace(source, lineStart))
|
|
1568
|
-
}
|
|
1569
|
-
|
|
1570
|
-
function trimEndIndex(source, start, end) {
|
|
1571
|
-
while (end > start && isWhitespace(source[end - 1])) {
|
|
1572
|
-
end -= 1
|
|
1573
|
-
}
|
|
1574
|
-
return end
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
function previousCodeChar(source, index) {
|
|
1578
|
-
index -= 1
|
|
1579
|
-
while (index >= 0 && isWhitespace(source[index])) {
|
|
1580
|
-
index -= 1
|
|
1581
|
-
}
|
|
1582
|
-
return source[index]
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1585
|
-
function bodyIncludesNewline(source, start, end) {
|
|
1586
|
-
for (let index = start; index < end; index += 1) {
|
|
1587
|
-
if (source[index] === "\n" || source[index] === "\r") return true
|
|
1588
|
-
}
|
|
1589
|
-
return false
|
|
1590
|
-
}
|
|
1591
|
-
|
|
1592
|
-
async function getPackageChanges(cwd, dependencies) {
|
|
1593
|
-
const devDependencies = { ...SETUP_DEV_DEPENDENCIES }
|
|
1594
|
-
if (await findViteConfigPath(cwd)) {
|
|
1595
|
-
Object.assign(devDependencies, VITE_DEV_DEPENDENCIES)
|
|
1596
|
-
}
|
|
1597
|
-
|
|
1598
|
-
return {
|
|
1599
|
-
dependencies,
|
|
1600
|
-
devDependencies,
|
|
1601
|
-
}
|
|
1602
|
-
}
|
|
1603
|
-
|
|
1604
|
-
async function updatePackageJson(cwd, changes, options) {
|
|
1605
|
-
const packagePath = path.join(cwd, "package.json")
|
|
1606
|
-
const dependencies = changes.dependencies || {}
|
|
1607
|
-
const devDependencies = changes.devDependencies || {}
|
|
1608
|
-
const packageJson = (await readJsonIfExists(packagePath)) || {
|
|
1609
|
-
name: path.basename(cwd).toLowerCase(),
|
|
1610
|
-
version: "0.0.0",
|
|
1611
|
-
private: true,
|
|
1612
|
-
}
|
|
1613
|
-
const added = [
|
|
1614
|
-
...addPackageSection(packageJson, "dependencies", dependencies),
|
|
1615
|
-
...addPackageSection(packageJson, "devDependencies", devDependencies),
|
|
1616
|
-
]
|
|
1617
|
-
|
|
1618
|
-
if (added.length === 0 && (await exists(packagePath))) {
|
|
1619
|
-
return [`kept ${relative(cwd, packagePath)}`]
|
|
1620
|
-
}
|
|
1621
|
-
|
|
1622
|
-
const status = await writeFile(packagePath, `${JSON.stringify(packageJson, null, 2)}\n`, {
|
|
1623
|
-
dryRun: options.dryRun,
|
|
1624
|
-
force: true,
|
|
1625
|
-
})
|
|
1626
|
-
|
|
1627
|
-
return [
|
|
1628
|
-
`${status} ${relative(cwd, packagePath)}`,
|
|
1629
|
-
]
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
|
-
function addPackageSection(packageJson, section, dependencies) {
|
|
1633
|
-
const added = []
|
|
1634
|
-
const existing = packageJson[section] || {}
|
|
1635
|
-
|
|
1636
|
-
for (const [name, version] of Object.entries(dependencies)) {
|
|
1637
|
-
if (hasPackageDependency(packageJson, name)) continue
|
|
1638
|
-
|
|
1639
|
-
existing[name] = version
|
|
1640
|
-
added.push(name)
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
if (added.length > 0 || packageJson[section]) {
|
|
1644
|
-
packageJson[section] = sortObject(existing)
|
|
1645
|
-
}
|
|
1646
|
-
|
|
1647
|
-
return added
|
|
1648
|
-
}
|
|
1649
|
-
|
|
1650
|
-
function hasPackageDependency(packageJson, name) {
|
|
1651
|
-
return Boolean(
|
|
1652
|
-
packageJson.dependencies?.[name] ||
|
|
1653
|
-
packageJson.devDependencies?.[name] ||
|
|
1654
|
-
packageJson.peerDependencies?.[name] ||
|
|
1655
|
-
packageJson.optionalDependencies?.[name]
|
|
1656
|
-
)
|
|
1657
|
-
}
|
|
1658
|
-
|
|
1659
|
-
function applyTemplate(source, config) {
|
|
1660
|
-
return source
|
|
1661
|
-
.replaceAll("@/lib/utils", config.aliases.utils)
|
|
1662
|
-
.replaceAll("@/hooks", config.aliases.hooks)
|
|
1663
|
-
}
|
|
1664
|
-
|
|
1665
|
-
function isTextRegistryFile(source) {
|
|
1666
|
-
return !/\.(avif|eot|gif|ico|jpe?g|otf|png|ttf|webp|woff2?)$/i.test(source)
|
|
1667
|
-
}
|
|
1668
|
-
|
|
1669
|
-
function resolveTarget(cwd, config, target) {
|
|
1670
|
-
const replacements = {
|
|
1671
|
-
"{{ui}}": aliasToPath(cwd, config.aliases.ui),
|
|
1672
|
-
"{{utils}}": stripExtension(aliasToPath(cwd, config.aliases.utils)),
|
|
1673
|
-
"{{hooks}}": aliasToPath(cwd, config.aliases.hooks),
|
|
1674
|
-
}
|
|
1675
|
-
|
|
1676
|
-
let resolved = target
|
|
1677
|
-
for (const [placeholder, value] of Object.entries(replacements)) {
|
|
1678
|
-
resolved = resolved.replaceAll(placeholder, value)
|
|
1679
|
-
}
|
|
1680
|
-
|
|
1681
|
-
return resolved
|
|
1682
|
-
}
|
|
1683
|
-
|
|
1684
|
-
function aliasToPath(cwd, alias) {
|
|
1685
|
-
if (!alias) return ""
|
|
1686
|
-
if (path.isAbsolute(alias)) return alias
|
|
1687
|
-
if (alias.startsWith("./")) return alias.slice(2)
|
|
1688
|
-
if (alias.startsWith("../")) return alias
|
|
1689
|
-
|
|
1690
|
-
if (alias.startsWith("@/") || alias.startsWith("~/")) {
|
|
1691
|
-
const rest = alias.slice(2)
|
|
1692
|
-
return path.join(shouldUseSrcDirectory(cwd) ? "src" : "", rest)
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1695
|
-
return alias
|
|
1696
|
-
}
|
|
1697
|
-
|
|
1698
|
-
function shouldUseSrcDirectory(cwd) {
|
|
1699
|
-
return pathExistsSyncHint(cwd, "src/app") || pathExistsSyncHint(cwd, "src")
|
|
1700
|
-
}
|
|
1701
|
-
|
|
1702
|
-
function pathExistsSyncHint(cwd, segment) {
|
|
1703
|
-
return Boolean(syncExistsCache.get(path.join(cwd, segment)))
|
|
1704
|
-
}
|
|
1705
|
-
|
|
1706
|
-
async function warmPathHints(cwd) {
|
|
1707
|
-
for (const segment of ["src", "src/app"]) {
|
|
1708
|
-
const target = path.join(cwd, segment)
|
|
1709
|
-
syncExistsCache.set(target, await exists(target))
|
|
1710
|
-
}
|
|
1711
|
-
}
|
|
1712
|
-
|
|
1713
|
-
async function writeFile(targetPath, content, options) {
|
|
1714
|
-
const alreadyExists = await exists(targetPath)
|
|
1715
|
-
const isBinary = Buffer.isBuffer(content)
|
|
1716
|
-
|
|
1717
|
-
if (options.dryRun) {
|
|
1718
|
-
if (!alreadyExists) return "would write"
|
|
1719
|
-
|
|
1720
|
-
const current = await fs.readFile(targetPath)
|
|
1721
|
-
const isEqual = isBinary
|
|
1722
|
-
? current.equals(content)
|
|
1723
|
-
: current.toString("utf8") === content
|
|
1724
|
-
if (isEqual) return "would keep"
|
|
1725
|
-
|
|
1726
|
-
return "would update"
|
|
1727
|
-
}
|
|
1728
|
-
|
|
1729
|
-
if (alreadyExists && !options.force) {
|
|
1730
|
-
const current = await fs.readFile(targetPath)
|
|
1731
|
-
const isEqual = isBinary
|
|
1732
|
-
? current.equals(content)
|
|
1733
|
-
: current.toString("utf8") === content
|
|
1734
|
-
if (isEqual) return "kept"
|
|
1735
|
-
}
|
|
1736
|
-
|
|
1737
|
-
await fs.mkdir(path.dirname(targetPath), { recursive: true })
|
|
1738
|
-
await fs.writeFile(targetPath, content, isBinary ? undefined : "utf8")
|
|
1739
|
-
return alreadyExists ? "updated" : "wrote"
|
|
1740
|
-
}
|
|
1741
|
-
|
|
1742
|
-
async function readJson(filePath) {
|
|
1743
|
-
return JSON.parse(await fs.readFile(filePath, "utf8"))
|
|
1744
|
-
}
|
|
1745
|
-
|
|
1746
|
-
async function readJsonc(filePath) {
|
|
1747
|
-
const content = await fs.readFile(filePath, "utf8")
|
|
1748
|
-
return JSON.parse(stripJsonComments(content))
|
|
1749
|
-
}
|
|
1750
|
-
|
|
1751
|
-
function stripJsonComments(content) {
|
|
1752
|
-
let output = ""
|
|
1753
|
-
let inString = false
|
|
1754
|
-
let escaped = false
|
|
1755
|
-
|
|
1756
|
-
for (let index = 0; index < content.length; index += 1) {
|
|
1757
|
-
const char = content[index]
|
|
1758
|
-
const next = content[index + 1]
|
|
1759
|
-
|
|
1760
|
-
if (inString) {
|
|
1761
|
-
output += char
|
|
1762
|
-
|
|
1763
|
-
if (escaped) {
|
|
1764
|
-
escaped = false
|
|
1765
|
-
} else if (char === "\\") {
|
|
1766
|
-
escaped = true
|
|
1767
|
-
} else if (char === "\"") {
|
|
1768
|
-
inString = false
|
|
1769
|
-
}
|
|
1770
|
-
|
|
1771
|
-
continue
|
|
1772
|
-
}
|
|
1773
|
-
|
|
1774
|
-
if (char === "\"") {
|
|
1775
|
-
inString = true
|
|
1776
|
-
output += char
|
|
1777
|
-
continue
|
|
1778
|
-
}
|
|
1779
|
-
|
|
1780
|
-
if (char === "/" && next === "/") {
|
|
1781
|
-
output += " "
|
|
1782
|
-
index += 2
|
|
1783
|
-
|
|
1784
|
-
while (index < content.length && content[index] !== "\n" && content[index] !== "\r") {
|
|
1785
|
-
output += " "
|
|
1786
|
-
index += 1
|
|
1787
|
-
}
|
|
1788
|
-
|
|
1789
|
-
if (index < content.length) {
|
|
1790
|
-
output += content[index]
|
|
1791
|
-
if (content[index] === "\r" && content[index + 1] === "\n") {
|
|
1792
|
-
index += 1
|
|
1793
|
-
output += content[index]
|
|
1794
|
-
}
|
|
1795
|
-
}
|
|
1796
|
-
|
|
1797
|
-
continue
|
|
1798
|
-
}
|
|
1799
|
-
|
|
1800
|
-
if (char === "/" && next === "*") {
|
|
1801
|
-
output += " "
|
|
1802
|
-
index += 2
|
|
1803
|
-
|
|
1804
|
-
while (index < content.length) {
|
|
1805
|
-
if (content[index] === "*" && content[index + 1] === "/") {
|
|
1806
|
-
output += " "
|
|
1807
|
-
index += 1
|
|
1808
|
-
break
|
|
1809
|
-
}
|
|
1810
|
-
|
|
1811
|
-
output += content[index] === "\n" || content[index] === "\r" ? content[index] : " "
|
|
1812
|
-
index += 1
|
|
1813
|
-
}
|
|
1814
|
-
|
|
1815
|
-
continue
|
|
1816
|
-
}
|
|
1817
|
-
|
|
1818
|
-
output += char
|
|
1819
|
-
}
|
|
1820
|
-
|
|
1821
|
-
return output
|
|
1822
|
-
}
|
|
1823
|
-
|
|
1824
|
-
async function readJsonIfExists(filePath) {
|
|
1825
|
-
try {
|
|
1826
|
-
return await readJson(filePath)
|
|
1827
|
-
} catch (error) {
|
|
1828
|
-
if (error.code === "ENOENT") return null
|
|
1829
|
-
throw error
|
|
1830
|
-
}
|
|
1831
|
-
}
|
|
1832
|
-
|
|
1833
|
-
async function readTextIfExists(filePath) {
|
|
1834
|
-
try {
|
|
1835
|
-
return await fs.readFile(filePath, "utf8")
|
|
1836
|
-
} catch (error) {
|
|
1837
|
-
if (error.code === "ENOENT") return null
|
|
1838
|
-
throw error
|
|
1839
|
-
}
|
|
983
|
+
function hashNormalizedText(content) {
|
|
984
|
+
return hashContent(String(content).replace(/\r\n/g, "\n").trim())
|
|
1840
985
|
}
|
|
1841
986
|
|
|
1842
987
|
async function exists(filePath) {
|
|
@@ -1848,145 +993,11 @@ async function exists(filePath) {
|
|
|
1848
993
|
}
|
|
1849
994
|
}
|
|
1850
995
|
|
|
1851
|
-
function removeExistingTokenBlock(content, fileLabel = "CSS file") {
|
|
1852
|
-
const lines = splitLinesPreservingEndings(content)
|
|
1853
|
-
const kept = []
|
|
1854
|
-
let found = false
|
|
1855
|
-
let inTokenBlock = false
|
|
1856
|
-
|
|
1857
|
-
for (const line of lines) {
|
|
1858
|
-
const trimmed = line.trimStart()
|
|
1859
|
-
|
|
1860
|
-
if (trimmed.startsWith(CSS_START)) {
|
|
1861
|
-
if (found || inTokenBlock) {
|
|
1862
|
-
throw new Error(`${fileLabel}: multiple Banhaten token blocks found.`)
|
|
1863
|
-
}
|
|
1864
|
-
found = true
|
|
1865
|
-
inTokenBlock = true
|
|
1866
|
-
continue
|
|
1867
|
-
}
|
|
1868
|
-
|
|
1869
|
-
if (trimmed.startsWith(CSS_END)) {
|
|
1870
|
-
if (!inTokenBlock) {
|
|
1871
|
-
throw new Error(`${fileLabel}: Banhaten token end marker has no start marker.`)
|
|
1872
|
-
}
|
|
1873
|
-
inTokenBlock = false
|
|
1874
|
-
continue
|
|
1875
|
-
}
|
|
1876
|
-
|
|
1877
|
-
if (!inTokenBlock) {
|
|
1878
|
-
kept.push(line)
|
|
1879
|
-
}
|
|
1880
|
-
}
|
|
1881
|
-
|
|
1882
|
-
if (inTokenBlock) {
|
|
1883
|
-
throw new Error(`${fileLabel}: Banhaten token block is missing its end marker.`)
|
|
1884
|
-
}
|
|
1885
|
-
|
|
1886
|
-
return { content: kept.join(""), found }
|
|
1887
|
-
}
|
|
1888
|
-
|
|
1889
|
-
function ensureGlobalCssImports(content) {
|
|
1890
|
-
const cleaned = splitLinesPreservingEndings(content)
|
|
1891
|
-
.filter((line) => {
|
|
1892
|
-
const importSpecifier = readCssImportSpecifier(line)
|
|
1893
|
-
return (
|
|
1894
|
-
importSpecifier !== "tailwindcss" &&
|
|
1895
|
-
!isManagedInterFontImport(importSpecifier)
|
|
1896
|
-
)
|
|
1897
|
-
})
|
|
1898
|
-
.join("")
|
|
1899
|
-
.trimStart()
|
|
1900
|
-
return `${TAILWIND_IMPORT}\n${FONT_IMPORTS.join("\n")}\n${cleaned}`
|
|
1901
|
-
}
|
|
1902
|
-
|
|
1903
|
-
function splitLinesPreservingEndings(content) {
|
|
1904
|
-
const lines = []
|
|
1905
|
-
let lineStart = 0
|
|
1906
|
-
let index = 0
|
|
1907
|
-
|
|
1908
|
-
while (index < content.length) {
|
|
1909
|
-
if (content[index] === "\r" && content[index + 1] === "\n") {
|
|
1910
|
-
lines.push(content.slice(lineStart, index + 2))
|
|
1911
|
-
index += 2
|
|
1912
|
-
lineStart = index
|
|
1913
|
-
continue
|
|
1914
|
-
}
|
|
1915
|
-
|
|
1916
|
-
if (content[index] === "\n") {
|
|
1917
|
-
lines.push(content.slice(lineStart, index + 1))
|
|
1918
|
-
index += 1
|
|
1919
|
-
lineStart = index
|
|
1920
|
-
continue
|
|
1921
|
-
}
|
|
1922
|
-
|
|
1923
|
-
index += 1
|
|
1924
|
-
}
|
|
1925
|
-
|
|
1926
|
-
if (lineStart < content.length) {
|
|
1927
|
-
lines.push(content.slice(lineStart))
|
|
1928
|
-
}
|
|
1929
|
-
|
|
1930
|
-
return lines
|
|
1931
|
-
}
|
|
1932
|
-
|
|
1933
|
-
function readCssImportSpecifier(line) {
|
|
1934
|
-
const trimmed = line.trim()
|
|
1935
|
-
const importPrefix = "@import"
|
|
1936
|
-
if (!trimmed.startsWith(importPrefix)) return null
|
|
1937
|
-
|
|
1938
|
-
let index = importPrefix.length
|
|
1939
|
-
while (trimmed[index] === " " || trimmed[index] === "\t") {
|
|
1940
|
-
index += 1
|
|
1941
|
-
}
|
|
1942
|
-
|
|
1943
|
-
const quote = trimmed[index]
|
|
1944
|
-
if (quote !== "\"" && quote !== "'") return null
|
|
1945
|
-
|
|
1946
|
-
const start = index + 1
|
|
1947
|
-
index = start
|
|
1948
|
-
while (index < trimmed.length && trimmed[index] !== quote) {
|
|
1949
|
-
index += 1
|
|
1950
|
-
}
|
|
1951
|
-
|
|
1952
|
-
if (index >= trimmed.length) return null
|
|
1953
|
-
const specifier = trimmed.slice(start, index)
|
|
1954
|
-
index += 1
|
|
1955
|
-
|
|
1956
|
-
while (trimmed[index] === " " || trimmed[index] === "\t") {
|
|
1957
|
-
index += 1
|
|
1958
|
-
}
|
|
1959
|
-
|
|
1960
|
-
if (trimmed[index] === ";") index += 1
|
|
1961
|
-
|
|
1962
|
-
while (trimmed[index] === " " || trimmed[index] === "\t") {
|
|
1963
|
-
index += 1
|
|
1964
|
-
}
|
|
1965
|
-
|
|
1966
|
-
return index === trimmed.length ? specifier : null
|
|
1967
|
-
}
|
|
1968
|
-
|
|
1969
|
-
function isManagedInterFontImport(specifier) {
|
|
1970
|
-
if (!specifier) return false
|
|
1971
|
-
if (specifier === "@fontsource/inter") return true
|
|
1972
|
-
if (specifier === "@fontsource-variable/inter") return true
|
|
1973
|
-
|
|
1974
|
-
for (const weight of ["400", "500", "600", "700"]) {
|
|
1975
|
-
if (specifier === `@fontsource/inter/${weight}.css`) return true
|
|
1976
|
-
}
|
|
1977
|
-
|
|
1978
|
-
return false
|
|
1979
|
-
}
|
|
1980
|
-
|
|
1981
996
|
function appendAliasSegment(alias, segment) {
|
|
1982
997
|
if (!alias) return null
|
|
1983
998
|
return `${alias.replace(/\/$/, "")}/${segment}`
|
|
1984
999
|
}
|
|
1985
1000
|
|
|
1986
|
-
function stripExtension(filePath) {
|
|
1987
|
-
return filePath.replace(/\.(t|j)sx?$/, "")
|
|
1988
|
-
}
|
|
1989
|
-
|
|
1990
1001
|
function sortObject(value) {
|
|
1991
1002
|
return Object.fromEntries(
|
|
1992
1003
|
Object.entries(value).sort(([left], [right]) => left.localeCompare(right))
|
|
@@ -1996,60 +1007,3 @@ function sortObject(value) {
|
|
|
1996
1007
|
function relative(cwd, targetPath) {
|
|
1997
1008
|
return path.relative(cwd, targetPath).replaceAll("\\", "/")
|
|
1998
1009
|
}
|
|
1999
|
-
|
|
2000
|
-
function printResult(title, writes, dryRun) {
|
|
2001
|
-
if (runtimeOptions.json) {
|
|
2002
|
-
writeJson({ dryRun, title, writes })
|
|
2003
|
-
return
|
|
2004
|
-
}
|
|
2005
|
-
|
|
2006
|
-
if (runtimeOptions.silent) return
|
|
2007
|
-
|
|
2008
|
-
console.log(`${dryRun ? "Dry run: " : ""}${title}`)
|
|
2009
|
-
for (const write of writes) {
|
|
2010
|
-
console.log(`- ${write}`)
|
|
2011
|
-
}
|
|
2012
|
-
}
|
|
2013
|
-
|
|
2014
|
-
async function printInstallHint(cwd, writes, dryRun) {
|
|
2015
|
-
if (dryRun || runtimeOptions.json || runtimeOptions.silent) return
|
|
2016
|
-
|
|
2017
|
-
const packageJsonChanged = writes.some((write) =>
|
|
2018
|
-
/^(wrote|updated) package\.json$/.test(write)
|
|
2019
|
-
)
|
|
2020
|
-
|
|
2021
|
-
if (packageJsonChanged) {
|
|
2022
|
-
console.log(`\nNext: run ${await getInstallCommand(cwd)} to install the added dependencies.`)
|
|
2023
|
-
}
|
|
2024
|
-
}
|
|
2025
|
-
|
|
2026
|
-
async function getInstallCommand(cwd) {
|
|
2027
|
-
const packageJson = await readJsonIfExists(path.join(cwd, "package.json"))
|
|
2028
|
-
const declaredPackageManager = parsePackageManager(packageJson?.packageManager)
|
|
2029
|
-
|
|
2030
|
-
if (declaredPackageManager) return `${declaredPackageManager} install`
|
|
2031
|
-
|
|
2032
|
-
const lockfileManagers = [
|
|
2033
|
-
["pnpm-lock.yaml", "pnpm"],
|
|
2034
|
-
["yarn.lock", "yarn"],
|
|
2035
|
-
["bun.lockb", "bun"],
|
|
2036
|
-
["bun.lock", "bun"],
|
|
2037
|
-
["package-lock.json", "npm"],
|
|
2038
|
-
["npm-shrinkwrap.json", "npm"],
|
|
2039
|
-
]
|
|
2040
|
-
|
|
2041
|
-
for (const [lockfile, manager] of lockfileManagers) {
|
|
2042
|
-
if (await exists(path.join(cwd, lockfile))) return `${manager} install`
|
|
2043
|
-
}
|
|
2044
|
-
|
|
2045
|
-
return "npm install"
|
|
2046
|
-
}
|
|
2047
|
-
|
|
2048
|
-
function parsePackageManager(packageManager) {
|
|
2049
|
-
if (typeof packageManager !== "string") return null
|
|
2050
|
-
|
|
2051
|
-
const name = packageManager.split("@")[0]
|
|
2052
|
-
if (["npm", "pnpm", "yarn", "bun"].includes(name)) return name
|
|
2053
|
-
|
|
2054
|
-
return null
|
|
2055
|
-
}
|