banhaten 0.1.3 → 0.3.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 +73 -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-implementation-ownership-audit.md +244 -0
- package/docs/design-system/component-quality-audit-2026-08-20.md +223 -0
- package/docs/design-system/component-showcase-consistency-report.md +2 -0
- package/docs/design-system/component-token-consistency-audit.md +13 -3
- package/docs/design-system/components/README.md +4 -2
- package/docs/design-system/components/accordion.md +16 -0
- package/docs/design-system/components/activity-feed.md +17 -1
- package/docs/design-system/components/activity-heatmap.md +73 -0
- 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 +19 -1
- package/docs/design-system/components/breadcrumbs.md +17 -0
- package/docs/design-system/components/button-group.md +35 -0
- package/docs/design-system/components/button.md +18 -0
- package/docs/design-system/components/card.md +26 -1
- package/docs/design-system/components/carousel.md +20 -3
- package/docs/design-system/components/catalog-components.md +18 -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 +18 -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 +16 -1
- package/docs/design-system/components/page-header.md +17 -1
- package/docs/design-system/components/pagination.md +31 -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 +20 -1
- 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 +17 -0
- package/docs/design-system/components/skeleton.md +18 -0
- package/docs/design-system/components/slideout.md +17 -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 +19 -0
- package/docs/design-system/components/table.md +36 -4
- 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 +17 -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 +61 -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 +100 -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 +30 -15
- package/registry/component-contract.json +40 -0
- package/registry/components/alert-dialog.tsx +2 -2
- package/registry/components/alert.tsx +101 -76
- package/registry/components/autocomplete.tsx +23 -9
- package/registry/components/avatar.tsx +14 -10
- package/registry/components/badge.tsx +18 -18
- package/registry/components/button-group.tsx +324 -5
- package/registry/components/button.tsx +38 -17
- package/registry/components/card.tsx +87 -62
- package/registry/components/carousel.tsx +67 -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 +38 -4
- package/registry/components/context-menu.tsx +1 -1
- package/registry/components/date-picker.tsx +312 -135
- package/registry/components/expanded/ActivityFeed.tsx +9 -22
- package/registry/components/expanded/ActivityHeatmap.tsx +64 -0
- package/registry/components/expanded/Banner.tsx +3 -4
- package/registry/components/expanded/Breadcrumbs.tsx +8 -5
- package/registry/components/expanded/CatalogComponentsShowcase.tsx +16 -17
- package/registry/components/expanded/CatalogDivider.tsx +4 -3
- package/registry/components/expanded/EmptyState.tsx +1 -1
- package/registry/components/expanded/FileUpload.tsx +16 -7
- package/registry/components/expanded/OnboardingStepListItem.tsx +2 -6
- package/registry/components/expanded/PageHeader.tsx +47 -56
- package/registry/components/expanded/Slideout.tsx +29 -424
- package/registry/components/expanded/Steps.tsx +151 -13
- package/registry/components/expanded/Table.tsx +161 -82
- package/registry/components/expanded/Timeline.tsx +14 -27
- package/registry/components/expanded/activityFeed.css +9 -9
- package/registry/components/expanded/activityHeatmap.css +66 -0
- package/registry/components/expanded/banner.css +21 -11
- package/registry/components/expanded/breadcrumbs.css +2 -2
- package/registry/components/expanded/divider.css +1 -1
- package/registry/components/expanded/emptyState.css +1 -1
- package/registry/components/expanded/fileUpload.css +7 -7
- package/registry/components/expanded/onboardingStepListItem.css +2 -2
- package/registry/components/expanded/pageHeader.css +14 -11
- package/registry/components/expanded/slideout.css +10 -690
- package/registry/components/expanded/steps.css +78 -15
- package/registry/components/expanded/table.css +60 -69
- package/registry/components/expanded/timeline.css +19 -18
- package/registry/components/field.tsx +61 -8
- 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 +87 -251
- package/registry/components/menubar.tsx +1 -1
- package/registry/components/modal.tsx +143 -103
- package/registry/components/native-select.tsx +2 -2
- package/registry/components/navigation-menu.tsx +1 -1
- package/registry/components/pagination.css +36 -0
- package/registry/components/pagination.tsx +88 -64
- package/registry/components/popover.tsx +162 -664
- package/registry/components/progress-slider.tsx +1 -1
- package/registry/components/radio-card.tsx +116 -87
- package/registry/components/resizable.tsx +36 -2
- package/registry/components/scroll-area.tsx +30 -6
- package/registry/components/segmented-control.tsx +87 -0
- package/registry/components/select-content.tsx +2 -2
- package/registry/components/select.tsx +86 -27
- package/registry/components/sheet.tsx +1 -1
- package/registry/components/sidebar.tsx +12 -6
- package/registry/components/skeleton.css +0 -3
- package/registry/components/social-button.tsx +59 -30
- package/registry/components/spinner.tsx +18 -55
- package/registry/components/table-elements.tsx +128 -0
- package/registry/components/tabs.css +1 -1
- package/registry/components/tabs.tsx +13 -14
- package/registry/components/tag.tsx +3 -3
- package/registry/components/textarea.tsx +3 -3
- package/registry/components/toast.tsx +16 -11
- package/registry/components/toggle.tsx +4 -4
- package/registry/components/toolbar.tsx +94 -22
- package/registry/components/tooltip.tsx +2 -2
- 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 +14 -11
- 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/activity-heatmap-demo.tsx +29 -0
- package/registry/examples/expanded/banner-demo.tsx +1 -1
- package/registry/examples/expanded/breadcrumbs-demo.tsx +4 -4
- 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 +10 -7
- 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 +5 -3
- 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 +5668 -375
- package/registry/styles/globals.css +2404 -3999
- package/registry.json +156 -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 +4444 -331
- 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
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
|
|
3
|
+
export function validateReceiptDestinations(installPlan, receipt, registry) {
|
|
4
|
+
if (!receipt) return
|
|
5
|
+
|
|
6
|
+
const conflicts = []
|
|
7
|
+
const recordedDestinations = receipt.installPlan?.destinations
|
|
8
|
+
const hasCompleteInstallPlan = Object.keys(installPlan.destinations).every(
|
|
9
|
+
(name) =>
|
|
10
|
+
typeof recordedDestinations?.[name] === "string" &&
|
|
11
|
+
recordedDestinations[name].trim().length > 0
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
if (hasCompleteInstallPlan) {
|
|
15
|
+
for (const [name, expected] of Object.entries(installPlan.destinations)) {
|
|
16
|
+
const recorded = recordedDestinations[name]
|
|
17
|
+
if (
|
|
18
|
+
recorded &&
|
|
19
|
+
normalizeReceiptTarget(recorded) !== normalizeReceiptTarget(expected)
|
|
20
|
+
) {
|
|
21
|
+
conflicts.push(`${recorded} (${name})`)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
} else {
|
|
25
|
+
validateLegacyReceiptDestinations(
|
|
26
|
+
installPlan,
|
|
27
|
+
receipt,
|
|
28
|
+
registry,
|
|
29
|
+
conflicts
|
|
30
|
+
)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const previousCssTarget = receipt.tokenCss?.target
|
|
34
|
+
if (
|
|
35
|
+
previousCssTarget &&
|
|
36
|
+
normalizeReceiptTarget(previousCssTarget) !==
|
|
37
|
+
normalizeReceiptTarget(installPlan.destinations.css)
|
|
38
|
+
) {
|
|
39
|
+
conflicts.push(`${previousCssTarget} (token CSS)`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (conflicts.length > 0) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
"the existing Banhaten receipt conflicts with the canonical install plan: " +
|
|
45
|
+
`${[...new Set(conflicts)].join(", ")}. ` +
|
|
46
|
+
"Move or remove the old generated files and .banhaten/installed.json before installing to the new destination."
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function normalizeReceiptTarget(target) {
|
|
52
|
+
return path.posix
|
|
53
|
+
.normalize(String(target).replaceAll("\\", "/"))
|
|
54
|
+
.replace(/^\.\//, "")
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function normalizeReceiptComponents(components) {
|
|
58
|
+
return Object.fromEntries(
|
|
59
|
+
Object.entries(components).map(([name, record]) => [
|
|
60
|
+
name,
|
|
61
|
+
{
|
|
62
|
+
...record,
|
|
63
|
+
files: Array.isArray(record.files)
|
|
64
|
+
? record.files.map((file) => ({
|
|
65
|
+
...file,
|
|
66
|
+
target: normalizeReceiptTarget(file.target),
|
|
67
|
+
}))
|
|
68
|
+
: record.files,
|
|
69
|
+
},
|
|
70
|
+
])
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function createInstallPlanReceipt(installPlan) {
|
|
75
|
+
return {
|
|
76
|
+
aliasRoots: sortObject(
|
|
77
|
+
Object.fromEntries(
|
|
78
|
+
Object.entries(installPlan.aliasRoots).map(([name, root]) => [
|
|
79
|
+
name,
|
|
80
|
+
root.relative,
|
|
81
|
+
])
|
|
82
|
+
)
|
|
83
|
+
),
|
|
84
|
+
destinations: sortObject({ ...installPlan.destinations }),
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function validateLegacyReceiptDestinations(
|
|
89
|
+
installPlan,
|
|
90
|
+
receipt,
|
|
91
|
+
registry,
|
|
92
|
+
conflicts
|
|
93
|
+
) {
|
|
94
|
+
const targetsByOwner = new Map()
|
|
95
|
+
|
|
96
|
+
for (const [target, record] of Object.entries(receipt.managedFiles || {})) {
|
|
97
|
+
addRecordedTarget(targetsByOwner, record.owner, target)
|
|
98
|
+
}
|
|
99
|
+
for (const [name, record] of Object.entries(receipt.components || {})) {
|
|
100
|
+
for (const file of Array.isArray(record.files) ? record.files : []) {
|
|
101
|
+
if (file?.target) addRecordedTarget(targetsByOwner, name, file.target)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const [owner, targets] of targetsByOwner) {
|
|
106
|
+
if (owner === "base") {
|
|
107
|
+
const allowedTargets = new Set(
|
|
108
|
+
(registry.base.files || []).map((file) =>
|
|
109
|
+
resolveLegacyRegistryTarget(installPlan, file.target)
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
for (const target of targets) {
|
|
113
|
+
if (!allowedTargets.has(normalizeReceiptTarget(target))) {
|
|
114
|
+
conflicts.push(`${target} (${owner})`)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const allowedRoots = registry.components[owner]
|
|
121
|
+
? [installPlan.destinations.ui]
|
|
122
|
+
: []
|
|
123
|
+
|
|
124
|
+
for (const target of targets) {
|
|
125
|
+
if (
|
|
126
|
+
allowedRoots.length > 0 &&
|
|
127
|
+
!allowedRoots.some((root) => isTargetWithin(root, target))
|
|
128
|
+
) {
|
|
129
|
+
conflicts.push(`${target} (${owner})`)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function resolveLegacyRegistryTarget(installPlan, target) {
|
|
136
|
+
const replacements = {
|
|
137
|
+
"{{hooks}}": installPlan.destinations.hooks,
|
|
138
|
+
"{{ui}}": installPlan.destinations.ui,
|
|
139
|
+
"{{utils}}": installPlan.destinations.utils,
|
|
140
|
+
}
|
|
141
|
+
let resolved = target
|
|
142
|
+
for (const [placeholder, destination] of Object.entries(replacements)) {
|
|
143
|
+
resolved = resolved.replaceAll(placeholder, destination)
|
|
144
|
+
}
|
|
145
|
+
return normalizeReceiptTarget(resolved)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function addRecordedTarget(targetsByOwner, owner, target) {
|
|
149
|
+
if (!owner || !target) return
|
|
150
|
+
const targets = targetsByOwner.get(owner) ?? new Set()
|
|
151
|
+
targets.add(normalizeReceiptTarget(target))
|
|
152
|
+
targetsByOwner.set(owner, targets)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function isTargetWithin(root, target) {
|
|
156
|
+
const normalizedRoot = normalizeReceiptTarget(root)
|
|
157
|
+
const normalizedTarget = normalizeReceiptTarget(target)
|
|
158
|
+
if (normalizedRoot === ".") {
|
|
159
|
+
return normalizedTarget !== ".." && !normalizedTarget.startsWith("../")
|
|
160
|
+
}
|
|
161
|
+
return (
|
|
162
|
+
normalizedTarget === normalizedRoot ||
|
|
163
|
+
normalizedTarget.startsWith(`${normalizedRoot}/`)
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function sortObject(value) {
|
|
168
|
+
return Object.fromEntries(
|
|
169
|
+
Object.entries(value).sort(([left], [right]) => left.localeCompare(right))
|
|
170
|
+
)
|
|
171
|
+
}
|
package/src/cli/jsonc.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
// JSONC parsing and the tsconfig/jsconfig path-alias editor.
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
appendArrayElement,
|
|
5
|
+
createBracketRange,
|
|
6
|
+
expectArrayLiteralValue,
|
|
7
|
+
expectObjectLiteralValue,
|
|
8
|
+
findObjectProperty,
|
|
9
|
+
insertObjectProperty,
|
|
10
|
+
isWhitespace,
|
|
11
|
+
skipWhitespaceAndComments
|
|
12
|
+
} from "./source-scanner.js"
|
|
13
|
+
|
|
14
|
+
export function parseJsonc(content) {
|
|
15
|
+
return JSON.parse(stripTrailingJsonCommas(stripJsonComments(content)))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function stripJsonComments(content) {
|
|
19
|
+
let output = ""
|
|
20
|
+
let inString = false
|
|
21
|
+
let escaped = false
|
|
22
|
+
|
|
23
|
+
for (let index = 0; index < content.length; index += 1) {
|
|
24
|
+
const char = content[index]
|
|
25
|
+
const next = content[index + 1]
|
|
26
|
+
|
|
27
|
+
if (inString) {
|
|
28
|
+
output += char
|
|
29
|
+
|
|
30
|
+
if (escaped) {
|
|
31
|
+
escaped = false
|
|
32
|
+
} else if (char === "\\") {
|
|
33
|
+
escaped = true
|
|
34
|
+
} else if (char === "\"") {
|
|
35
|
+
inString = false
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (char === "\"") {
|
|
42
|
+
inString = true
|
|
43
|
+
output += char
|
|
44
|
+
continue
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (char === "/" && next === "/") {
|
|
48
|
+
output += " "
|
|
49
|
+
index += 2
|
|
50
|
+
|
|
51
|
+
while (index < content.length && content[index] !== "\n" && content[index] !== "\r") {
|
|
52
|
+
output += " "
|
|
53
|
+
index += 1
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (index < content.length) {
|
|
57
|
+
output += content[index]
|
|
58
|
+
if (content[index] === "\r" && content[index + 1] === "\n") {
|
|
59
|
+
index += 1
|
|
60
|
+
output += content[index]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (char === "/" && next === "*") {
|
|
68
|
+
output += " "
|
|
69
|
+
index += 2
|
|
70
|
+
|
|
71
|
+
while (index < content.length) {
|
|
72
|
+
if (content[index] === "*" && content[index + 1] === "/") {
|
|
73
|
+
output += " "
|
|
74
|
+
index += 1
|
|
75
|
+
break
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
output += content[index] === "\n" || content[index] === "\r" ? content[index] : " "
|
|
79
|
+
index += 1
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
output += char
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return output
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function stripTrailingJsonCommas(content) {
|
|
92
|
+
let output = ""
|
|
93
|
+
let inString = false
|
|
94
|
+
let escaped = false
|
|
95
|
+
|
|
96
|
+
for (let index = 0; index < content.length; index += 1) {
|
|
97
|
+
const char = content[index]
|
|
98
|
+
|
|
99
|
+
if (inString) {
|
|
100
|
+
output += char
|
|
101
|
+
if (escaped) {
|
|
102
|
+
escaped = false
|
|
103
|
+
} else if (char === "\\") {
|
|
104
|
+
escaped = true
|
|
105
|
+
} else if (char === "\"") {
|
|
106
|
+
inString = false
|
|
107
|
+
}
|
|
108
|
+
continue
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (char === "\"") {
|
|
112
|
+
inString = true
|
|
113
|
+
output += char
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (char === ",") {
|
|
118
|
+
let lookahead = index + 1
|
|
119
|
+
while (isWhitespace(content[lookahead])) lookahead += 1
|
|
120
|
+
if (content[lookahead] === "}" || content[lookahead] === "]") {
|
|
121
|
+
output += " "
|
|
122
|
+
continue
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
output += char
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return output
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// The scanner reports syntax problems without naming a dialect. Callers own
|
|
133
|
+
// that context, so a malformed tsconfig never reads as a broken Vite config.
|
|
134
|
+
function asTypescriptConfigError(error) {
|
|
135
|
+
if (error instanceof Error && !/^TypeScript config: /.test(error.message)) {
|
|
136
|
+
error.message = `TypeScript config: ${error.message}`
|
|
137
|
+
}
|
|
138
|
+
return error
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function updateJsoncAliasSource(source, options) {
|
|
142
|
+
try {
|
|
143
|
+
return updateJsoncAliasSourceUnchecked(source, options)
|
|
144
|
+
} catch (error) {
|
|
145
|
+
throw asTypescriptConfigError(error)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function updateJsoncAliasSourceUnchecked(source, { aliasPattern, targetPattern }) {
|
|
150
|
+
const rootStart = skipWhitespaceAndComments(source, 0)
|
|
151
|
+
if (source[rootStart] !== "{") {
|
|
152
|
+
throw new Error("TypeScript config must contain an object literal")
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const rootRange = createBracketRange(source, rootStart, "{")
|
|
156
|
+
const compilerOptionsProperty = findObjectProperty(
|
|
157
|
+
source,
|
|
158
|
+
rootRange,
|
|
159
|
+
"compilerOptions"
|
|
160
|
+
)
|
|
161
|
+
const aliasPropertyText = `${JSON.stringify(aliasPattern)}: [${JSON.stringify(targetPattern)}],`
|
|
162
|
+
|
|
163
|
+
if (!compilerOptionsProperty) {
|
|
164
|
+
return insertObjectProperty(
|
|
165
|
+
source,
|
|
166
|
+
rootRange,
|
|
167
|
+
`"compilerOptions": {\n "paths": {\n ${aliasPropertyText}\n },\n},`
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const compilerOptionsRange = expectObjectLiteralValue(
|
|
172
|
+
source,
|
|
173
|
+
compilerOptionsProperty,
|
|
174
|
+
"TypeScript compilerOptions must be an object literal"
|
|
175
|
+
)
|
|
176
|
+
const pathsProperty = findObjectProperty(source, compilerOptionsRange, "paths")
|
|
177
|
+
|
|
178
|
+
if (!pathsProperty) {
|
|
179
|
+
return insertObjectProperty(
|
|
180
|
+
source,
|
|
181
|
+
compilerOptionsRange,
|
|
182
|
+
`"paths": {\n ${aliasPropertyText}\n},`
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const pathsRange = expectObjectLiteralValue(
|
|
187
|
+
source,
|
|
188
|
+
pathsProperty,
|
|
189
|
+
"TypeScript compilerOptions.paths must be an object literal"
|
|
190
|
+
)
|
|
191
|
+
const aliasProperty = findObjectProperty(source, pathsRange, aliasPattern)
|
|
192
|
+
|
|
193
|
+
if (!aliasProperty) {
|
|
194
|
+
return insertObjectProperty(source, pathsRange, aliasPropertyText)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const aliasTargets = expectArrayLiteralValue(
|
|
198
|
+
source,
|
|
199
|
+
aliasProperty,
|
|
200
|
+
`TypeScript path ${aliasPattern} must be an array literal`
|
|
201
|
+
)
|
|
202
|
+
return appendArrayElement(source, aliasTargets, JSON.stringify(targetPattern))
|
|
203
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import fs from "node:fs/promises"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
|
|
4
|
+
import { inspectPackageManagerContext } from "./package-manager.js"
|
|
5
|
+
import {
|
|
6
|
+
dependencyVersionSatisfies,
|
|
7
|
+
findPackageDependencies,
|
|
8
|
+
} from "./package-version.js"
|
|
9
|
+
|
|
10
|
+
const DEPENDENCY_SECTIONS = [
|
|
11
|
+
"dependencies",
|
|
12
|
+
"devDependencies",
|
|
13
|
+
"peerDependencies",
|
|
14
|
+
"optionalDependencies",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
export async function inspectPackageInstallState({
|
|
18
|
+
cwd,
|
|
19
|
+
packageJson,
|
|
20
|
+
requirements,
|
|
21
|
+
}) {
|
|
22
|
+
const managerState = await inspectPackageManagerContext(cwd)
|
|
23
|
+
const { context } = managerState
|
|
24
|
+
const installedPackages = new Map()
|
|
25
|
+
|
|
26
|
+
for (const requirement of requirements) {
|
|
27
|
+
const installed = await findInstalledPackage(
|
|
28
|
+
cwd,
|
|
29
|
+
context.root,
|
|
30
|
+
requirement.name
|
|
31
|
+
)
|
|
32
|
+
if (
|
|
33
|
+
!installed ||
|
|
34
|
+
!dependencyVersionSatisfies(installed.version, requirement.range)
|
|
35
|
+
) {
|
|
36
|
+
return { evidence: [], required: true }
|
|
37
|
+
}
|
|
38
|
+
installedPackages.set(requirement.name, installed)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!context.lockfilePath) {
|
|
42
|
+
return {
|
|
43
|
+
evidence: createCleanEvidence(managerState, installedPackages),
|
|
44
|
+
required: false,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (context.manager !== "npm") {
|
|
49
|
+
return { evidence: [], required: true }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const lockEvidence = await readJsonEvidence(context.lockfilePath)
|
|
53
|
+
if (!lockEvidence) return { evidence: [], required: true }
|
|
54
|
+
|
|
55
|
+
const lockState = inspectNpmLock({
|
|
56
|
+
cwd,
|
|
57
|
+
lock: lockEvidence.value,
|
|
58
|
+
lockRoot: context.root,
|
|
59
|
+
packageJson,
|
|
60
|
+
requirements,
|
|
61
|
+
})
|
|
62
|
+
if (!lockState.compatible) return { evidence: [], required: true }
|
|
63
|
+
|
|
64
|
+
for (const [name, installed] of installedPackages) {
|
|
65
|
+
if (installed.version !== lockState.lockedVersions.get(name)) {
|
|
66
|
+
return { evidence: [], required: true }
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
evidence: createCleanEvidence(
|
|
72
|
+
managerState,
|
|
73
|
+
installedPackages,
|
|
74
|
+
[lockEvidence]
|
|
75
|
+
),
|
|
76
|
+
required: false,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function createCleanEvidence(
|
|
81
|
+
managerState,
|
|
82
|
+
installedPackages,
|
|
83
|
+
extra = []
|
|
84
|
+
) {
|
|
85
|
+
return [
|
|
86
|
+
...managerState.evidence,
|
|
87
|
+
...extra,
|
|
88
|
+
...[...installedPackages.values()].map(toEvidence),
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function inspectNpmLock({
|
|
93
|
+
cwd,
|
|
94
|
+
lock,
|
|
95
|
+
lockRoot,
|
|
96
|
+
packageJson,
|
|
97
|
+
requirements,
|
|
98
|
+
}) {
|
|
99
|
+
if (
|
|
100
|
+
!lock ||
|
|
101
|
+
typeof lock !== "object" ||
|
|
102
|
+
Array.isArray(lock) ||
|
|
103
|
+
![2, 3].includes(lock.lockfileVersion) ||
|
|
104
|
+
!lock.packages ||
|
|
105
|
+
typeof lock.packages !== "object" ||
|
|
106
|
+
Array.isArray(lock.packages)
|
|
107
|
+
) {
|
|
108
|
+
return { compatible: false, lockedVersions: new Map() }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const importerKey = path.relative(lockRoot, cwd).replaceAll("\\", "/")
|
|
112
|
+
const importer = lock.packages[importerKey]
|
|
113
|
+
if (!importer || typeof importer !== "object" || Array.isArray(importer)) {
|
|
114
|
+
return { compatible: false, lockedVersions: new Map() }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const section of DEPENDENCY_SECTIONS) {
|
|
118
|
+
if (!dependencySectionMatches(packageJson[section], importer[section])) {
|
|
119
|
+
return { compatible: false, lockedVersions: new Map() }
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const lockedVersions = new Map()
|
|
124
|
+
for (const requirement of requirements) {
|
|
125
|
+
const lockedVersion = findModernLockedVersion(
|
|
126
|
+
lock.packages,
|
|
127
|
+
importerKey,
|
|
128
|
+
requirement.name
|
|
129
|
+
)
|
|
130
|
+
const declarations = findPackageDependencies(packageJson, requirement.name)
|
|
131
|
+
if (
|
|
132
|
+
!dependencyVersionSatisfies(lockedVersion, requirement.range) ||
|
|
133
|
+
declarations.length === 0 ||
|
|
134
|
+
declarations.some(
|
|
135
|
+
({ version }) => !dependencyVersionSatisfies(lockedVersion, version)
|
|
136
|
+
)
|
|
137
|
+
) {
|
|
138
|
+
return { compatible: false, lockedVersions: new Map() }
|
|
139
|
+
}
|
|
140
|
+
lockedVersions.set(requirement.name, lockedVersion)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return { compatible: true, lockedVersions }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function dependencySectionMatches(manifestSection, lockSection) {
|
|
147
|
+
const manifest = normalizeDependencySection(manifestSection)
|
|
148
|
+
const locked = normalizeDependencySection(lockSection)
|
|
149
|
+
if (!manifest || !locked) return false
|
|
150
|
+
const manifestEntries = Object.entries(manifest)
|
|
151
|
+
const lockedEntries = Object.entries(locked)
|
|
152
|
+
|
|
153
|
+
return manifestEntries.length === lockedEntries.length &&
|
|
154
|
+
manifestEntries.every(([name, range]) => locked[name] === range)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function normalizeDependencySection(value) {
|
|
158
|
+
if (value === undefined) return {}
|
|
159
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
160
|
+
? value
|
|
161
|
+
: null
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function findModernLockedVersion(packages, importerKey, name) {
|
|
165
|
+
let directory = importerKey
|
|
166
|
+
|
|
167
|
+
while (true) {
|
|
168
|
+
const prefix = directory ? `${directory}/` : ""
|
|
169
|
+
const entryKey = `${prefix}node_modules/${name}`
|
|
170
|
+
if (Object.hasOwn(packages, entryKey)) {
|
|
171
|
+
const entry = packages[entryKey]
|
|
172
|
+
return entry && typeof entry.version === "string" && !entry.link
|
|
173
|
+
? entry.version
|
|
174
|
+
: null
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!directory) return null
|
|
178
|
+
const parent = path.posix.dirname(directory)
|
|
179
|
+
directory = parent === "." ? "" : parent
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function findInstalledPackage(cwd, projectRoot, name) {
|
|
184
|
+
for (const nodeModulesPath of nodeModulesSearchPaths(cwd, projectRoot)) {
|
|
185
|
+
const manifestPath = path.join(
|
|
186
|
+
nodeModulesPath,
|
|
187
|
+
...name.split("/"),
|
|
188
|
+
"package.json"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
const source = await fs.readFile(manifestPath)
|
|
193
|
+
const manifest = JSON.parse(source.toString("utf8"))
|
|
194
|
+
if (manifest.name === name && typeof manifest.version === "string") {
|
|
195
|
+
return { path: manifestPath, source, version: manifest.version }
|
|
196
|
+
}
|
|
197
|
+
return null
|
|
198
|
+
} catch (error) {
|
|
199
|
+
if (error.code === "ENOENT") continue
|
|
200
|
+
if (error instanceof SyntaxError) return null
|
|
201
|
+
throw error
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return null
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function nodeModulesSearchPaths(cwd, projectRoot) {
|
|
209
|
+
const paths = []
|
|
210
|
+
const boundary = path.resolve(projectRoot)
|
|
211
|
+
let directory = path.resolve(cwd)
|
|
212
|
+
|
|
213
|
+
while (true) {
|
|
214
|
+
paths.push(path.join(directory, "node_modules"))
|
|
215
|
+
if (directory === boundary) return paths
|
|
216
|
+
|
|
217
|
+
const parent = path.dirname(directory)
|
|
218
|
+
if (parent === directory || !isWithin(parent, boundary)) return paths
|
|
219
|
+
directory = parent
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function isWithin(directory, boundary) {
|
|
224
|
+
const relative = path.relative(boundary, directory)
|
|
225
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative))
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function readJsonEvidence(filePath) {
|
|
229
|
+
const evidence = await readFileEvidence(filePath)
|
|
230
|
+
if (!evidence) return null
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
return {
|
|
234
|
+
...evidence,
|
|
235
|
+
value: JSON.parse(evidence.source.toString("utf8")),
|
|
236
|
+
}
|
|
237
|
+
} catch (error) {
|
|
238
|
+
if (error instanceof SyntaxError) return null
|
|
239
|
+
throw error
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function readFileEvidence(filePath) {
|
|
244
|
+
try {
|
|
245
|
+
return { path: filePath, source: await fs.readFile(filePath) }
|
|
246
|
+
} catch (error) {
|
|
247
|
+
if (error.code === "ENOENT") return null
|
|
248
|
+
throw error
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function toEvidence({ path: evidencePath, source }) {
|
|
253
|
+
return { path: evidencePath, source }
|
|
254
|
+
}
|