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
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import fs from "node:fs/promises"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
import { minimatch } from "minimatch"
|
|
4
|
+
import { parse as parseYaml } from "yaml"
|
|
5
|
+
|
|
6
|
+
const LOCKFILES = [
|
|
7
|
+
{ manager: "pnpm", name: "pnpm-lock.yaml" },
|
|
8
|
+
{ manager: "yarn", name: "yarn.lock" },
|
|
9
|
+
{ manager: "bun", name: "bun.lockb" },
|
|
10
|
+
{ manager: "bun", name: "bun.lock" },
|
|
11
|
+
{ manager: "npm", name: "npm-shrinkwrap.json" },
|
|
12
|
+
{ manager: "npm", name: "package-lock.json" },
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
export function packageManagerStatePaths(root) {
|
|
16
|
+
return [
|
|
17
|
+
path.join(root, "package.json"),
|
|
18
|
+
path.join(root, "pnpm-workspace.yaml"),
|
|
19
|
+
...LOCKFILES.map(({ name }) => path.join(root, name)),
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function inspectPackageManagerContext(cwd) {
|
|
24
|
+
const context = await resolvePackageManagerContext(cwd)
|
|
25
|
+
const paths = new Set(
|
|
26
|
+
context.evidenceRoots.flatMap((root) => packageManagerStatePaths(root))
|
|
27
|
+
)
|
|
28
|
+
const evidence = await Promise.all(
|
|
29
|
+
[...paths].map(async (filePath) => {
|
|
30
|
+
try {
|
|
31
|
+
return { path: filePath, source: await fs.readFile(filePath) }
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (error.code === "ENOENT") return { path: filePath, source: null }
|
|
34
|
+
throw error
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
)
|
|
38
|
+
const confirmedContext = await resolvePackageManagerContext(cwd)
|
|
39
|
+
if (!samePackageManagerContext(context, confirmedContext)) {
|
|
40
|
+
throw new Error("package-manager context changed while preparing the install plan")
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return { context, evidence }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function resolvePackageManagerContext(cwd) {
|
|
47
|
+
const projectRoot = path.resolve(cwd)
|
|
48
|
+
const localContext = await inspectPackageManagerRoot(projectRoot)
|
|
49
|
+
const workspace = await findWorkspaceRoot(projectRoot)
|
|
50
|
+
if (workspace.root) {
|
|
51
|
+
const workspaceContext = await inspectPackageManagerRoot(workspace.root)
|
|
52
|
+
if (workspaceContext) {
|
|
53
|
+
if (
|
|
54
|
+
localContext &&
|
|
55
|
+
localContext.manager !== workspaceContext.manager
|
|
56
|
+
) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`${projectRoot} declares packageManager "${localContext.manager}", ` +
|
|
59
|
+
`but its workspace root uses "${workspaceContext.manager}".`
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
return withEvidenceRoots(workspaceContext, workspace.evidenceRoots)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (localContext) {
|
|
67
|
+
return withEvidenceRoots(localContext, workspace.evidenceRoots)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
evidenceRoots: workspace.evidenceRoots,
|
|
72
|
+
lockfilePath: null,
|
|
73
|
+
manager: "npm",
|
|
74
|
+
root: projectRoot,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function inspectPackageManagerRoot(root) {
|
|
79
|
+
const packageJson = await readJsonIfValid(path.join(root, "package.json"))
|
|
80
|
+
const declaredManager = parsePackageManager(packageJson?.packageManager)
|
|
81
|
+
|
|
82
|
+
if (declaredManager) {
|
|
83
|
+
const lockfile = await findManagerLockfile(root, declaredManager)
|
|
84
|
+
return {
|
|
85
|
+
lockfilePath: lockfile ? path.join(root, lockfile.name) : null,
|
|
86
|
+
manager: declaredManager,
|
|
87
|
+
root,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const lockfiles = []
|
|
92
|
+
for (const lockfile of LOCKFILES) {
|
|
93
|
+
if (await exists(path.join(root, lockfile.name))) lockfiles.push(lockfile)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const managers = new Set(lockfiles.map(({ manager }) => manager))
|
|
97
|
+
if (managers.size > 1) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`${root} contains lockfiles for multiple package managers. ` +
|
|
100
|
+
"Remove stale lockfiles or declare packageManager in package.json."
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (lockfiles.length > 0) {
|
|
105
|
+
return {
|
|
106
|
+
lockfilePath: path.join(root, lockfiles[0].name),
|
|
107
|
+
manager: lockfiles[0].manager,
|
|
108
|
+
root,
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function findWorkspaceRoot(projectRoot) {
|
|
116
|
+
let directory = projectRoot
|
|
117
|
+
const evidenceRoots = []
|
|
118
|
+
|
|
119
|
+
while (true) {
|
|
120
|
+
evidenceRoots.push(directory)
|
|
121
|
+
if (directory !== projectRoot) {
|
|
122
|
+
if (await isWorkspaceRoot(directory, projectRoot)) {
|
|
123
|
+
return { evidenceRoots, root: directory }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (await exists(path.join(directory, ".git"))) {
|
|
128
|
+
return { evidenceRoots, root: null }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const parent = path.dirname(directory)
|
|
132
|
+
if (parent === directory) return { evidenceRoots, root: null }
|
|
133
|
+
directory = parent
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function withEvidenceRoots(context, evidenceRoots) {
|
|
138
|
+
return { ...context, evidenceRoots }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function samePackageManagerContext(left, right) {
|
|
142
|
+
return left.manager === right.manager &&
|
|
143
|
+
left.root === right.root &&
|
|
144
|
+
left.lockfilePath === right.lockfilePath &&
|
|
145
|
+
left.evidenceRoots.join("\0") === right.evidenceRoots.join("\0")
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function isWorkspaceRoot(candidate, projectRoot) {
|
|
149
|
+
const relativeProject = path.relative(candidate, projectRoot).replaceAll("\\", "/")
|
|
150
|
+
const pnpmPatterns = await readPnpmWorkspacePatterns(
|
|
151
|
+
path.join(candidate, "pnpm-workspace.yaml")
|
|
152
|
+
)
|
|
153
|
+
if (pnpmPatterns && workspacePatternsMatch(pnpmPatterns, relativeProject)) {
|
|
154
|
+
return true
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const packageJson = await readJsonIfValid(path.join(candidate, "package.json"))
|
|
158
|
+
const patterns = Array.isArray(packageJson?.workspaces)
|
|
159
|
+
? packageJson.workspaces
|
|
160
|
+
: packageJson?.workspaces?.packages
|
|
161
|
+
|
|
162
|
+
if (!Array.isArray(patterns)) return false
|
|
163
|
+
return workspacePatternsMatch(patterns, relativeProject)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function workspacePatternsMatch(patterns, value) {
|
|
167
|
+
const validPatterns = patterns.filter(
|
|
168
|
+
(pattern) => typeof pattern === "string" && pattern.length <= 512
|
|
169
|
+
)
|
|
170
|
+
const included = validPatterns
|
|
171
|
+
.filter((pattern) => !pattern.startsWith("!"))
|
|
172
|
+
.some((pattern) => workspacePatternMatches(pattern, value))
|
|
173
|
+
const excluded = validPatterns
|
|
174
|
+
.filter((pattern) => pattern.startsWith("!"))
|
|
175
|
+
.some((pattern) => workspacePatternMatches(pattern.slice(1), value))
|
|
176
|
+
|
|
177
|
+
return included && !excluded
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function workspacePatternMatches(pattern, value) {
|
|
181
|
+
try {
|
|
182
|
+
return minimatch(value, pattern.replace(/^\.\//, "").replace(/\/$/, ""), {
|
|
183
|
+
dot: true,
|
|
184
|
+
nocomment: true,
|
|
185
|
+
nonegate: true,
|
|
186
|
+
})
|
|
187
|
+
} catch {
|
|
188
|
+
return false
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function readPnpmWorkspacePatterns(filePath) {
|
|
193
|
+
try {
|
|
194
|
+
const workspace = parseYaml(await fs.readFile(filePath, "utf8"))
|
|
195
|
+
return Array.isArray(workspace?.packages) ? workspace.packages : null
|
|
196
|
+
} catch (error) {
|
|
197
|
+
if (error.code === "ENOENT" || error.name === "YAMLParseError") return null
|
|
198
|
+
throw error
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function findManagerLockfile(root, manager) {
|
|
203
|
+
for (const lockfile of LOCKFILES) {
|
|
204
|
+
if (
|
|
205
|
+
lockfile.manager === manager &&
|
|
206
|
+
await exists(path.join(root, lockfile.name))
|
|
207
|
+
) {
|
|
208
|
+
return lockfile
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return null
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function readJsonIfValid(filePath) {
|
|
216
|
+
try {
|
|
217
|
+
return JSON.parse(await fs.readFile(filePath, "utf8"))
|
|
218
|
+
} catch (error) {
|
|
219
|
+
if (error.code === "ENOENT" || error instanceof SyntaxError) return null
|
|
220
|
+
throw error
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function exists(filePath) {
|
|
225
|
+
try {
|
|
226
|
+
await fs.access(filePath)
|
|
227
|
+
return true
|
|
228
|
+
} catch {
|
|
229
|
+
return false
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function parsePackageManager(packageManager) {
|
|
234
|
+
if (typeof packageManager !== "string") return null
|
|
235
|
+
|
|
236
|
+
const name = packageManager.split("@")[0]
|
|
237
|
+
return ["npm", "pnpm", "yarn", "bun"].includes(name) ? name : null
|
|
238
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import {
|
|
2
|
+
findPackageDependencies,
|
|
3
|
+
reconcileDependencyRange,
|
|
4
|
+
} from "./package-version.js"
|
|
5
|
+
|
|
6
|
+
const DEPENDENCY_SECTIONS = [
|
|
7
|
+
"dependencies",
|
|
8
|
+
"devDependencies",
|
|
9
|
+
"peerDependencies",
|
|
10
|
+
"optionalDependencies",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
export function createPackageDependencyPlan(packageJson, changes) {
|
|
14
|
+
if (!packageJson || typeof packageJson !== "object" || Array.isArray(packageJson)) {
|
|
15
|
+
throw new Error("package.json root must be an object.")
|
|
16
|
+
}
|
|
17
|
+
validatePackageDependencySections(packageJson)
|
|
18
|
+
const requirements = collectPackageRequirements(changes)
|
|
19
|
+
const operations = []
|
|
20
|
+
|
|
21
|
+
for (const requirement of requirements.values()) {
|
|
22
|
+
const declarations = findPackageDependencies(packageJson, requirement.name)
|
|
23
|
+
|
|
24
|
+
if (declarations.length === 0) {
|
|
25
|
+
operations.push({
|
|
26
|
+
kind: "add",
|
|
27
|
+
name: requirement.name,
|
|
28
|
+
range: requirement.range,
|
|
29
|
+
section: requirement.section,
|
|
30
|
+
})
|
|
31
|
+
continue
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
for (const declaration of declarations) {
|
|
35
|
+
const resolution = reconcileDependencyRange(
|
|
36
|
+
declaration.version,
|
|
37
|
+
requirement.range
|
|
38
|
+
)
|
|
39
|
+
assertDependencyResolution(
|
|
40
|
+
requirement.name,
|
|
41
|
+
declaration,
|
|
42
|
+
requirement.range,
|
|
43
|
+
resolution
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
if (resolution.status === "strengthened") {
|
|
47
|
+
operations.push({
|
|
48
|
+
kind: "strengthen",
|
|
49
|
+
name: requirement.name,
|
|
50
|
+
range: resolution.range,
|
|
51
|
+
section: declaration.section,
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
operations,
|
|
59
|
+
requirements: [...requirements.values()],
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function applyPackageDependencyPlan(packageJson, operations) {
|
|
64
|
+
const touchedSections = new Set()
|
|
65
|
+
|
|
66
|
+
for (const operation of operations) {
|
|
67
|
+
const dependencies = packageJson[operation.section] || {}
|
|
68
|
+
dependencies[operation.name] = operation.range
|
|
69
|
+
packageJson[operation.section] = dependencies
|
|
70
|
+
touchedSections.add(operation.section)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const section of touchedSections) {
|
|
74
|
+
packageJson[section] = sortObject(packageJson[section])
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function mergeDependencyRequirements(target, incoming, source) {
|
|
79
|
+
for (const [name, range] of Object.entries(incoming)) {
|
|
80
|
+
if (!Object.hasOwn(target, name)) {
|
|
81
|
+
target[name] = range
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const resolution = reconcileDependencyRange(target[name], range)
|
|
86
|
+
if (resolution.status === "satisfied") continue
|
|
87
|
+
if (resolution.status === "strengthened") {
|
|
88
|
+
target[name] = resolution.range
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
throw new Error(
|
|
93
|
+
`${source} declares dependency "${name}" as "${range}", which cannot be combined safely with "${target[name]}".`
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function collectPackageRequirements(changes) {
|
|
99
|
+
const requirements = new Map()
|
|
100
|
+
|
|
101
|
+
for (const section of ["dependencies", "devDependencies"]) {
|
|
102
|
+
for (const [name, range] of Object.entries(changes[section] || {})) {
|
|
103
|
+
const validation = reconcileDependencyRange(range, range)
|
|
104
|
+
if (validation.status === "invalid-required") {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`Banhaten declares an invalid semantic version range for "${name}": "${range}".`
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const current = requirements.get(name)
|
|
111
|
+
if (!current) {
|
|
112
|
+
requirements.set(name, { name, range, section })
|
|
113
|
+
continue
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (section === "dependencies") current.section = section
|
|
117
|
+
|
|
118
|
+
const resolution = reconcileDependencyRange(current.range, range)
|
|
119
|
+
if (resolution.status === "satisfied") continue
|
|
120
|
+
if (resolution.status === "strengthened") {
|
|
121
|
+
current.range = resolution.range
|
|
122
|
+
continue
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
throw new Error(
|
|
126
|
+
`Banhaten registry dependency conflict for "${name}": ` +
|
|
127
|
+
`"${current.range}" and "${range}" cannot be combined safely.`
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return requirements
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function validatePackageDependencySections(packageJson) {
|
|
136
|
+
for (const section of DEPENDENCY_SECTIONS) {
|
|
137
|
+
const dependencies = packageJson[section]
|
|
138
|
+
if (
|
|
139
|
+
dependencies !== undefined &&
|
|
140
|
+
(!dependencies ||
|
|
141
|
+
typeof dependencies !== "object" ||
|
|
142
|
+
Array.isArray(dependencies))
|
|
143
|
+
) {
|
|
144
|
+
throw new Error(`package.json ${section} must be an object.`)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function assertDependencyResolution(name, declaration, requiredRange, resolution) {
|
|
150
|
+
if (resolution.status === "satisfied" || resolution.status === "strengthened") {
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (resolution.status === "unsupported") {
|
|
155
|
+
throw new Error(
|
|
156
|
+
`dependency version conflict for "${name}": ${declaration.section} declares unsupported non-semver spec ` +
|
|
157
|
+
`"${declaration.version}", but Banhaten requires "${requiredRange}".`
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (resolution.status === "partial-overlap") {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`dependency range conflict for "${name}": ${declaration.section} declares "${declaration.version}", ` +
|
|
164
|
+
`which only partially overlaps Banhaten's required "${requiredRange}". ` +
|
|
165
|
+
"Declare a range contained by both requirements."
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (resolution.status === "invalid-required") {
|
|
170
|
+
throw new Error(
|
|
171
|
+
`Banhaten declares an invalid semantic version range for "${name}": "${requiredRange}".`
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
throw new Error(
|
|
176
|
+
`dependency version conflict for "${name}": ${declaration.section} declares "${declaration.version}", ` +
|
|
177
|
+
`but Banhaten requires "${requiredRange}".`
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function sortObject(value) {
|
|
182
|
+
return Object.fromEntries(
|
|
183
|
+
Object.entries(value).sort(([left], [right]) => left.localeCompare(right))
|
|
184
|
+
)
|
|
185
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import semver from "semver"
|
|
2
|
+
|
|
3
|
+
const DEPENDENCY_SECTIONS = [
|
|
4
|
+
"dependencies",
|
|
5
|
+
"devDependencies",
|
|
6
|
+
"peerDependencies",
|
|
7
|
+
"optionalDependencies",
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
export function findPackageDependencies(packageJson, name) {
|
|
11
|
+
const declarations = []
|
|
12
|
+
|
|
13
|
+
for (const section of DEPENDENCY_SECTIONS) {
|
|
14
|
+
const dependencies = packageJson[section]
|
|
15
|
+
if (
|
|
16
|
+
dependencies &&
|
|
17
|
+
typeof dependencies === "object" &&
|
|
18
|
+
!Array.isArray(dependencies) &&
|
|
19
|
+
Object.hasOwn(dependencies, name)
|
|
20
|
+
) {
|
|
21
|
+
declarations.push({
|
|
22
|
+
section,
|
|
23
|
+
version: dependencies[name],
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return declarations
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function reconcileDependencyRange(declaredRange, requiredRange) {
|
|
32
|
+
const required = parseRange(requiredRange)
|
|
33
|
+
if (required.status !== "valid") {
|
|
34
|
+
return {
|
|
35
|
+
status: "invalid-required",
|
|
36
|
+
range: requiredRange,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const declared = parseRange(declaredRange)
|
|
41
|
+
if (declared.status === "invalid") {
|
|
42
|
+
return {
|
|
43
|
+
status: "unsupported",
|
|
44
|
+
range: declaredRange,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (declared.status === "empty") {
|
|
48
|
+
return {
|
|
49
|
+
status: "conflict",
|
|
50
|
+
range: declaredRange,
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (semver.subset(declared.range, required.range)) {
|
|
55
|
+
return {
|
|
56
|
+
status: "satisfied",
|
|
57
|
+
range: declaredRange,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (semver.subset(required.range, declared.range)) {
|
|
62
|
+
return {
|
|
63
|
+
status: "strengthened",
|
|
64
|
+
range: requiredRange.trim(),
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (semver.intersects(declared.range, required.range)) {
|
|
69
|
+
return {
|
|
70
|
+
status: "partial-overlap",
|
|
71
|
+
range: declaredRange,
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
status: "conflict",
|
|
77
|
+
range: declaredRange,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function dependencyVersionSatisfies(version, requiredRange) {
|
|
82
|
+
if (typeof version !== "string" || semver.valid(version) === null) return false
|
|
83
|
+
const required = parseRange(requiredRange)
|
|
84
|
+
if (required.status !== "valid") return false
|
|
85
|
+
return semver.satisfies(version, required.range)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parseRange(value) {
|
|
89
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
90
|
+
return { status: "invalid" }
|
|
91
|
+
}
|
|
92
|
+
if (semver.validRange(value) === null) return { status: "invalid" }
|
|
93
|
+
|
|
94
|
+
const range = new semver.Range(value)
|
|
95
|
+
if (semver.minVersion(range) === null) return { status: "empty" }
|
|
96
|
+
return { range, status: "valid" }
|
|
97
|
+
}
|