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.
Files changed (258) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/CONTRIBUTING.md +44 -0
  3. package/README.md +65 -13
  4. package/SECURITY.md +15 -0
  5. package/banhaten.config.example.json +0 -2
  6. package/docs/design-system/README.md +9 -0
  7. package/docs/design-system/adoption-quality-remediation.md +87 -0
  8. package/docs/design-system/agent-usage.md +13 -0
  9. package/docs/design-system/appearance-presets.md +6 -1
  10. package/docs/design-system/component-docs-pattern.md +81 -0
  11. package/docs/design-system/component-token-consistency-audit.md +13 -3
  12. package/docs/design-system/components/README.md +3 -2
  13. package/docs/design-system/components/accordion.md +16 -0
  14. package/docs/design-system/components/activity-feed.md +15 -1
  15. package/docs/design-system/components/alert-dialog.md +17 -0
  16. package/docs/design-system/components/alert.md +46 -14
  17. package/docs/design-system/components/aspect-ratio.md +15 -0
  18. package/docs/design-system/components/attribute.md +16 -0
  19. package/docs/design-system/components/autocomplete.md +19 -0
  20. package/docs/design-system/components/avatar.md +18 -0
  21. package/docs/design-system/components/badge.md +19 -0
  22. package/docs/design-system/components/banner.md +15 -1
  23. package/docs/design-system/components/breadcrumbs.md +14 -0
  24. package/docs/design-system/components/button-group.md +34 -0
  25. package/docs/design-system/components/button.md +18 -0
  26. package/docs/design-system/components/card.md +18 -1
  27. package/docs/design-system/components/carousel.md +20 -3
  28. package/docs/design-system/components/catalog-components.md +15 -1
  29. package/docs/design-system/components/chart.md +15 -0
  30. package/docs/design-system/components/checkbox.md +16 -0
  31. package/docs/design-system/components/collapsible.md +15 -0
  32. package/docs/design-system/components/command.md +15 -0
  33. package/docs/design-system/components/context-menu.md +15 -0
  34. package/docs/design-system/components/date-picker.md +21 -1
  35. package/docs/design-system/components/divider.md +16 -0
  36. package/docs/design-system/components/empty-state.md +17 -0
  37. package/docs/design-system/components/field.md +17 -0
  38. package/docs/design-system/components/file-upload.md +14 -0
  39. package/docs/design-system/components/hover-card.md +15 -0
  40. package/docs/design-system/components/input-group.md +17 -0
  41. package/docs/design-system/components/input-otp.md +18 -0
  42. package/docs/design-system/components/input.md +43 -7
  43. package/docs/design-system/components/item.md +72 -0
  44. package/docs/design-system/components/kbd.md +18 -0
  45. package/docs/design-system/components/label.md +16 -0
  46. package/docs/design-system/components/menu.md +17 -0
  47. package/docs/design-system/components/menubar.md +15 -0
  48. package/docs/design-system/components/modal.md +25 -4
  49. package/docs/design-system/components/native-select.md +18 -0
  50. package/docs/design-system/components/navigation-menu.md +15 -0
  51. package/docs/design-system/components/onboarding-step-list-item.md +15 -1
  52. package/docs/design-system/components/page-header.md +15 -1
  53. package/docs/design-system/components/pagination.md +30 -8
  54. package/docs/design-system/components/popover.md +19 -0
  55. package/docs/design-system/components/progress-slider.md +18 -0
  56. package/docs/design-system/components/progress.md +17 -0
  57. package/docs/design-system/components/radio-card.md +16 -0
  58. package/docs/design-system/components/radio-group.md +17 -0
  59. package/docs/design-system/components/resizable.md +15 -0
  60. package/docs/design-system/components/scroll-area.md +15 -0
  61. package/docs/design-system/components/segmented-control.md +71 -0
  62. package/docs/design-system/components/select.md +44 -7
  63. package/docs/design-system/components/sheet.md +17 -0
  64. package/docs/design-system/components/sidebar.md +15 -0
  65. package/docs/design-system/components/skeleton.md +18 -0
  66. package/docs/design-system/components/slideout.md +14 -0
  67. package/docs/design-system/components/slider.md +20 -0
  68. package/docs/design-system/components/social-button.md +21 -3
  69. package/docs/design-system/components/spinner.md +16 -0
  70. package/docs/design-system/components/steps.md +14 -0
  71. package/docs/design-system/components/table.md +15 -1
  72. package/docs/design-system/components/tabs.md +17 -0
  73. package/docs/design-system/components/tag.md +17 -0
  74. package/docs/design-system/components/textarea.md +16 -0
  75. package/docs/design-system/components/timeline.md +14 -0
  76. package/docs/design-system/components/toast.md +17 -0
  77. package/docs/design-system/components/toggle.md +17 -0
  78. package/docs/design-system/components/toolbar.md +21 -1
  79. package/docs/design-system/components/tooltip.md +18 -1
  80. package/docs/design-system/density-and-elevation.md +44 -0
  81. package/docs/design-system/direction-and-localization.md +23 -0
  82. package/docs/design-system/form-recipes.md +73 -0
  83. package/docs/design-system/gap-promotion-policy.md +21 -0
  84. package/docs/design-system/gap-promotion-report.md +166 -0
  85. package/docs/design-system/installation.md +68 -0
  86. package/docs/design-system/launch-comparison-shadcn.md +98 -0
  87. package/docs/design-system/registry-and-cli.md +30 -7
  88. package/docs/design-system/release-policy.md +30 -0
  89. package/docs/design-system/roadmap.md +14 -0
  90. package/docs/design-system/support-matrix.md +23 -0
  91. package/docs/design-system/tabs.md +1 -1
  92. package/docs/design-system/token-governance.md +23 -1
  93. package/package.json +29 -15
  94. package/registry/component-contract.json +40 -0
  95. package/registry/components/alert-dialog.tsx +1 -1
  96. package/registry/components/alert.tsx +86 -65
  97. package/registry/components/autocomplete.tsx +23 -9
  98. package/registry/components/avatar.tsx +7 -7
  99. package/registry/components/badge.tsx +1 -1
  100. package/registry/components/button-group.tsx +321 -4
  101. package/registry/components/button.tsx +38 -17
  102. package/registry/components/card.tsx +68 -59
  103. package/registry/components/carousel.tsx +53 -5
  104. package/registry/components/chart.tsx +36 -3
  105. package/registry/components/checkbox.tsx +3 -3
  106. package/registry/components/collapsible.tsx +1 -1
  107. package/registry/components/command.tsx +1 -1
  108. package/registry/components/context-menu.tsx +1 -1
  109. package/registry/components/date-picker.tsx +245 -104
  110. package/registry/components/expanded/ActivityFeed.tsx +6 -15
  111. package/registry/components/expanded/Banner.tsx +3 -3
  112. package/registry/components/expanded/Breadcrumbs.tsx +5 -5
  113. package/registry/components/expanded/CatalogComponentsShowcase.tsx +13 -13
  114. package/registry/components/expanded/CatalogDivider.tsx +1 -1
  115. package/registry/components/expanded/EmptyState.tsx +1 -1
  116. package/registry/components/expanded/FileUpload.tsx +9 -2
  117. package/registry/components/expanded/OnboardingStepListItem.tsx +2 -4
  118. package/registry/components/expanded/PageHeader.tsx +13 -13
  119. package/registry/components/expanded/Slideout.tsx +15 -415
  120. package/registry/components/expanded/Steps.tsx +4 -4
  121. package/registry/components/expanded/Table.tsx +116 -69
  122. package/registry/components/expanded/Timeline.tsx +3 -3
  123. package/registry/components/expanded/activityFeed.css +1 -1
  124. package/registry/components/expanded/breadcrumbs.css +1 -1
  125. package/registry/components/expanded/divider.css +1 -1
  126. package/registry/components/expanded/emptyState.css +1 -1
  127. package/registry/components/expanded/fileUpload.css +6 -6
  128. package/registry/components/expanded/slideout.css +9 -685
  129. package/registry/components/expanded/steps.css +3 -3
  130. package/registry/components/expanded/table.css +2 -2
  131. package/registry/components/expanded/timeline.css +2 -2
  132. package/registry/components/input-content.ts +1 -0
  133. package/registry/components/input-otp.tsx +13 -3
  134. package/registry/components/input.tsx +68 -43
  135. package/registry/components/item.tsx +189 -0
  136. package/registry/components/kbd.tsx +111 -4
  137. package/registry/components/menu.tsx +31 -245
  138. package/registry/components/menubar.tsx +1 -1
  139. package/registry/components/modal.tsx +142 -102
  140. package/registry/components/native-select.tsx +2 -2
  141. package/registry/components/pagination.tsx +80 -58
  142. package/registry/components/popover.tsx +162 -664
  143. package/registry/components/radio-card.tsx +116 -87
  144. package/registry/components/segmented-control.tsx +87 -0
  145. package/registry/components/select-content.tsx +2 -2
  146. package/registry/components/select.tsx +52 -16
  147. package/registry/components/sheet.tsx +1 -1
  148. package/registry/components/sidebar.tsx +1 -1
  149. package/registry/components/skeleton.css +0 -3
  150. package/registry/components/social-button.tsx +47 -25
  151. package/registry/components/spinner.tsx +18 -55
  152. package/registry/components/tabs.css +1 -1
  153. package/registry/components/tabs.tsx +8 -4
  154. package/registry/components/tag.tsx +2 -2
  155. package/registry/components/textarea.tsx +3 -3
  156. package/registry/components/toast.tsx +16 -11
  157. package/registry/components/toggle.tsx +2 -2
  158. package/registry/components/toolbar.tsx +89 -17
  159. package/registry/components/tooltip.tsx +1 -1
  160. package/registry/examples/alert-demo.tsx +25 -5
  161. package/registry/examples/attribute-demo.tsx +1 -1
  162. package/registry/examples/autocomplete-demo.tsx +3 -3
  163. package/registry/examples/avatar-demo.tsx +7 -7
  164. package/registry/examples/badge-demo.tsx +1 -1
  165. package/registry/examples/button-demo.tsx +6 -6
  166. package/registry/examples/button-group-demo.tsx +23 -2
  167. package/registry/examples/card-demo.tsx +46 -8
  168. package/registry/examples/carousel-demo.tsx +20 -2
  169. package/registry/examples/chart-demo.tsx +1 -1
  170. package/registry/examples/checkbox-demo.tsx +5 -5
  171. package/registry/examples/collection-filter-bar-demo.tsx +135 -0
  172. package/registry/examples/command-demo.tsx +3 -3
  173. package/registry/examples/{expanded/command-bar-demo.tsx → command-palette-demo.tsx} +5 -15
  174. package/registry/examples/context-menu-demo.tsx +1 -1
  175. package/registry/examples/control-density-contract-demo.tsx +61 -0
  176. package/registry/examples/date-picker-demo.tsx +12 -12
  177. package/registry/examples/divider-demo.tsx +3 -3
  178. package/registry/examples/expanded/banner-demo.tsx +1 -1
  179. package/registry/examples/expanded/breadcrumbs-demo.tsx +1 -1
  180. package/registry/examples/expanded/empty-state-demo.tsx +1 -1
  181. package/registry/examples/expanded/file-upload-demo.tsx +1 -1
  182. package/registry/examples/expanded/onboarding-step-demo.tsx +1 -1
  183. package/registry/examples/expanded/slideout-demo.tsx +2 -2
  184. package/registry/examples/expanded/steps-demo.tsx +1 -1
  185. package/registry/examples/expanded/tabs-demo.tsx +1 -1
  186. package/registry/examples/expanded/timeline-demo.tsx +1 -1
  187. package/registry/examples/hover-card-demo.tsx +2 -2
  188. package/registry/examples/input-demo.tsx +6 -6
  189. package/registry/examples/input-otp-demo.tsx +4 -4
  190. package/registry/examples/item-demo.tsx +46 -0
  191. package/registry/examples/kbd-demo.tsx +1 -1
  192. package/registry/examples/label-demo.tsx +1 -1
  193. package/registry/examples/menu-demo.tsx +1 -1
  194. package/registry/examples/modal-demo.tsx +67 -9
  195. package/registry/examples/navigation-menu-demo.tsx +1 -1
  196. package/registry/examples/pagination-demo.tsx +6 -3
  197. package/registry/examples/popover-demo.tsx +19 -19
  198. package/registry/examples/progress-demo.tsx +1 -1
  199. package/registry/examples/progress-slider-demo.tsx +1 -1
  200. package/registry/examples/radio-card-demo.tsx +4 -4
  201. package/registry/examples/radio-group-demo.tsx +2 -2
  202. package/registry/examples/resizable-demo.tsx +1 -1
  203. package/registry/examples/results-toolbar-demo.tsx +44 -0
  204. package/registry/examples/scroll-area-demo.tsx +3 -3
  205. package/registry/examples/segmented-control-demo.tsx +39 -0
  206. package/registry/examples/select-demo.tsx +1 -1
  207. package/registry/examples/sidebar-demo.tsx +1 -1
  208. package/registry/examples/slider-demo.tsx +3 -3
  209. package/registry/examples/social-button-demo.tsx +18 -8
  210. package/registry/examples/spinner-demo.tsx +13 -13
  211. package/registry/examples/tabs-demo.tsx +1 -1
  212. package/registry/examples/tag-demo.tsx +1 -1
  213. package/registry/examples/textarea-demo.tsx +4 -4
  214. package/registry/examples/toast-demo.tsx +1 -1
  215. package/registry/examples/toggle-demo.tsx +6 -6
  216. package/registry/examples/tooltip-demo.tsx +4 -4
  217. package/registry/guidance/USAGE.md +27 -0
  218. package/registry/hooks/use-direction.ts +193 -13
  219. package/registry/index.json +5311 -377
  220. package/registry/styles/globals.css +2440 -4061
  221. package/registry.json +119 -149
  222. package/schema/config.schema.json +11 -8
  223. package/schema/registry.schema.json +150 -45
  224. package/schema/tokens.schema.json +134 -34
  225. package/src/cli/command-line.js +194 -0
  226. package/src/cli/config-editors.js +9 -0
  227. package/src/cli/doctor.js +399 -0
  228. package/src/cli/file-transaction.js +295 -0
  229. package/src/cli/global-css.js +153 -0
  230. package/src/cli/index.js +567 -1613
  231. package/src/cli/install-plan.js +592 -0
  232. package/src/cli/install-receipt.js +171 -0
  233. package/src/cli/jsonc.js +203 -0
  234. package/src/cli/package-install-state.js +254 -0
  235. package/src/cli/package-manager.js +238 -0
  236. package/src/cli/package-plan.js +185 -0
  237. package/src/cli/package-version.js +97 -0
  238. package/src/cli/registry-query.js +207 -0
  239. package/src/cli/reporting.js +55 -0
  240. package/src/cli/schema-validation.js +19 -0
  241. package/src/cli/source-scanner.js +686 -0
  242. package/src/cli/vite-config.js +478 -0
  243. package/tokens/banhaten.tokens.json +4312 -309
  244. package/docs/design-system/components/command-bar.md +0 -57
  245. package/registry/components/expanded/BannerBoard.tsx +0 -225
  246. package/registry/components/expanded/CatalogTag.tsx +0 -85
  247. package/registry/components/expanded/CommandBar.tsx +0 -386
  248. package/registry/components/expanded/IconExplorer.tsx +0 -612
  249. package/registry/components/expanded/TimelineShowcase.tsx +0 -158
  250. package/registry/components/expanded/commandBar.css +0 -470
  251. package/registry/components/expanded/iconExplorer.css +0 -764
  252. package/registry/components/expanded/iconPacks.ts +0 -866
  253. package/registry/components/expanded/tag.css +0 -148
  254. package/registry/examples/expanded/icons-demo.tsx +0 -5
  255. package/tokens/Color mode.zip +0 -0
  256. package/tokens/Numbers.zip +0 -0
  257. package/tokens/Radius.zip +0 -0
  258. 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
+ }