banhaten 0.3.0 → 0.5.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 (348) hide show
  1. package/CHANGELOG.md +139 -73
  2. package/CONTRIBUTING.md +46 -44
  3. package/README.md +192 -190
  4. package/SECURITY.md +15 -15
  5. package/banhaten.config.example.json +11 -11
  6. package/docs/design-spacing.md +13 -0
  7. package/docs/design-system/README.md +22 -20
  8. package/docs/design-system/adoption-quality-remediation.md +87 -87
  9. package/docs/design-system/agent-usage.md +13 -13
  10. package/docs/design-system/appearance-presets.md +189 -189
  11. package/docs/design-system/appearances/default.md +94 -94
  12. package/docs/design-system/appearances/rounded.md +95 -95
  13. package/docs/design-system/appearances/sharp.md +95 -95
  14. package/docs/design-system/component-docs-pattern.md +81 -81
  15. package/docs/design-system/component-implementation-ownership-audit.md +245 -244
  16. package/docs/design-system/component-quality-audit-2026-08-20.md +225 -223
  17. package/docs/design-system/component-showcase-consistency-report.md +219 -219
  18. package/docs/design-system/component-token-consistency-audit.md +173 -173
  19. package/docs/design-system/components/README.md +90 -76
  20. package/docs/design-system/components/accordion.md +53 -53
  21. package/docs/design-system/components/activity-feed.md +94 -94
  22. package/docs/design-system/components/activity-heatmap.md +57 -57
  23. package/docs/design-system/components/agent-input.md +85 -0
  24. package/docs/design-system/components/alert-dialog.md +72 -72
  25. package/docs/design-system/components/alert.md +98 -98
  26. package/docs/design-system/components/approval-card.md +134 -0
  27. package/docs/design-system/components/aspect-ratio.md +45 -45
  28. package/docs/design-system/components/attribute.md +89 -89
  29. package/docs/design-system/components/autocomplete.md +75 -75
  30. package/docs/design-system/components/avatar.md +54 -54
  31. package/docs/design-system/components/badge.md +54 -54
  32. package/docs/design-system/components/banner.md +87 -87
  33. package/docs/design-system/components/breadcrumbs.md +177 -176
  34. package/docs/design-system/components/button-group.md +98 -98
  35. package/docs/design-system/components/button.md +83 -80
  36. package/docs/design-system/components/card.md +88 -88
  37. package/docs/design-system/components/carousel.md +46 -46
  38. package/docs/design-system/components/catalog-components.md +47 -47
  39. package/docs/design-system/components/chart.md +95 -24
  40. package/docs/design-system/components/checkbox.md +54 -54
  41. package/docs/design-system/components/collapsible.md +67 -53
  42. package/docs/design-system/components/command.md +62 -62
  43. package/docs/design-system/components/context-menu.md +46 -46
  44. package/docs/design-system/components/date-picker.md +81 -79
  45. package/docs/design-system/components/divider.md +102 -102
  46. package/docs/design-system/components/empty-state.md +56 -56
  47. package/docs/design-system/components/field.md +70 -70
  48. package/docs/design-system/components/file-upload.md +187 -187
  49. package/docs/design-system/components/hover-card.md +48 -48
  50. package/docs/design-system/components/icons.md +48 -48
  51. package/docs/design-system/components/inline-citations.md +93 -0
  52. package/docs/design-system/components/input-group.md +58 -58
  53. package/docs/design-system/components/input-otp.md +56 -56
  54. package/docs/design-system/components/input.md +117 -61
  55. package/docs/design-system/components/item.md +59 -57
  56. package/docs/design-system/components/kbd.md +54 -49
  57. package/docs/design-system/components/label.md +49 -49
  58. package/docs/design-system/components/menu.md +71 -61
  59. package/docs/design-system/components/menubar.md +47 -47
  60. package/docs/design-system/components/modal.md +105 -105
  61. package/docs/design-system/components/native-select.md +54 -54
  62. package/docs/design-system/components/navigation-menu.md +50 -50
  63. package/docs/design-system/components/onboarding-step-list-item.md +82 -82
  64. package/docs/design-system/components/page-header.md +93 -86
  65. package/docs/design-system/components/pagination.md +57 -57
  66. package/docs/design-system/components/popover.md +60 -60
  67. package/docs/design-system/components/progress-slider.md +50 -50
  68. package/docs/design-system/components/progress.md +76 -76
  69. package/docs/design-system/components/radio-card.md +51 -51
  70. package/docs/design-system/components/radio-group.md +57 -57
  71. package/docs/design-system/components/resizable.md +44 -44
  72. package/docs/design-system/components/rolling-count.md +61 -0
  73. package/docs/design-system/components/scroll-area.md +51 -51
  74. package/docs/design-system/components/segmented-control.md +54 -54
  75. package/docs/design-system/components/select.md +68 -62
  76. package/docs/design-system/components/sheet.md +67 -67
  77. package/docs/design-system/components/shimmer-text.md +67 -0
  78. package/docs/design-system/components/sidebar.md +127 -76
  79. package/docs/design-system/components/skeleton.md +75 -75
  80. package/docs/design-system/components/slideout.md +65 -65
  81. package/docs/design-system/components/slider.md +63 -63
  82. package/docs/design-system/components/social-button.md +50 -50
  83. package/docs/design-system/components/spinner.md +62 -62
  84. package/docs/design-system/components/steps.md +65 -65
  85. package/docs/design-system/components/streaming-text.md +82 -0
  86. package/docs/design-system/components/table.md +419 -414
  87. package/docs/design-system/components/tabs.md +59 -55
  88. package/docs/design-system/components/tag.md +80 -80
  89. package/docs/design-system/components/task-list.md +95 -0
  90. package/docs/design-system/components/textarea.md +50 -50
  91. package/docs/design-system/components/thinking-reasoning.md +72 -0
  92. package/docs/design-system/components/thinking-state.md +67 -0
  93. package/docs/design-system/components/timeline.md +83 -83
  94. package/docs/design-system/components/toast.md +58 -58
  95. package/docs/design-system/components/toggle.md +81 -81
  96. package/docs/design-system/components/toolbar.md +92 -87
  97. package/docs/design-system/components/tooltip.md +94 -92
  98. package/docs/design-system/components/typography.md +18 -18
  99. package/docs/design-system/density-and-elevation.md +65 -61
  100. package/docs/design-system/design-system-test-missing-items.md +368 -368
  101. package/docs/design-system/direction-and-localization.md +23 -23
  102. package/docs/design-system/form-recipes.md +73 -73
  103. package/docs/design-system/gap-promotion-policy.md +21 -21
  104. package/docs/design-system/icons.md +74 -69
  105. package/docs/design-system/installation.md +70 -68
  106. package/docs/design-system/launch-comparison-shadcn.md +98 -98
  107. package/docs/design-system/registry-and-cli.md +64 -64
  108. package/docs/design-system/release-policy.md +30 -30
  109. package/docs/design-system/roadmap.md +14 -14
  110. package/docs/design-system/support-matrix.md +23 -23
  111. package/docs/design-system/tabs.md +54 -53
  112. package/docs/design-system/template-gap-audit.md +32 -0
  113. package/docs/design-system/templates.md +59 -0
  114. package/docs/design-system/token-governance.md +72 -60
  115. package/package.json +98 -98
  116. package/registry/component-contract.json +40 -40
  117. package/registry/components/accordion.tsx +155 -155
  118. package/registry/components/agent-input-parts.tsx +552 -0
  119. package/registry/components/agent-input-state.ts +156 -0
  120. package/registry/components/agent-input.css +307 -0
  121. package/registry/components/agent-input.tsx +382 -0
  122. package/registry/components/alert-dialog.tsx +297 -297
  123. package/registry/components/alert.tsx +293 -293
  124. package/registry/components/approval-card.css +409 -0
  125. package/registry/components/approval-card.tsx +401 -0
  126. package/registry/components/approval-frame.tsx +108 -0
  127. package/registry/components/approval-questions.tsx +559 -0
  128. package/registry/components/aspect-ratio.tsx +30 -30
  129. package/registry/components/attribute.tsx +454 -448
  130. package/registry/components/autocomplete.tsx +598 -651
  131. package/registry/components/avatar.tsx +385 -381
  132. package/registry/components/badge.tsx +629 -629
  133. package/registry/components/button-group.tsx +565 -565
  134. package/registry/components/button.tsx +127 -123
  135. package/registry/components/card.tsx +638 -638
  136. package/registry/components/carousel.tsx +296 -296
  137. package/registry/components/chart-block-data.ts +177 -0
  138. package/registry/components/chart-block.css +171 -0
  139. package/registry/components/chart-block.tsx +260 -0
  140. package/registry/components/chart-interactions.ts +123 -0
  141. package/registry/components/chart-options.cartesian.ts +162 -0
  142. package/registry/components/chart-options.paint.ts +99 -0
  143. package/registry/components/chart-options.parts.ts +72 -0
  144. package/registry/components/chart-options.radial.ts +109 -0
  145. package/registry/components/chart-options.ts +44 -0
  146. package/registry/components/chart-options.types.ts +67 -0
  147. package/registry/components/chart.tsx +203 -203
  148. package/registry/components/checkbox.tsx +247 -247
  149. package/registry/components/collapsible.tsx +109 -69
  150. package/registry/components/command.tsx +208 -208
  151. package/registry/components/context-menu.tsx +236 -236
  152. package/registry/components/data-chart.tsx +40 -0
  153. package/registry/components/date-picker.tsx +1289 -1277
  154. package/registry/components/echart-theme.ts +246 -0
  155. package/registry/components/echart.css +49 -0
  156. package/registry/components/echart.tsx +178 -0
  157. package/registry/components/expanded/ActivityFeed.tsx +227 -227
  158. package/registry/components/expanded/ActivityHeatmap.tsx +64 -64
  159. package/registry/components/expanded/Banner.tsx +179 -179
  160. package/registry/components/expanded/Breadcrumbs.tsx +131 -131
  161. package/registry/components/expanded/CatalogComponentsShowcase.tsx +205 -205
  162. package/registry/components/expanded/CatalogDivider.tsx +49 -49
  163. package/registry/components/expanded/EmptyState.tsx +155 -155
  164. package/registry/components/expanded/FileUpload.tsx +543 -543
  165. package/registry/components/expanded/OnboardingStepListItem.tsx +60 -59
  166. package/registry/components/expanded/PageHeader.tsx +179 -166
  167. package/registry/components/expanded/Slideout.tsx +108 -108
  168. package/registry/components/expanded/Steps.tsx +402 -402
  169. package/registry/components/expanded/Table.tsx +1092 -1071
  170. package/registry/components/expanded/Timeline.tsx +203 -203
  171. package/registry/components/expanded/activityFeed.css +248 -248
  172. package/registry/components/expanded/activityHeatmap.css +66 -66
  173. package/registry/components/expanded/banner.css +260 -255
  174. package/registry/components/expanded/breadcrumbs.css +140 -140
  175. package/registry/components/expanded/catalogComponentsShowcase.css +87 -87
  176. package/registry/components/expanded/divider.css +75 -75
  177. package/registry/components/expanded/emptyState.css +111 -111
  178. package/registry/components/expanded/fileUpload.css +457 -457
  179. package/registry/components/expanded/onboardingStepListItem.css +135 -126
  180. package/registry/components/expanded/pageHeader.css +248 -290
  181. package/registry/components/expanded/slideout.css +276 -276
  182. package/registry/components/expanded/steps.css +356 -356
  183. package/registry/components/expanded/table.css +606 -603
  184. package/registry/components/expanded/timeline.css +286 -286
  185. package/registry/components/field.tsx +283 -283
  186. package/registry/components/hover-card.tsx +48 -48
  187. package/registry/components/inline-citations.css +126 -0
  188. package/registry/components/inline-citations.tsx +292 -0
  189. package/registry/components/input-content.ts +107 -107
  190. package/registry/components/input-group.tsx +130 -130
  191. package/registry/components/input-otp.tsx +584 -584
  192. package/registry/components/input.tsx +990 -972
  193. package/registry/components/item.tsx +189 -189
  194. package/registry/components/kbd.tsx +246 -151
  195. package/registry/components/label.tsx +78 -78
  196. package/registry/components/menu.tsx +978 -986
  197. package/registry/components/menubar.tsx +226 -226
  198. package/registry/components/modal.tsx +872 -872
  199. package/registry/components/native-select.tsx +205 -205
  200. package/registry/components/navigation-menu.tsx +171 -171
  201. package/registry/components/pagination.css +36 -36
  202. package/registry/components/pagination.tsx +560 -555
  203. package/registry/components/popover.tsx +338 -338
  204. package/registry/components/progress-slider.tsx +216 -216
  205. package/registry/components/radio-card.tsx +676 -677
  206. package/registry/components/radio-group.tsx +570 -570
  207. package/registry/components/resizable.tsx +108 -108
  208. package/registry/components/rolling-count.css +37 -0
  209. package/registry/components/rolling-count.tsx +110 -0
  210. package/registry/components/scroll-area.tsx +91 -91
  211. package/registry/components/segmented-control.tsx +87 -87
  212. package/registry/components/select-content.tsx +218 -218
  213. package/registry/components/select.tsx +937 -932
  214. package/registry/components/sheet.tsx +305 -305
  215. package/registry/components/shimmer-text.css +69 -0
  216. package/registry/components/shimmer-text.tsx +43 -0
  217. package/registry/components/sidebar.tsx +1433 -358
  218. package/registry/components/skeleton.css +54 -54
  219. package/registry/components/skeleton.tsx +482 -482
  220. package/registry/components/slider.tsx +380 -380
  221. package/registry/components/social-button.tsx +387 -387
  222. package/registry/components/spinner.tsx +78 -78
  223. package/registry/components/streaming-text.css +45 -0
  224. package/registry/components/streaming-text.tsx +165 -0
  225. package/registry/components/table-elements.tsx +128 -128
  226. package/registry/components/tabs.css +222 -218
  227. package/registry/components/tabs.tsx +241 -241
  228. package/registry/components/tag.tsx +423 -423
  229. package/registry/components/task-list.css +328 -0
  230. package/registry/components/task-list.tsx +429 -0
  231. package/registry/components/textarea.tsx +610 -610
  232. package/registry/components/thinking-reasoning.css +178 -0
  233. package/registry/components/thinking-reasoning.tsx +253 -0
  234. package/registry/components/thinking-state.css +8 -0
  235. package/registry/components/thinking-state.tsx +46 -0
  236. package/registry/components/toast.tsx +136 -136
  237. package/registry/components/toggle.tsx +256 -256
  238. package/registry/components/toolbar.tsx +539 -539
  239. package/registry/components/tooltip.tsx +433 -431
  240. package/registry/examples/agent-input-demo.tsx +90 -0
  241. package/registry/examples/alert-demo.tsx +34 -34
  242. package/registry/examples/alert-dialog-demo.tsx +42 -42
  243. package/registry/examples/approval-card-demo.tsx +83 -0
  244. package/registry/examples/aspect-ratio-demo.tsx +11 -11
  245. package/registry/examples/attribute-demo.tsx +65 -65
  246. package/registry/examples/autocomplete-demo.tsx +109 -109
  247. package/registry/examples/avatar-demo.tsx +132 -132
  248. package/registry/examples/badge-demo.tsx +69 -69
  249. package/registry/examples/button-demo.tsx +86 -83
  250. package/registry/examples/button-group-demo.tsx +63 -63
  251. package/registry/examples/card-demo.tsx +86 -86
  252. package/registry/examples/carousel-demo.tsx +43 -43
  253. package/registry/examples/chart-block-demo.tsx +5 -0
  254. package/registry/examples/chart-demo.tsx +18 -33
  255. package/registry/examples/checkbox-demo.tsx +62 -62
  256. package/registry/examples/collapsible-demo.tsx +16 -16
  257. package/registry/examples/collection-filter-bar-demo.tsx +135 -135
  258. package/registry/examples/command-demo.tsx +42 -42
  259. package/registry/examples/command-palette-demo.tsx +235 -236
  260. package/registry/examples/context-menu-demo.tsx +29 -29
  261. package/registry/examples/control-density-contract-demo.tsx +65 -61
  262. package/registry/examples/date-picker-demo.tsx +127 -127
  263. package/registry/examples/divider-demo.tsx +17 -17
  264. package/registry/examples/expanded/activity-heatmap-demo.tsx +29 -29
  265. package/registry/examples/expanded/banner-demo.tsx +48 -48
  266. package/registry/examples/expanded/breadcrumbs-demo.tsx +59 -59
  267. package/registry/examples/expanded/empty-state-demo.tsx +39 -39
  268. package/registry/examples/expanded/file-upload-demo.tsx +60 -60
  269. package/registry/examples/expanded/onboarding-step-demo.tsx +11 -11
  270. package/registry/examples/expanded/slideout-demo.tsx +15 -15
  271. package/registry/examples/expanded/steps-demo.tsx +29 -29
  272. package/registry/examples/expanded/tabs-demo.tsx +13 -13
  273. package/registry/examples/expanded/timeline-demo.tsx +18 -18
  274. package/registry/examples/field-demo.tsx +51 -51
  275. package/registry/examples/hover-card-demo.tsx +23 -23
  276. package/registry/examples/inline-citations-demo.tsx +31 -0
  277. package/registry/examples/input-actions-demo.tsx +36 -0
  278. package/registry/examples/input-demo.tsx +87 -87
  279. package/registry/examples/input-group-demo.tsx +16 -16
  280. package/registry/examples/input-otp-demo.tsx +72 -72
  281. package/registry/examples/item-demo.tsx +46 -46
  282. package/registry/examples/kbd-demo.tsx +12 -11
  283. package/registry/examples/label-demo.tsx +20 -20
  284. package/registry/examples/menu-demo.tsx +122 -122
  285. package/registry/examples/menubar-demo.tsx +34 -34
  286. package/registry/examples/modal-demo.tsx +74 -74
  287. package/registry/examples/native-select-demo.tsx +16 -16
  288. package/registry/examples/navigation-menu-demo.tsx +29 -29
  289. package/registry/examples/pagination-demo.tsx +20 -20
  290. package/registry/examples/popover-demo.tsx +547 -546
  291. package/registry/examples/progress-demo.tsx +37 -37
  292. package/registry/examples/progress-slider-demo.tsx +29 -29
  293. package/registry/examples/radio-card-demo.tsx +51 -51
  294. package/registry/examples/radio-group-demo.tsx +62 -62
  295. package/registry/examples/resizable-demo.tsx +22 -22
  296. package/registry/examples/results-toolbar-demo.tsx +44 -44
  297. package/registry/examples/rolling-count-demo.tsx +39 -0
  298. package/registry/examples/scroll-area-demo.tsx +15 -15
  299. package/registry/examples/segmented-control-demo.tsx +39 -39
  300. package/registry/examples/select-demo.tsx +87 -87
  301. package/registry/examples/sheet-demo.tsx +47 -47
  302. package/registry/examples/shimmer-text-demo.tsx +20 -0
  303. package/registry/examples/sidebar-demo.tsx +101 -57
  304. package/registry/examples/slider-demo.tsx +31 -31
  305. package/registry/examples/social-button-demo.tsx +61 -61
  306. package/registry/examples/spinner-demo.tsx +59 -59
  307. package/registry/examples/streaming-text-demo.tsx +28 -0
  308. package/registry/examples/tabs-demo.tsx +13 -13
  309. package/registry/examples/tag-demo.tsx +29 -29
  310. package/registry/examples/task-list-demo.tsx +74 -0
  311. package/registry/examples/textarea-demo.tsx +79 -79
  312. package/registry/examples/thinking-reasoning-demo.tsx +74 -0
  313. package/registry/examples/thinking-state-demo.tsx +13 -0
  314. package/registry/examples/toast-demo.tsx +35 -35
  315. package/registry/examples/toggle-demo.tsx +59 -59
  316. package/registry/examples/tooltip-demo.tsx +115 -115
  317. package/registry/guidance/USAGE.md +27 -27
  318. package/registry/guidance/charts.LICENSE.txt +24 -0
  319. package/registry/hooks/use-controllable-state.ts +33 -0
  320. package/registry/hooks/use-direction.ts +207 -207
  321. package/registry/hooks/use-listbox-navigation.ts +73 -0
  322. package/registry/hooks/use-motion-end.ts +55 -0
  323. package/registry/hooks/use-reduced-motion.ts +28 -0
  324. package/registry/index.json +1634 -184
  325. package/registry/styles/globals.css +1462 -1570
  326. package/registry.json +364 -10
  327. package/schema/config.schema.json +51 -51
  328. package/schema/registry.schema.json +194 -190
  329. package/schema/tokens.schema.json +167 -163
  330. package/src/cli/command-line.js +194 -194
  331. package/src/cli/config-editors.js +9 -9
  332. package/src/cli/doctor.js +399 -399
  333. package/src/cli/file-transaction.js +302 -295
  334. package/src/cli/global-css.js +153 -153
  335. package/src/cli/index.js +1002 -1009
  336. package/src/cli/install-plan.js +592 -592
  337. package/src/cli/install-receipt.js +171 -171
  338. package/src/cli/jsonc.js +203 -203
  339. package/src/cli/package-install-state.js +254 -254
  340. package/src/cli/package-manager.js +238 -238
  341. package/src/cli/package-plan.js +185 -185
  342. package/src/cli/package-version.js +97 -97
  343. package/src/cli/registry-query.js +207 -207
  344. package/src/cli/reporting.js +55 -55
  345. package/src/cli/schema-validation.js +19 -19
  346. package/src/cli/source-scanner.js +686 -686
  347. package/src/cli/vite-config.js +478 -478
  348. package/tokens/banhaten.tokens.json +458 -224
@@ -1,478 +1,478 @@
1
- // The scanner reports syntax problems without naming a dialect; this module is
2
- // the only place that knows the source is a Vite config, so it adds that word.
3
- function asViteConfigError(error) {
4
- if (error instanceof Error && !/^unsupported Vite config: /.test(error.message)) {
5
- error.message = `unsupported Vite config: ${error.message}`
6
- }
7
- return error
8
- }
9
-
10
- // Vite config editing. This module owns every assumption about what a Vite
11
- // config looks like; the scanner primitives it builds on know nothing about Vite.
12
-
13
- import {
14
- appendArrayElement,
15
- appendObjectProperty,
16
- arrayHasCallExpression,
17
- createBracketRange,
18
- expectArrayLiteralValue,
19
- expectObjectLiteralValue,
20
- findExportDefaultExpressionStart,
21
- findObjectProperty,
22
- findTopLevelCommaOrEnd,
23
- insertObjectProperty,
24
- parseImportDeclarations,
25
- prependImportLines,
26
- readIdentifierAt,
27
- readObjectPropertyNames,
28
- readPropertyName,
29
- readStringLiteral,
30
- skipWhitespaceAndComments,
31
- skipWhitespaceCommentsAndCommas
32
- } from "./source-scanner.js"
33
-
34
- function ensureNodeUrlImport(source) {
35
- const imports = parseImportDeclarations(source)
36
- const nodeUrlImports = imports.filter((entry) => entry.moduleSpecifier === "node:url")
37
- const importedLocals = new Set(nodeUrlImports.flatMap((entry) => entry.namedLocals))
38
- const missing = ["fileURLToPath", "URL"].filter((name) => !importedLocals.has(name))
39
-
40
- if (missing.length === 0) return source
41
-
42
- return prependImportLines(
43
- source,
44
- [`import { ${missing.join(", ")} } from "node:url"`]
45
- )
46
- }
47
-
48
- function ensureTailwindViteImport(source, localName) {
49
- const imports = parseImportDeclarations(source)
50
- const tailwindImport = imports.find(
51
- (entry) => entry.moduleSpecifier === "@tailwindcss/vite"
52
- )
53
-
54
- if (!tailwindImport) {
55
- return prependImportLines(
56
- source,
57
- [`import ${localName} from "@tailwindcss/vite"`]
58
- )
59
- }
60
-
61
- if (!tailwindImport.defaultLocal) {
62
- throw new Error(
63
- "unsupported Vite config: @tailwindcss/vite must be imported with a default binding."
64
- )
65
- }
66
-
67
- return source
68
- }
69
-
70
- export function updateViteConfigSource(source, options) {
71
- try {
72
- return updateViteConfigSourceUnchecked(source, options)
73
- } catch (error) {
74
- throw asViteConfigError(error)
75
- }
76
- }
77
-
78
- function updateViteConfigSourceUnchecked(source, { aliasRoot, sourceRoot }) {
79
- validateViteConfigStructure(source)
80
- const tailwindLocalName = getTailwindViteLocalName(source)
81
- let next = aliasRoot
82
- ? addViteResolveAlias(source, aliasRoot, sourceRoot)
83
- : source
84
- const addedFileUrlAlias = Boolean(aliasRoot) && next !== source
85
- next = addVitePluginCall(next, `${tailwindLocalName}()`, tailwindLocalName)
86
- next = ensureTailwindViteImport(next, tailwindLocalName)
87
- if (addedFileUrlAlias) next = ensureNodeUrlImport(next)
88
- return next
89
- }
90
-
91
- export function readViteAliasSourceRoot(source, options) {
92
- try {
93
- return readViteAliasSourceRootUnchecked(source, options)
94
- } catch (error) {
95
- throw asViteConfigError(error)
96
- }
97
- }
98
-
99
- function readViteAliasSourceRootUnchecked(source, { aliasRoot }) {
100
- validateViteConfigStructure(source)
101
- const configObject = findViteConfigObject(source)
102
- const resolveProperty = findObjectProperty(source, configObject, "resolve")
103
- if (!resolveProperty) return null
104
-
105
- const resolveObject = expectObjectLiteralValue(
106
- source,
107
- resolveProperty,
108
- "unsupported Vite config: resolve must be an object literal."
109
- )
110
- const aliasProperty = findObjectProperty(source, resolveObject, "alias")
111
- if (!aliasProperty) return null
112
-
113
- const aliasObject = expectObjectLiteralValue(
114
- source,
115
- aliasProperty,
116
- "unsupported Vite config: resolve.alias must be an object literal."
117
- )
118
- const rootProperty = findObjectProperty(source, aliasObject, aliasRoot)
119
- if (!rootProperty) return null
120
-
121
- const expression = source
122
- .slice(rootProperty.valueStart, rootProperty.valueEnd)
123
- .trim()
124
- const staticRoot = readStaticAliasRoot(expression)
125
- if (staticRoot !== null) {
126
- assertStaticAliasBinding(source, staticRoot)
127
- return staticRoot
128
- }
129
-
130
- throw new Error(
131
- `unsupported Vite config: resolve.alias[${JSON.stringify(aliasRoot)}] must use a static string, ` +
132
- "fileURLToPath(new URL(..., import.meta.url)), or path.resolve(__dirname, ...)."
133
- )
134
- }
135
-
136
- export function readViteAliasKeys(source) {
137
- try {
138
- return readViteAliasKeysUnchecked(source)
139
- } catch (error) {
140
- throw asViteConfigError(error)
141
- }
142
- }
143
-
144
- function readViteAliasKeysUnchecked(source) {
145
- validateViteConfigStructure(source)
146
- const configObject = findViteConfigObject(source)
147
- const resolveProperty = findObjectProperty(source, configObject, "resolve")
148
- if (!resolveProperty) return []
149
-
150
- const resolveObject = expectObjectLiteralValue(
151
- source,
152
- resolveProperty,
153
- "unsupported Vite config: resolve must be an object literal."
154
- )
155
- const aliasProperty = findObjectProperty(source, resolveObject, "alias")
156
- if (!aliasProperty) return []
157
-
158
- const aliasObject = expectObjectLiteralValue(
159
- source,
160
- aliasProperty,
161
- "unsupported Vite config: resolve.alias must be an object literal."
162
- )
163
- return readObjectPropertyNames(source, aliasObject)
164
- }
165
-
166
- function readStaticAliasRoot(expression) {
167
- const directLiteral = readStringLiteral(expression, 0)
168
- if (
169
- directLiteral &&
170
- skipWhitespaceAndComments(expression, directLiteral.end) === expression.length
171
- ) {
172
- return { kind: "literal", value: directLiteral.value }
173
- }
174
-
175
- const patterns = [
176
- {
177
- kind: "file-url",
178
- pattern: /^fileURLToPath\s*\(\s*new\s+URL\s*\(\s*(["'])(.*?)\1\s*,\s*import\.meta\.url\s*\)\s*\)$/s,
179
- },
180
- {
181
- kind: "path-default",
182
- pattern: /^path\.resolve\s*\(\s*(?:__dirname|import\.meta\.dirname)\s*,\s*(["'])(.*?)\1\s*\)$/s,
183
- },
184
- {
185
- kind: "path-named",
186
- pattern: /^resolve\s*\(\s*(?:__dirname|import\.meta\.dirname)\s*,\s*(["'])(.*?)\1\s*\)$/s,
187
- },
188
- ]
189
-
190
- for (const candidate of patterns) {
191
- const match = expression.match(candidate.pattern)
192
- if (match) {
193
- return {
194
- kind: candidate.kind,
195
- value: match[2].replace(/\\(.)/g, "$1"),
196
- }
197
- }
198
- }
199
-
200
- return null
201
- }
202
-
203
- function assertStaticAliasBinding(source, staticRoot) {
204
- const imports = parseImportDeclarations(source)
205
-
206
- if (staticRoot.kind === "file-url") {
207
- const nodeUrlImport = imports.find(
208
- (entry) =>
209
- (entry.moduleSpecifier === "node:url" || entry.moduleSpecifier === "url") &&
210
- entry.namedLocals.includes("fileURLToPath")
211
- )
212
- if (!nodeUrlImport) {
213
- throw new Error(
214
- "unsupported Vite config: fileURLToPath must be imported by name from node:url."
215
- )
216
- }
217
- }
218
-
219
- if (staticRoot.kind === "path-default") {
220
- const nodePathImport = imports.find(
221
- (entry) =>
222
- (entry.moduleSpecifier === "node:path" || entry.moduleSpecifier === "path") &&
223
- entry.defaultLocal === "path"
224
- )
225
- if (!nodePathImport) {
226
- throw new Error(
227
- "unsupported Vite config: path must be a default import from node:path."
228
- )
229
- }
230
- }
231
-
232
- if (staticRoot.kind === "path-named") {
233
- const nodePathImport = imports.find(
234
- (entry) =>
235
- (entry.moduleSpecifier === "node:path" || entry.moduleSpecifier === "path") &&
236
- entry.namedLocals.includes("resolve")
237
- )
238
- if (!nodePathImport) {
239
- throw new Error(
240
- "unsupported Vite config: resolve must be imported by name from node:path."
241
- )
242
- }
243
- }
244
- }
245
-
246
- function validateViteConfigStructure(source) {
247
- const configObject = findViteConfigObject(source)
248
- assertStaticObjectStructure(source, configObject, "config")
249
-
250
- const resolveProperty = findObjectProperty(source, configObject, "resolve")
251
- if (!resolveProperty) return
252
-
253
- const resolveObject = expectObjectLiteralValue(
254
- source,
255
- resolveProperty,
256
- "unsupported Vite config: resolve must be an object literal."
257
- )
258
- assertStaticObjectStructure(source, resolveObject, "resolve")
259
-
260
- const aliasProperty = findObjectProperty(source, resolveObject, "alias")
261
- if (!aliasProperty) return
262
-
263
- const aliasObject = expectObjectLiteralValue(
264
- source,
265
- aliasProperty,
266
- "unsupported Vite config: resolve.alias must be an object literal."
267
- )
268
- assertStaticObjectStructure(source, aliasObject, "resolve.alias")
269
- }
270
-
271
- function assertStaticObjectStructure(source, objectRange, label) {
272
- const bodyEnd = objectRange.end - 1
273
- const seen = new Set()
274
- let index = objectRange.start + 1
275
-
276
- while (index < bodyEnd) {
277
- index = skipWhitespaceCommentsAndCommas(source, index, bodyEnd)
278
- if (index >= bodyEnd) return
279
-
280
- const propertyStart = index
281
- const propertyEnd = findTopLevelCommaOrEnd(
282
- source,
283
- propertyStart,
284
- bodyEnd
285
- )
286
-
287
- if (source.startsWith("...", propertyStart)) {
288
- throw new Error(
289
- `unsupported Vite config: ${label} cannot contain spread properties.`
290
- )
291
- }
292
- if (source[propertyStart] === "[") {
293
- throw new Error(
294
- `unsupported Vite config: ${label} cannot contain computed properties.`
295
- )
296
- }
297
-
298
- const name = readPropertyName(source, propertyStart)
299
- if (name) {
300
- if (seen.has(name.name)) {
301
- throw new Error(
302
- `unsupported Vite config: ${label} contains duplicate property ${JSON.stringify(name.name)}.`
303
- )
304
- }
305
- seen.add(name.name)
306
- }
307
-
308
- index = propertyEnd + 1
309
- }
310
- }
311
-
312
- function getTailwindViteLocalName(source) {
313
- const tailwindImport = parseImportDeclarations(source).find(
314
- (entry) => entry.moduleSpecifier === "@tailwindcss/vite"
315
- )
316
-
317
- if (!tailwindImport) return "tailwindcss"
318
- if (tailwindImport.defaultLocal) return tailwindImport.defaultLocal
319
-
320
- throw new Error(
321
- "unsupported Vite config: @tailwindcss/vite must be imported with a default binding."
322
- )
323
- }
324
-
325
- function addViteResolveAlias(source, aliasRoot, sourceRoot) {
326
- const configObject = findViteConfigObject(source)
327
- const resolveProperty = findObjectProperty(source, configObject, "resolve")
328
- const aliasEntry = `${JSON.stringify(aliasRoot)}: fileURLToPath(new URL(${JSON.stringify(
329
- sourceRoot
330
- )}, import.meta.url))`
331
-
332
- if (!resolveProperty) {
333
- return insertObjectProperty(
334
- source,
335
- configObject,
336
- [
337
- "resolve: {",
338
- " alias: {",
339
- ` ${aliasEntry},`,
340
- " },",
341
- "},",
342
- ].join("\n")
343
- )
344
- }
345
-
346
- const resolveObject = expectObjectLiteralValue(
347
- source,
348
- resolveProperty,
349
- "unsupported Vite config: resolve must be an object literal."
350
- )
351
- const aliasProperty = findObjectProperty(source, resolveObject, "alias")
352
-
353
- if (!aliasProperty) {
354
- return insertObjectProperty(
355
- source,
356
- resolveObject,
357
- [
358
- "alias: {",
359
- ` ${aliasEntry},`,
360
- "},",
361
- ].join("\n")
362
- )
363
- }
364
-
365
- const aliasObject = expectObjectLiteralValue(
366
- source,
367
- aliasProperty,
368
- "unsupported Vite config: resolve.alias must be an object literal."
369
- )
370
-
371
- if (findObjectProperty(source, aliasObject, aliasRoot)) {
372
- return source
373
- }
374
-
375
- return appendObjectProperty(source, aliasObject, `${aliasEntry},`)
376
- }
377
-
378
- function addVitePluginCall(source, pluginCall, pluginLocalName) {
379
- const configObject = findViteConfigObject(source)
380
- const pluginsProperty = findObjectProperty(source, configObject, "plugins")
381
-
382
- if (!pluginsProperty) {
383
- return insertObjectProperty(source, configObject, `plugins: [${pluginCall}],`)
384
- }
385
-
386
- const pluginsArray = expectArrayLiteralValue(
387
- source,
388
- pluginsProperty,
389
- "unsupported Vite config: plugins must be an array literal."
390
- )
391
-
392
- if (arrayHasCallExpression(source, pluginsArray, pluginLocalName)) {
393
- return source
394
- }
395
-
396
- return appendArrayElement(source, pluginsArray, pluginCall)
397
- }
398
-
399
- function findViteConfigObject(source) {
400
- const expressionStart = findExportDefaultExpressionStart(source)
401
-
402
- if (expressionStart === -1) {
403
- throw new Error(
404
- "unsupported Vite config: expected `export default defineConfig({ ... })` or `export default { ... }`."
405
- )
406
- }
407
-
408
- if (source[expressionStart] === "{") {
409
- return createBracketRange(source, expressionStart, "{")
410
- }
411
-
412
- const callee = readIdentifierAt(source, expressionStart)
413
- if (callee?.name !== "defineConfig") {
414
- throw new Error(
415
- "unsupported Vite config: default export must be `defineConfig({ ... })` or an object literal."
416
- )
417
- }
418
-
419
- let index = skipWhitespaceAndComments(source, callee.end)
420
- if (source[index] !== "(") {
421
- throw new Error("unsupported Vite config: malformed defineConfig call.")
422
- }
423
-
424
- index = skipWhitespaceAndComments(source, index + 1)
425
-
426
- // defineConfig({ ... })
427
- if (source[index] === "{") {
428
- return createBracketRange(source, index, "{")
429
- }
430
-
431
- // defineConfig((env) => ({ ... })) — the other documented Vite form, used
432
- // whenever the config depends on `mode` or `command`. The config object is
433
- // the arrow's returned expression, so resolve to that and edit it in place.
434
- const arrowBody = findConfigArrowBodyStart(source, index)
435
- if (arrowBody !== -1) {
436
- return createBracketRange(source, arrowBody, "{")
437
- }
438
-
439
- throw new Error(
440
- "unsupported Vite config: defineConfig must receive a config object literal " +
441
- "or an arrow function returning one, e.g. `defineConfig(({ mode }) => ({ ... }))`."
442
- )
443
- }
444
-
445
- // Given the index of defineConfig's first argument, return the index of the
446
- // object literal an arrow function returns, or -1 when the argument is not an
447
- // arrow whose body is a parenthesised object.
448
- function findConfigArrowBodyStart(source, argumentStart) {
449
- let index = argumentStart
450
-
451
- // An optional `async` before the parameter list.
452
- const asyncKeyword = readIdentifierAt(source, index)
453
- if (asyncKeyword?.name === "async") {
454
- index = skipWhitespaceAndComments(source, asyncKeyword.end)
455
- }
456
-
457
- // Parameter list: either `(...)` or a single bare identifier.
458
- if (source[index] === "(") {
459
- const parameters = createBracketRange(source, index, "(")
460
- index = skipWhitespaceAndComments(source, parameters.end + 1)
461
- } else {
462
- const parameter = readIdentifierAt(source, index)
463
- if (!parameter) return -1
464
- index = skipWhitespaceAndComments(source, parameter.end)
465
- }
466
-
467
- if (source.slice(index, index + 2) !== "=>") return -1
468
- index = skipWhitespaceAndComments(source, index + 2)
469
-
470
- // `=> ({ ... })`. A block body (`=> { return { ... } }`) is deliberately not
471
- // followed: locating the returned object there means tracking control flow,
472
- // and editing it in place would be guesswork. Those users get the explicit
473
- // message above rather than a silently wrong edit.
474
- if (source[index] !== "(") return -1
475
- index = skipWhitespaceAndComments(source, index + 1)
476
-
477
- return source[index] === "{" ? index : -1
478
- }
1
+ // The scanner reports syntax problems without naming a dialect; this module is
2
+ // the only place that knows the source is a Vite config, so it adds that word.
3
+ function asViteConfigError(error) {
4
+ if (error instanceof Error && !/^unsupported Vite config: /.test(error.message)) {
5
+ error.message = `unsupported Vite config: ${error.message}`
6
+ }
7
+ return error
8
+ }
9
+
10
+ // Vite config editing. This module owns every assumption about what a Vite
11
+ // config looks like; the scanner primitives it builds on know nothing about Vite.
12
+
13
+ import {
14
+ appendArrayElement,
15
+ appendObjectProperty,
16
+ arrayHasCallExpression,
17
+ createBracketRange,
18
+ expectArrayLiteralValue,
19
+ expectObjectLiteralValue,
20
+ findExportDefaultExpressionStart,
21
+ findObjectProperty,
22
+ findTopLevelCommaOrEnd,
23
+ insertObjectProperty,
24
+ parseImportDeclarations,
25
+ prependImportLines,
26
+ readIdentifierAt,
27
+ readObjectPropertyNames,
28
+ readPropertyName,
29
+ readStringLiteral,
30
+ skipWhitespaceAndComments,
31
+ skipWhitespaceCommentsAndCommas
32
+ } from "./source-scanner.js"
33
+
34
+ function ensureNodeUrlImport(source) {
35
+ const imports = parseImportDeclarations(source)
36
+ const nodeUrlImports = imports.filter((entry) => entry.moduleSpecifier === "node:url")
37
+ const importedLocals = new Set(nodeUrlImports.flatMap((entry) => entry.namedLocals))
38
+ const missing = ["fileURLToPath", "URL"].filter((name) => !importedLocals.has(name))
39
+
40
+ if (missing.length === 0) return source
41
+
42
+ return prependImportLines(
43
+ source,
44
+ [`import { ${missing.join(", ")} } from "node:url"`]
45
+ )
46
+ }
47
+
48
+ function ensureTailwindViteImport(source, localName) {
49
+ const imports = parseImportDeclarations(source)
50
+ const tailwindImport = imports.find(
51
+ (entry) => entry.moduleSpecifier === "@tailwindcss/vite"
52
+ )
53
+
54
+ if (!tailwindImport) {
55
+ return prependImportLines(
56
+ source,
57
+ [`import ${localName} from "@tailwindcss/vite"`]
58
+ )
59
+ }
60
+
61
+ if (!tailwindImport.defaultLocal) {
62
+ throw new Error(
63
+ "unsupported Vite config: @tailwindcss/vite must be imported with a default binding."
64
+ )
65
+ }
66
+
67
+ return source
68
+ }
69
+
70
+ export function updateViteConfigSource(source, options) {
71
+ try {
72
+ return updateViteConfigSourceUnchecked(source, options)
73
+ } catch (error) {
74
+ throw asViteConfigError(error)
75
+ }
76
+ }
77
+
78
+ function updateViteConfigSourceUnchecked(source, { aliasRoot, sourceRoot }) {
79
+ validateViteConfigStructure(source)
80
+ const tailwindLocalName = getTailwindViteLocalName(source)
81
+ let next = aliasRoot
82
+ ? addViteResolveAlias(source, aliasRoot, sourceRoot)
83
+ : source
84
+ const addedFileUrlAlias = Boolean(aliasRoot) && next !== source
85
+ next = addVitePluginCall(next, `${tailwindLocalName}()`, tailwindLocalName)
86
+ next = ensureTailwindViteImport(next, tailwindLocalName)
87
+ if (addedFileUrlAlias) next = ensureNodeUrlImport(next)
88
+ return next
89
+ }
90
+
91
+ export function readViteAliasSourceRoot(source, options) {
92
+ try {
93
+ return readViteAliasSourceRootUnchecked(source, options)
94
+ } catch (error) {
95
+ throw asViteConfigError(error)
96
+ }
97
+ }
98
+
99
+ function readViteAliasSourceRootUnchecked(source, { aliasRoot }) {
100
+ validateViteConfigStructure(source)
101
+ const configObject = findViteConfigObject(source)
102
+ const resolveProperty = findObjectProperty(source, configObject, "resolve")
103
+ if (!resolveProperty) return null
104
+
105
+ const resolveObject = expectObjectLiteralValue(
106
+ source,
107
+ resolveProperty,
108
+ "unsupported Vite config: resolve must be an object literal."
109
+ )
110
+ const aliasProperty = findObjectProperty(source, resolveObject, "alias")
111
+ if (!aliasProperty) return null
112
+
113
+ const aliasObject = expectObjectLiteralValue(
114
+ source,
115
+ aliasProperty,
116
+ "unsupported Vite config: resolve.alias must be an object literal."
117
+ )
118
+ const rootProperty = findObjectProperty(source, aliasObject, aliasRoot)
119
+ if (!rootProperty) return null
120
+
121
+ const expression = source
122
+ .slice(rootProperty.valueStart, rootProperty.valueEnd)
123
+ .trim()
124
+ const staticRoot = readStaticAliasRoot(expression)
125
+ if (staticRoot !== null) {
126
+ assertStaticAliasBinding(source, staticRoot)
127
+ return staticRoot
128
+ }
129
+
130
+ throw new Error(
131
+ `unsupported Vite config: resolve.alias[${JSON.stringify(aliasRoot)}] must use a static string, ` +
132
+ "fileURLToPath(new URL(..., import.meta.url)), or path.resolve(__dirname, ...)."
133
+ )
134
+ }
135
+
136
+ export function readViteAliasKeys(source) {
137
+ try {
138
+ return readViteAliasKeysUnchecked(source)
139
+ } catch (error) {
140
+ throw asViteConfigError(error)
141
+ }
142
+ }
143
+
144
+ function readViteAliasKeysUnchecked(source) {
145
+ validateViteConfigStructure(source)
146
+ const configObject = findViteConfigObject(source)
147
+ const resolveProperty = findObjectProperty(source, configObject, "resolve")
148
+ if (!resolveProperty) return []
149
+
150
+ const resolveObject = expectObjectLiteralValue(
151
+ source,
152
+ resolveProperty,
153
+ "unsupported Vite config: resolve must be an object literal."
154
+ )
155
+ const aliasProperty = findObjectProperty(source, resolveObject, "alias")
156
+ if (!aliasProperty) return []
157
+
158
+ const aliasObject = expectObjectLiteralValue(
159
+ source,
160
+ aliasProperty,
161
+ "unsupported Vite config: resolve.alias must be an object literal."
162
+ )
163
+ return readObjectPropertyNames(source, aliasObject)
164
+ }
165
+
166
+ function readStaticAliasRoot(expression) {
167
+ const directLiteral = readStringLiteral(expression, 0)
168
+ if (
169
+ directLiteral &&
170
+ skipWhitespaceAndComments(expression, directLiteral.end) === expression.length
171
+ ) {
172
+ return { kind: "literal", value: directLiteral.value }
173
+ }
174
+
175
+ const patterns = [
176
+ {
177
+ kind: "file-url",
178
+ pattern: /^fileURLToPath\s*\(\s*new\s+URL\s*\(\s*(["'])(.*?)\1\s*,\s*import\.meta\.url\s*\)\s*\)$/s,
179
+ },
180
+ {
181
+ kind: "path-default",
182
+ pattern: /^path\.resolve\s*\(\s*(?:__dirname|import\.meta\.dirname)\s*,\s*(["'])(.*?)\1\s*\)$/s,
183
+ },
184
+ {
185
+ kind: "path-named",
186
+ pattern: /^resolve\s*\(\s*(?:__dirname|import\.meta\.dirname)\s*,\s*(["'])(.*?)\1\s*\)$/s,
187
+ },
188
+ ]
189
+
190
+ for (const candidate of patterns) {
191
+ const match = expression.match(candidate.pattern)
192
+ if (match) {
193
+ return {
194
+ kind: candidate.kind,
195
+ value: match[2].replace(/\\(.)/g, "$1"),
196
+ }
197
+ }
198
+ }
199
+
200
+ return null
201
+ }
202
+
203
+ function assertStaticAliasBinding(source, staticRoot) {
204
+ const imports = parseImportDeclarations(source)
205
+
206
+ if (staticRoot.kind === "file-url") {
207
+ const nodeUrlImport = imports.find(
208
+ (entry) =>
209
+ (entry.moduleSpecifier === "node:url" || entry.moduleSpecifier === "url") &&
210
+ entry.namedLocals.includes("fileURLToPath")
211
+ )
212
+ if (!nodeUrlImport) {
213
+ throw new Error(
214
+ "unsupported Vite config: fileURLToPath must be imported by name from node:url."
215
+ )
216
+ }
217
+ }
218
+
219
+ if (staticRoot.kind === "path-default") {
220
+ const nodePathImport = imports.find(
221
+ (entry) =>
222
+ (entry.moduleSpecifier === "node:path" || entry.moduleSpecifier === "path") &&
223
+ entry.defaultLocal === "path"
224
+ )
225
+ if (!nodePathImport) {
226
+ throw new Error(
227
+ "unsupported Vite config: path must be a default import from node:path."
228
+ )
229
+ }
230
+ }
231
+
232
+ if (staticRoot.kind === "path-named") {
233
+ const nodePathImport = imports.find(
234
+ (entry) =>
235
+ (entry.moduleSpecifier === "node:path" || entry.moduleSpecifier === "path") &&
236
+ entry.namedLocals.includes("resolve")
237
+ )
238
+ if (!nodePathImport) {
239
+ throw new Error(
240
+ "unsupported Vite config: resolve must be imported by name from node:path."
241
+ )
242
+ }
243
+ }
244
+ }
245
+
246
+ function validateViteConfigStructure(source) {
247
+ const configObject = findViteConfigObject(source)
248
+ assertStaticObjectStructure(source, configObject, "config")
249
+
250
+ const resolveProperty = findObjectProperty(source, configObject, "resolve")
251
+ if (!resolveProperty) return
252
+
253
+ const resolveObject = expectObjectLiteralValue(
254
+ source,
255
+ resolveProperty,
256
+ "unsupported Vite config: resolve must be an object literal."
257
+ )
258
+ assertStaticObjectStructure(source, resolveObject, "resolve")
259
+
260
+ const aliasProperty = findObjectProperty(source, resolveObject, "alias")
261
+ if (!aliasProperty) return
262
+
263
+ const aliasObject = expectObjectLiteralValue(
264
+ source,
265
+ aliasProperty,
266
+ "unsupported Vite config: resolve.alias must be an object literal."
267
+ )
268
+ assertStaticObjectStructure(source, aliasObject, "resolve.alias")
269
+ }
270
+
271
+ function assertStaticObjectStructure(source, objectRange, label) {
272
+ const bodyEnd = objectRange.end - 1
273
+ const seen = new Set()
274
+ let index = objectRange.start + 1
275
+
276
+ while (index < bodyEnd) {
277
+ index = skipWhitespaceCommentsAndCommas(source, index, bodyEnd)
278
+ if (index >= bodyEnd) return
279
+
280
+ const propertyStart = index
281
+ const propertyEnd = findTopLevelCommaOrEnd(
282
+ source,
283
+ propertyStart,
284
+ bodyEnd
285
+ )
286
+
287
+ if (source.startsWith("...", propertyStart)) {
288
+ throw new Error(
289
+ `unsupported Vite config: ${label} cannot contain spread properties.`
290
+ )
291
+ }
292
+ if (source[propertyStart] === "[") {
293
+ throw new Error(
294
+ `unsupported Vite config: ${label} cannot contain computed properties.`
295
+ )
296
+ }
297
+
298
+ const name = readPropertyName(source, propertyStart)
299
+ if (name) {
300
+ if (seen.has(name.name)) {
301
+ throw new Error(
302
+ `unsupported Vite config: ${label} contains duplicate property ${JSON.stringify(name.name)}.`
303
+ )
304
+ }
305
+ seen.add(name.name)
306
+ }
307
+
308
+ index = propertyEnd + 1
309
+ }
310
+ }
311
+
312
+ function getTailwindViteLocalName(source) {
313
+ const tailwindImport = parseImportDeclarations(source).find(
314
+ (entry) => entry.moduleSpecifier === "@tailwindcss/vite"
315
+ )
316
+
317
+ if (!tailwindImport) return "tailwindcss"
318
+ if (tailwindImport.defaultLocal) return tailwindImport.defaultLocal
319
+
320
+ throw new Error(
321
+ "unsupported Vite config: @tailwindcss/vite must be imported with a default binding."
322
+ )
323
+ }
324
+
325
+ function addViteResolveAlias(source, aliasRoot, sourceRoot) {
326
+ const configObject = findViteConfigObject(source)
327
+ const resolveProperty = findObjectProperty(source, configObject, "resolve")
328
+ const aliasEntry = `${JSON.stringify(aliasRoot)}: fileURLToPath(new URL(${JSON.stringify(
329
+ sourceRoot
330
+ )}, import.meta.url))`
331
+
332
+ if (!resolveProperty) {
333
+ return insertObjectProperty(
334
+ source,
335
+ configObject,
336
+ [
337
+ "resolve: {",
338
+ " alias: {",
339
+ ` ${aliasEntry},`,
340
+ " },",
341
+ "},",
342
+ ].join("\n")
343
+ )
344
+ }
345
+
346
+ const resolveObject = expectObjectLiteralValue(
347
+ source,
348
+ resolveProperty,
349
+ "unsupported Vite config: resolve must be an object literal."
350
+ )
351
+ const aliasProperty = findObjectProperty(source, resolveObject, "alias")
352
+
353
+ if (!aliasProperty) {
354
+ return insertObjectProperty(
355
+ source,
356
+ resolveObject,
357
+ [
358
+ "alias: {",
359
+ ` ${aliasEntry},`,
360
+ "},",
361
+ ].join("\n")
362
+ )
363
+ }
364
+
365
+ const aliasObject = expectObjectLiteralValue(
366
+ source,
367
+ aliasProperty,
368
+ "unsupported Vite config: resolve.alias must be an object literal."
369
+ )
370
+
371
+ if (findObjectProperty(source, aliasObject, aliasRoot)) {
372
+ return source
373
+ }
374
+
375
+ return appendObjectProperty(source, aliasObject, `${aliasEntry},`)
376
+ }
377
+
378
+ function addVitePluginCall(source, pluginCall, pluginLocalName) {
379
+ const configObject = findViteConfigObject(source)
380
+ const pluginsProperty = findObjectProperty(source, configObject, "plugins")
381
+
382
+ if (!pluginsProperty) {
383
+ return insertObjectProperty(source, configObject, `plugins: [${pluginCall}],`)
384
+ }
385
+
386
+ const pluginsArray = expectArrayLiteralValue(
387
+ source,
388
+ pluginsProperty,
389
+ "unsupported Vite config: plugins must be an array literal."
390
+ )
391
+
392
+ if (arrayHasCallExpression(source, pluginsArray, pluginLocalName)) {
393
+ return source
394
+ }
395
+
396
+ return appendArrayElement(source, pluginsArray, pluginCall)
397
+ }
398
+
399
+ function findViteConfigObject(source) {
400
+ const expressionStart = findExportDefaultExpressionStart(source)
401
+
402
+ if (expressionStart === -1) {
403
+ throw new Error(
404
+ "unsupported Vite config: expected `export default defineConfig({ ... })` or `export default { ... }`."
405
+ )
406
+ }
407
+
408
+ if (source[expressionStart] === "{") {
409
+ return createBracketRange(source, expressionStart, "{")
410
+ }
411
+
412
+ const callee = readIdentifierAt(source, expressionStart)
413
+ if (callee?.name !== "defineConfig") {
414
+ throw new Error(
415
+ "unsupported Vite config: default export must be `defineConfig({ ... })` or an object literal."
416
+ )
417
+ }
418
+
419
+ let index = skipWhitespaceAndComments(source, callee.end)
420
+ if (source[index] !== "(") {
421
+ throw new Error("unsupported Vite config: malformed defineConfig call.")
422
+ }
423
+
424
+ index = skipWhitespaceAndComments(source, index + 1)
425
+
426
+ // defineConfig({ ... })
427
+ if (source[index] === "{") {
428
+ return createBracketRange(source, index, "{")
429
+ }
430
+
431
+ // defineConfig((env) => ({ ... })) — the other documented Vite form, used
432
+ // whenever the config depends on `mode` or `command`. The config object is
433
+ // the arrow's returned expression, so resolve to that and edit it in place.
434
+ const arrowBody = findConfigArrowBodyStart(source, index)
435
+ if (arrowBody !== -1) {
436
+ return createBracketRange(source, arrowBody, "{")
437
+ }
438
+
439
+ throw new Error(
440
+ "unsupported Vite config: defineConfig must receive a config object literal " +
441
+ "or an arrow function returning one, e.g. `defineConfig(({ mode }) => ({ ... }))`."
442
+ )
443
+ }
444
+
445
+ // Given the index of defineConfig's first argument, return the index of the
446
+ // object literal an arrow function returns, or -1 when the argument is not an
447
+ // arrow whose body is a parenthesised object.
448
+ function findConfigArrowBodyStart(source, argumentStart) {
449
+ let index = argumentStart
450
+
451
+ // An optional `async` before the parameter list.
452
+ const asyncKeyword = readIdentifierAt(source, index)
453
+ if (asyncKeyword?.name === "async") {
454
+ index = skipWhitespaceAndComments(source, asyncKeyword.end)
455
+ }
456
+
457
+ // Parameter list: either `(...)` or a single bare identifier.
458
+ if (source[index] === "(") {
459
+ const parameters = createBracketRange(source, index, "(")
460
+ index = skipWhitespaceAndComments(source, parameters.end + 1)
461
+ } else {
462
+ const parameter = readIdentifierAt(source, index)
463
+ if (!parameter) return -1
464
+ index = skipWhitespaceAndComments(source, parameter.end)
465
+ }
466
+
467
+ if (source.slice(index, index + 2) !== "=>") return -1
468
+ index = skipWhitespaceAndComments(source, index + 2)
469
+
470
+ // `=> ({ ... })`. A block body (`=> { return { ... } }`) is deliberately not
471
+ // followed: locating the returned object there means tracking control flow,
472
+ // and editing it in place would be guesswork. Those users get the explicit
473
+ // message above rather than a silently wrong edit.
474
+ if (source[index] !== "(") return -1
475
+ index = skipWhitespaceAndComments(source, index + 1)
476
+
477
+ return source[index] === "{" ? index : -1
478
+ }