bitboss-ui 3.0.0-beta.48 → 3.0.0-beta.49
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/README.md +24 -7
- package/bin/bitboss-ui-mcp.mjs +15 -3
- package/bin/bitboss-ui.mjs +115 -13
- package/dist/ai/BbAvatar.md +1 -1
- package/dist/ai/BbBreadcrumbs.md +65 -65
- package/dist/ai/BbButton.md +16 -5
- package/dist/ai/BbCheckbox.md +0 -2
- package/dist/ai/BbCheckboxGroup.md +13 -20
- package/dist/ai/BbDatePickerInput.md +4 -2
- package/dist/ai/BbDialog.md +1 -8
- package/dist/ai/BbDropdown.md +231 -220
- package/dist/ai/BbDropdownButton.md +65 -65
- package/dist/ai/BbDropdownGroup.md +2 -2
- package/dist/ai/BbDropzone.md +10 -10
- package/dist/ai/BbForm.md +43 -52
- package/dist/ai/BbHoverCard.md +4 -2
- package/dist/ai/BbNumberInput.md +88 -21
- package/dist/ai/BbPopover.md +29 -23
- package/dist/ai/BbProgress.md +48 -22
- package/dist/ai/BbRadioGroup.md +13 -19
- package/dist/ai/BbSelect.md +16 -36
- package/dist/ai/BbSelectPopover.md +14 -30
- package/dist/ai/BbSmoothHeight.md +0 -2
- package/dist/ai/BbSwitchGroup.md +13 -20
- package/dist/ai/BbTable.md +11 -10
- package/dist/ai/BbTabs.md +106 -105
- package/dist/ai/BbTabsList.md +2 -2
- package/dist/ai/BbTabsPanels.md +1 -1
- package/dist/ai/BbTabsRoot.md +2 -2
- package/dist/ai/BbTooltip.md +24 -10
- package/dist/ai/BbTree.md +14 -11
- package/dist/ai/changelog.json +256 -5
- package/dist/ai/components.json +36 -34
- package/dist/ai/composables/useBbDropdownContext.md +16 -16
- package/dist/ai/composables/useBbFormField.md +5 -1
- package/dist/ai/composables/useBbTabsContext.md +14 -14
- package/dist/ai/guides/agent-contract.md +27 -27
- package/dist/ai/guides/ai-router.md +1 -1
- package/dist/ai/guides/component-picker.md +3 -3
- package/dist/ai/guides/fetch-items-playbook.md +1 -1
- package/dist/ai/guides/icons-policy.md +10 -10
- package/dist/ai/guides/inertia-helpers.md +120 -67
- package/dist/ai/guides/installation-and-plugin-setup.md +43 -15
- package/dist/ai/guides/migration/components/bb-breadcrumbs.md +3 -2
- package/dist/ai/guides/migration/components/bb-checkbox-group.md +16 -16
- package/dist/ai/guides/migration/components/bb-dropdown-button.md +9 -8
- package/dist/ai/guides/migration/components/bb-dropdown.md +15 -14
- package/dist/ai/guides/migration/components/bb-radio-group.md +2 -1
- package/dist/ai/guides/migration/components/bb-select-popover.md +6 -1
- package/dist/ai/guides/migration/components/bb-select.md +10 -2
- package/dist/ai/guides/migration/components/bb-switch-group.md +2 -1
- package/dist/ai/guides/migration/components/bb-tabs.md +9 -8
- package/dist/ai/guides/migration/components/bb-tooltip.md +1 -1
- package/dist/ai/guides/migration/v2-to-v3.md +114 -17
- package/dist/ai/guides/migration/v3-beta-tooling.md +11 -0
- package/dist/ai/guides/options-items-playbook.md +30 -24
- package/dist/ai/guides/passthrough.md +2 -2
- package/dist/ai/guides/preset-components.md +18 -3
- package/dist/ai/guides/typescript-generic-components.md +1 -1
- package/dist/ai/guides/validated-forms.md +11 -22
- package/dist/ai/guides/validation-providers.md +4 -3
- package/dist/ai/helpers/inertia/manifest.json +636 -0
- package/dist/ai/helpers/inertia/types.ts +15 -0
- package/dist/ai/helpers/inertia/useAsyncFn.ts +110 -0
- package/dist/ai/helpers/inertia/useConfirmOnRouteLeave.ts +102 -0
- package/dist/ai/helpers/inertia/useDeleteCmd.ts +30 -0
- package/dist/ai/helpers/inertia/useHasChanged.ts +36 -0
- package/dist/ai/helpers/inertia/useIndexById.ts +23 -0
- package/dist/ai/helpers/inertia/useLazyProp.ts +148 -0
- package/dist/ai/helpers/inertia/useLazyPropGetter.ts +50 -0
- package/dist/ai/helpers/inertia/useLoading.ts +20 -0
- package/dist/ai/helpers/inertia/usePaginatedResponseContext.ts +88 -0
- package/dist/ai/helpers/inertia/usePromiseForm.ts +62 -0
- package/dist/ai/helpers/inertia/useProp.ts +63 -0
- package/dist/ai/helpers/inertia/useQuery.ts +19 -0
- package/dist/ai/helpers/inertia/useQueryObject.ts +116 -0
- package/dist/ai/helpers/inertia/useResourceContext.ts +69 -0
- package/dist/ai/helpers/inertia/useRouteQuery.ts +54 -0
- package/dist/ai/helpers/inertia/useScreenSizes.ts +23 -0
- package/dist/ai/helpers/inertia/useTableContext.ts +34 -0
- package/dist/ai/helpers/inertia/useTableSelectionContext.ts +48 -0
- package/dist/ai/helpers/inertia/useUntil.ts +33 -0
- package/dist/ai/helpers/inertia/utils.ts +24 -0
- package/dist/ai/index.md +6 -6
- package/dist/ai/manifest/components/BbBreadcrumbs.json +10 -6
- package/dist/ai/manifest/components/BbBreadcrumbs.tree.json +1 -1
- package/dist/ai/manifest/components/BbButton.json +1 -1
- package/dist/ai/manifest/components/BbCheckboxGroup.json +2 -2
- package/dist/ai/manifest/components/BbDropdown.json +23 -20
- package/dist/ai/manifest/components/BbDropdown.tree.json +2 -2
- package/dist/ai/manifest/components/BbDropdownButton.json +22 -19
- package/dist/ai/manifest/components/BbDropdownButton.tree.json +1 -1
- package/dist/ai/manifest/components/BbDropdownGroup.json +2 -2
- package/dist/ai/manifest/components/BbForm.json +1 -0
- package/dist/ai/manifest/components/BbNumberInput.json +6 -1
- package/dist/ai/manifest/components/BbNumberInput.tree.json +2 -1
- package/dist/ai/manifest/components/BbPopover.json +1 -0
- package/dist/ai/manifest/components/BbProgress.json +6 -6
- package/dist/ai/manifest/components/BbProgress.tree.json +1 -0
- package/dist/ai/manifest/components/BbRadioGroup.json +2 -2
- package/dist/ai/manifest/components/BbSelect.json +2 -2
- package/dist/ai/manifest/components/BbSelectPopover.json +2 -2
- package/dist/ai/manifest/components/BbSwitchGroup.json +2 -2
- package/dist/ai/manifest/components/BbTabs.json +7 -6
- package/dist/ai/manifest/components/BbTabsList.json +3 -3
- package/dist/ai/manifest/components/BbTabsPanels.json +1 -1
- package/dist/ai/manifest/components/BbTabsRoot.json +5 -4
- package/dist/ai/manifest/components/BbTooltip.json +1 -1
- package/dist/ai/manifest/index.json +2 -2
- package/dist/ai/manifest/meta.json +2 -0
- package/dist/ai/manifest/types.advanced.json +10 -9
- package/dist/ai/manifest/types.api.json +5 -4
- package/dist/ai/recipes/inertia/approvals-inbox.md +16 -18
- package/dist/ai/recipes/inertia/inline-edit-workspace.md +23 -33
- package/dist/ai/recipes/inertia/layout-scaffold.md +16 -16
- package/dist/ai/recipes/inertia/onboarding.md +6 -10
- package/dist/ai/recipes/inertia/ownership-atlas.md +15 -18
- package/dist/ai/recipes/inertia/record-form.md +11 -11
- package/dist/ai/recipes/inertia/records-workspace.md +38 -41
- package/dist/ai/recipes/inertia/settings-workspace.md +26 -38
- package/dist/ai/recipes/inertia/upload-center.md +31 -39
- package/dist/ai/recipes/inertia/wizard-form.md +10 -19
- package/dist/ai/recipes/nuxt/approvals-inbox.md +7 -8
- package/dist/ai/recipes/nuxt/inline-edit-workspace.md +14 -24
- package/dist/ai/recipes/nuxt/layout-scaffold.md +16 -16
- package/dist/ai/recipes/nuxt/onboarding.md +2 -6
- package/dist/ai/recipes/nuxt/ownership-atlas.md +7 -9
- package/dist/ai/recipes/nuxt/record-form.md +5 -5
- package/dist/ai/recipes/nuxt/records-workspace.md +29 -31
- package/dist/ai/recipes/nuxt/settings-workspace.md +22 -34
- package/dist/ai/recipes/nuxt/upload-center.md +22 -29
- package/dist/ai/recipes/nuxt/wizard-form.md +3 -11
- package/dist/ai/recipes/vue/approvals-inbox.md +7 -8
- package/dist/ai/recipes/vue/inline-edit-workspace.md +14 -24
- package/dist/ai/recipes/vue/layout-scaffold.md +16 -16
- package/dist/ai/recipes/vue/onboarding.md +2 -6
- package/dist/ai/recipes/vue/ownership-atlas.md +7 -9
- package/dist/ai/recipes/vue/record-form.md +5 -5
- package/dist/ai/recipes/vue/records-workspace.md +29 -31
- package/dist/ai/recipes/vue/settings-workspace.md +22 -34
- package/dist/ai/recipes/vue/upload-center.md +22 -29
- package/dist/ai/recipes/vue/wizard-form.md +3 -11
- package/dist/ai/source/BbAccordion.md +1 -1
- package/dist/ai/source/BbAlert.md +3 -3
- package/dist/ai/source/BbBadge.md +2 -2
- package/dist/ai/source/BbBadgeButton.md +5 -4
- package/dist/ai/source/BbBaseButton.md +1 -1
- package/dist/ai/source/BbBaseRadioIcon.md +1 -0
- package/dist/ai/source/BbBaseSwitchIcon.md +1 -0
- package/dist/ai/source/BbBreadcrumbs.md +63 -42
- package/dist/ai/source/BbButton.md +71 -33
- package/dist/ai/source/BbCalendar.md +17 -16
- package/dist/ai/source/BbCheckbox.md +3 -2
- package/dist/ai/source/BbCheckboxGroup.md +11 -4
- package/dist/ai/source/BbColorInput.md +10 -9
- package/dist/ai/source/BbConfirm.md +1 -1
- package/dist/ai/source/BbConfirmPortal.md +1 -1
- package/dist/ai/source/BbDatePicker.md +2 -2
- package/dist/ai/source/BbDatePickerInput.md +47 -20
- package/dist/ai/source/BbDialog.md +5 -5
- package/dist/ai/source/BbDropdown.md +230 -158
- package/dist/ai/source/BbDropdownButton.md +19 -12
- package/dist/ai/source/BbDropdownGroup.md +126 -93
- package/dist/ai/source/BbForm.md +25 -1
- package/dist/ai/source/BbIndicator.md +4 -0
- package/dist/ai/source/BbNumberInput.md +855 -19
- package/dist/ai/source/BbOffCanvas.md +27 -11
- package/dist/ai/source/BbPagination.md +3 -3
- package/dist/ai/source/BbPopover.md +152 -8
- package/dist/ai/source/BbPopoverPortal.md +11 -0
- package/dist/ai/source/BbProgress.md +82 -18
- package/dist/ai/source/BbRadioGroup.md +12 -5
- package/dist/ai/source/BbRating.md +11 -9
- package/dist/ai/source/BbSelect.md +16 -10
- package/dist/ai/source/BbSelectPopover.md +29 -23
- package/dist/ai/source/BbSlider.md +12 -6
- package/dist/ai/source/BbSwitch.md +3 -2
- package/dist/ai/source/BbSwitchGroup.md +5 -2
- package/dist/ai/source/BbTable.md +102 -18
- package/dist/ai/source/BbTabs.md +27 -23
- package/dist/ai/source/BbTabsList.md +26 -22
- package/dist/ai/source/BbTabsPanels.md +26 -22
- package/dist/ai/source/BbTabsRoot.md +25 -21
- package/dist/ai/source/BbTag.md +10 -9
- package/dist/ai/source/BbTextInput.md +5 -4
- package/dist/ai/source/BbTextarea.md +6 -5
- package/dist/ai/source/BbTimePicker.md +1 -1
- package/dist/ai/source/BbTimePickerInput.md +46 -9
- package/dist/ai/source/BbToast.md +10 -7
- package/dist/ai/source/BbToastPortal.md +10 -7
- package/dist/ai/source/BbTooltip.md +54 -8
- package/dist/ai/source/BbTree.md +21 -3
- package/dist/ai/source/CommonField.md +6 -4
- package/dist/ai/source/CommonFieldInput.md +15 -10
- package/dist/ai/source/CommonPopover.md +6 -0
- package/dist/ai/source/CommonTimeSelector.md +5 -4
- package/dist/components/BbBadge/BbBadgeButton.vue_vue_type_script_setup_true_lang.js +10 -8
- package/dist/components/BbBaseButton/BbBaseButton.vue_vue_type_script_setup_true_lang.js +8 -8
- package/dist/components/BbBreadcrumbs/BbBreadcrumbs.vue_vue_type_script_setup_true_lang.js +45 -45
- package/dist/components/BbBreadcrumbs/types.d.ts +25 -17
- package/dist/components/BbButton/BbButton.vue_vue_type_script_setup_true_lang.js +84 -71
- package/dist/components/BbButton/toggleRoving.d.ts +6 -0
- package/dist/components/BbButton/toggleRoving.js +45 -0
- package/dist/components/BbButton/types.d.ts +4 -1
- package/dist/components/BbCalendar/BbCalendar.vue_vue_type_script_setup_true_lang.js +178 -178
- package/dist/components/BbCalendar/CalendarMonthPanel.vue_vue_type_script_setup_true_lang.js +15 -15
- package/dist/components/BbCalendar/CalendarYearPanel.vue_vue_type_script_setup_true_lang.js +28 -28
- package/dist/components/BbCalendar/useCalendarGrid.js +141 -139
- package/dist/components/BbCalendar/useDayjsLocale.d.ts +38 -15
- package/dist/components/BbCalendar/useDayjsLocale.js +31 -30
- package/dist/components/BbCheckbox/BbCheckbox.vue_vue_type_script_setup_true_lang.js +48 -48
- package/dist/components/BbCheckboxGroup/BbCheckboxGroup.vue_vue_type_script_setup_true_lang.js +81 -80
- package/dist/components/BbCheckboxGroup/types.d.ts +6 -2
- package/dist/components/BbColorInput/BbColorInput.vue_vue_type_script_setup_true_lang.js +69 -69
- package/dist/components/BbDatePickerInput/BbDatePickerInput.vue_vue_type_script_setup_true_lang.js +363 -349
- package/dist/components/BbDropdown/AdaptiveDropdown.vue_vue_type_script_setup_true_lang.js +91 -86
- package/dist/components/BbDropdown/BbDropdown.vue_vue_type_script_setup_true_lang.js +298 -289
- package/dist/components/BbDropdown/BbDropdownList.vue_vue_type_script_setup_true_lang.js +322 -356
- package/dist/components/BbDropdown/DropdownPipelineResolver.vue_vue_type_script_setup_true_lang.js +17 -15
- package/dist/components/BbDropdown/DropdownRow.vue.d.ts +40 -0
- package/dist/components/BbDropdown/DropdownRow.vue.js +5 -0
- package/dist/components/BbDropdown/DropdownRow.vue_vue_type_script_setup_true_lang.js +60 -0
- package/dist/components/BbDropdown/normalizeGroups.d.ts +8 -2
- package/dist/components/BbDropdown/normalizeGroups.js +60 -46
- package/dist/components/BbDropdown/types.d.ts +119 -88
- package/dist/components/BbDropdownButton/BbDropdownButton.vue_vue_type_script_setup_true_lang.js +11 -2
- package/dist/components/BbDropdownButton/types.d.ts +2 -2
- package/dist/components/BbForm/BbForm.vue.d.ts +9 -0
- package/dist/components/BbForm/BbForm.vue_vue_type_script_setup_true_lang.js +114 -105
- package/dist/components/BbForm/BbFormCustomField.js +1 -1
- package/dist/components/BbForm/bindFormField.js +2 -1
- package/dist/components/BbForm/formFields.d.ts +5 -0
- package/dist/components/BbNumberInput/BbNumberInput.vue.d.ts +0 -1
- package/dist/components/BbNumberInput/BbNumberInput.vue_vue_type_script_setup_true_lang.js +452 -125
- package/dist/components/BbNumberInput/numberDisplay.d.ts +256 -0
- package/dist/components/BbNumberInput/numberDisplay.js +499 -0
- package/dist/components/BbNumberInput/types.d.ts +54 -1
- package/dist/components/BbNumberInput/warroom/corpus.d.ts +58 -0
- package/dist/components/BbNumberInput/warroom/currency-verify.rows.d.ts +28 -0
- package/dist/components/BbNumberInput/warroom/currency.rows.d.ts +2 -0
- package/dist/components/BbNumberInput/warroom/harness.d.ts +217 -0
- package/dist/components/BbNumberInput/warroom/oracle.d.ts +150 -0
- package/dist/components/BbPagination/BbPagination.vue_vue_type_script_setup_true_lang.js +7 -7
- package/dist/components/BbPopover/BbPopover.vue.d.ts +1 -0
- package/dist/components/BbPopover/BbPopover.vue_vue_type_script_setup_true_lang.js +207 -164
- package/dist/components/BbPopover/types.d.ts +11 -0
- package/dist/components/BbProgress/BbProgress.vue_vue_type_script_setup_true_lang.js +15 -7
- package/dist/components/BbProgress/types.d.ts +23 -7
- package/dist/components/BbProgress/types.js +1 -1
- package/dist/components/BbRadioGroup/BbRadioGroup.vue_vue_type_script_setup_true_lang.js +102 -101
- package/dist/components/BbRadioGroup/types.d.ts +6 -2
- package/dist/components/BbRating/BbRating.vue_vue_type_script_setup_true_lang.js +63 -63
- package/dist/components/BbSelect/BbSelect.vue_vue_type_script_setup_true_lang.js +156 -154
- package/dist/components/BbSelect/types.d.ts +6 -2
- package/dist/components/BbSelectPopover/BbSelectPopover.vue_vue_type_script_setup_true_lang.js +2 -1
- package/dist/components/BbSelectPopover/types.d.ts +6 -2
- package/dist/components/BbSlider/BbSlider.vue_vue_type_script_setup_true_lang.js +153 -153
- package/dist/components/BbSwitch/BbSwitch.vue_vue_type_script_setup_true_lang.js +62 -62
- package/dist/components/BbSwitchGroup/BbSwitchGroup.vue_vue_type_script_setup_true_lang.js +131 -130
- package/dist/components/BbTable/BbTable.vue_vue_type_script_setup_true_lang.js +711 -676
- package/dist/components/BbTabs/BbTabs.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/components/BbTabs/BbTabsList.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/components/BbTabs/types.d.ts +25 -21
- package/dist/components/BbTag/BbTag.vue_vue_type_script_setup_true_lang.js +76 -76
- package/dist/components/BbTextInput/BbTextInput.vue_vue_type_script_setup_true_lang.js +58 -58
- package/dist/components/BbTextarea/BbTextarea.vue_vue_type_script_setup_true_lang.js +82 -82
- package/dist/components/BbTimePickerInput/BbTimePickerInput.vue_vue_type_script_setup_true_lang.js +246 -236
- package/dist/components/BbTooltip/BbTooltip.vue_vue_type_script_setup_true_lang.js +102 -97
- package/dist/components/BbTooltip/tooltipWarmup.d.ts +23 -0
- package/dist/components/BbTooltip/tooltipWarmup.js +7 -0
- package/dist/components/BbTooltip/types.d.ts +7 -2
- package/dist/components/BbTree/BbTree.vue_vue_type_script_setup_true_lang.js +29 -21
- package/dist/components/BbTree/BbTreeLevel.vue_vue_type_script_setup_true_lang.js +10 -7
- package/dist/components/CommonFieldInput/CommonFieldInput.vue_vue_type_script_setup_true_lang.js +37 -36
- package/dist/components/CommonTimeSelector/CommonTimeSelector.vue_vue_type_script_setup_true_lang.js +9 -9
- package/dist/composables/formSubmitLock.d.ts +6 -0
- package/dist/composables/formSubmitLock.js +4 -0
- package/dist/composables/useBaseOptions.d.ts +7 -1
- package/dist/composables/useBaseOptions.js +32 -32
- package/dist/composables/useBbDropdownContext.d.ts +8 -8
- package/dist/composables/useBbFormField.d.ts +9 -0
- package/dist/composables/useBbFormField.js +5 -3
- package/dist/composables/useBbTabsContext.d.ts +10 -10
- package/dist/composables/useBbTabsContext.js +18 -18
- package/dist/composables/useFormBinding.d.ts +11 -0
- package/dist/composables/useFormBinding.js +15 -13
- package/dist/composables/useItemText.d.ts +5 -1
- package/dist/composables/useItemText.js +8 -3
- package/dist/composables/useItemValue.d.ts +5 -1
- package/dist/composables/useItemValue.js +8 -3
- package/dist/composables/useLocale.js +10 -9
- package/dist/deprecation/ai-deprecations.json.d.ts +130 -0
- package/dist/deprecation/ai-deprecations.json.js +1 -1
- package/dist/deprecation/removedProps.js +1 -1
- package/dist/directives/bbDropdown.js +1 -1
- package/dist/i18n/index.d.ts +7 -12
- package/dist/i18n/locales/cs.json +11 -14
- package/dist/i18n/locales/da.json +13 -16
- package/dist/i18n/locales/de-CH.json +137 -0
- package/dist/i18n/locales/de.json +14 -17
- package/dist/i18n/locales/el.json +10 -13
- package/dist/i18n/locales/en-GB.json +137 -0
- package/dist/i18n/locales/en.json +8 -11
- package/dist/i18n/locales/es-MX.json +137 -0
- package/dist/i18n/locales/es.json +16 -19
- package/dist/i18n/locales/fi.json +8 -11
- package/dist/i18n/locales/fr.json +23 -26
- package/dist/i18n/locales/hu.json +8 -11
- package/dist/i18n/locales/it.json +12 -15
- package/dist/i18n/locales/ja.json +13 -16
- package/dist/i18n/locales/ko.json +10 -13
- package/dist/i18n/locales/nb.json +13 -16
- package/dist/i18n/locales/nl.json +21 -24
- package/dist/i18n/locales/pl.json +10 -13
- package/dist/i18n/locales/{pt.json → pt-BR.json} +19 -22
- package/dist/i18n/locales/pt-PT.json +137 -0
- package/dist/i18n/locales/ro.json +22 -25
- package/dist/i18n/locales/ru.json +11 -14
- package/dist/i18n/locales/sv.json +16 -19
- package/dist/i18n/locales/tr.json +14 -17
- package/dist/i18n/locales/uk.json +11 -14
- package/dist/i18n/locales/{zh-cn.json → zh-CN.json} +10 -13
- package/dist/i18n/locales/zh-HK.json +137 -0
- package/dist/i18n/locales/zh-TW.json +137 -0
- package/dist/i18n/regionHints.d.ts +16 -0
- package/dist/i18n/regionHints.js +49 -0
- package/dist/i18n/regions.d.ts +286 -0
- package/dist/i18n/regions.js +265 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -4
- package/dist/llms-full.txt +1785 -1637
- package/dist/llms-medium.txt +74 -46
- package/dist/llms.txt +7 -7
- package/dist/locale-blueprint.json +16 -13
- package/dist/styles.css +2 -2
- package/dist/types/ptComponentMap.d.ts +1 -1
- package/dist/utilities/functions/slotKey.d.ts +2 -2
- package/dist/utils/versionCheck.js +1 -1
- package/dist/vite-plugin.d.ts +22 -19
- package/dist/vite.js +309 -297
- package/llms.txt +7 -7
- package/package.json +5 -3
- package/scripts/lib/agent-manifest.mjs +2 -1
- package/scripts/lib/eslint-disable.mjs +12 -4
- package/scripts/lib/eslint-plugin.d.ts +5 -1
- package/scripts/lib/eslint-plugin.mjs +187 -6
- package/scripts/lib/helper-canon.mjs +176 -0
- package/scripts/lib/helpers-add.mjs +1303 -0
- package/scripts/lib/local-components.mjs +33 -4
- package/scripts/lib/locale-check.mjs +165 -0
- package/scripts/lib/redundant-item-accessor.mjs +398 -0
- package/scripts/lib/renamed-item-field.mjs +343 -0
- package/scripts/lib/slot-key.mjs +2 -2
- package/scripts/lib/slot-renames.mjs +1 -1
- package/scripts/lib/validate-bb-markup.mjs +123 -56
- package/dist/i18n/index.js +0 -27
|
@@ -14,23 +14,52 @@
|
|
|
14
14
|
* `unknown-component` error (a typo, an invented or removed name).
|
|
15
15
|
*/
|
|
16
16
|
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
17
|
-
import { basename, join, relative, sep } from 'node:path';
|
|
17
|
+
import { basename, dirname, join, relative, resolve, sep } from 'node:path';
|
|
18
18
|
|
|
19
19
|
const BB_NAME = /^Bb[A-Z0-9]/;
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Whether an import source is the library itself. `bitboss-ui` always is.
|
|
23
|
+
* Inside the library's own repo the showcases import the SOURCE
|
|
24
|
+
* (`@/components/BbSelect/BbSelect.vue`, `../BbSelect.vue`): the rules'
|
|
25
|
+
* `libraryImports` option lists those prefixes (empty by default, so a
|
|
26
|
+
* consumer's own `Bb*` imports stay theirs). A relative source is resolved
|
|
27
|
+
* against the importing file and matched as a path from `cwd`, so
|
|
28
|
+
* `src/components/` covers `../BbSelect.vue` from a showcase.
|
|
29
|
+
* @param {string} source
|
|
30
|
+
* @param {{ libraryImports?: string[], filename?: string, cwd?: string }} [library]
|
|
31
|
+
*/
|
|
32
|
+
export function isLibrarySource(source, library) {
|
|
33
|
+
if (source === 'bitboss-ui' || source.startsWith('bitboss-ui/')) return true;
|
|
34
|
+
const prefixes = library?.libraryImports ?? [];
|
|
35
|
+
if (!prefixes.length) return false;
|
|
36
|
+
let candidate = source;
|
|
37
|
+
if (source.startsWith('.') && library?.filename) {
|
|
38
|
+
candidate = relative(
|
|
39
|
+
library.cwd ?? process.cwd(),
|
|
40
|
+
resolve(dirname(library.filename), source)
|
|
41
|
+
)
|
|
42
|
+
.split(sep)
|
|
43
|
+
.join('/');
|
|
44
|
+
}
|
|
45
|
+
return prefixes.some((prefix) => candidate.startsWith(prefix));
|
|
46
|
+
}
|
|
47
|
+
|
|
21
48
|
/**
|
|
22
49
|
* `Bb*` names a file imports from anywhere but `bitboss-ui` (its subpaths
|
|
23
|
-
* included
|
|
50
|
+
* included, and the `libraryImports` prefixes, see `isLibrarySource`) —
|
|
51
|
+
* default and named imports alike. Type-only imports are skipped.
|
|
24
52
|
* @param {string} code
|
|
53
|
+
* @param {{ libraryImports?: string[], filename?: string, cwd?: string }} [library]
|
|
25
54
|
* @returns {Map<string, number>} name → 1-based line of the import
|
|
26
55
|
*/
|
|
27
|
-
export function localBbImports(code) {
|
|
56
|
+
export function localBbImports(code, library) {
|
|
28
57
|
const found = new Map();
|
|
29
58
|
const IMPORT = /\bimport\s+(type\s+)?([\s\S]*?)\s+from\s*['"]([^'"]+)['"]/g;
|
|
30
59
|
for (const match of code.matchAll(IMPORT)) {
|
|
31
60
|
const [, typeOnly, clause, source] = match;
|
|
32
61
|
if (typeOnly) continue;
|
|
33
|
-
if (source
|
|
62
|
+
if (isLibrarySource(source, library)) continue;
|
|
34
63
|
const line = code.slice(0, match.index).split('\n').length;
|
|
35
64
|
const names = [];
|
|
36
65
|
const defaultName = /^\s*([A-Za-z_$][\w$]*)/.exec(
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `check`: a locale the library does not ship (owner ruling 2026-10-07). The
|
|
3
|
+
* locale list is fixed and typed, so `locale: 'en-UK'` is a type error where
|
|
4
|
+
* TypeScript sees it — but a `vite.config.js`, a Nuxt config or a
|
|
5
|
+
* `setConfig({ locale })` in a plain `<script>` is not type-checked, and the
|
|
6
|
+
* plugin's own warning only reaches whoever reads the dev-server log.
|
|
7
|
+
*
|
|
8
|
+
* `unknown-locale` reads the build plugin's inline `locale` / `enabledLocales`
|
|
9
|
+
* literals and every `setConfig({ locale: '…' })` literal, and names the
|
|
10
|
+
* nearest shipped locale (`en-UK` → `en-GB`, `de-LU` → `de`). The shipped list
|
|
11
|
+
* is the manifest's `BuiltinLocaleKey` values; the aliases the runtime takes
|
|
12
|
+
* (`zh-cn`, `pt`, `it-IT`) are derived the way the runtime defines them: any
|
|
13
|
+
* case, and the full code of a language's default region (CLDR likely
|
|
14
|
+
* subtags, `Intl.Locale.prototype.maximize`). Regexes over comment-blanked
|
|
15
|
+
* source, the same trade the rest of `check` makes.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { readFileSync } from 'node:fs';
|
|
19
|
+
import { readPluginOptions } from './hand-imported-styles.mjs';
|
|
20
|
+
|
|
21
|
+
export const UNKNOWN_LOCALE_KIND = 'unknown-locale';
|
|
22
|
+
|
|
23
|
+
/** The canonical keys, from the manifest. */
|
|
24
|
+
const shippedKeys = (manifest) => {
|
|
25
|
+
const types = manifest?.types;
|
|
26
|
+
const list =
|
|
27
|
+
types instanceof Map ? [...types.values()] : Object.values(types ?? {});
|
|
28
|
+
return list.find((type) => type?.name === 'BuiltinLocaleKey')?.values ?? [];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** `language-REGION` of a tag's likely subtags, or `undefined`. */
|
|
32
|
+
const likely = (tag) => {
|
|
33
|
+
try {
|
|
34
|
+
const { language, region } = new Intl.Locale(tag).maximize();
|
|
35
|
+
return region ? `${language}-${region}` : language;
|
|
36
|
+
} catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resolves a locale as written to its canonical key, or `undefined`.
|
|
43
|
+
* @param {string} input
|
|
44
|
+
* @param {string[]} keys the canonical keys
|
|
45
|
+
*/
|
|
46
|
+
export function resolveLocale(input, keys) {
|
|
47
|
+
const lower = input.trim().replace(/_/g, '-').toLowerCase();
|
|
48
|
+
const byLower = new Map(keys.map((key) => [key.toLowerCase(), key]));
|
|
49
|
+
if (byLower.has(lower)) return byLower.get(lower);
|
|
50
|
+
const [language] = lower.split('-');
|
|
51
|
+
const full = likely(lower)?.toLowerCase();
|
|
52
|
+
// A bare language whose default region is a key (`pt` → `pt-BR`).
|
|
53
|
+
if (!lower.includes('-') && full && byLower.has(full))
|
|
54
|
+
return byLower.get(full);
|
|
55
|
+
// The default region spelled out (`it-IT` → `it`, `en-US` → `en`).
|
|
56
|
+
if (byLower.has(language) && likely(language)?.toLowerCase() === lower)
|
|
57
|
+
return byLower.get(language);
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The nearest shipped locale: same language, `UK` read as `GB`; else the bare language. */
|
|
62
|
+
export function nearestLocale(input, keys) {
|
|
63
|
+
if (resolveLocale(input, keys)) return undefined;
|
|
64
|
+
const [language, region] = input.trim().replace(/_/g, '-').split('-');
|
|
65
|
+
if (!language) return undefined;
|
|
66
|
+
const fixed = region
|
|
67
|
+
? ({ UK: 'GB' }[region.toUpperCase()] ?? region.toUpperCase())
|
|
68
|
+
: undefined;
|
|
69
|
+
return (
|
|
70
|
+
(fixed && resolveLocale(`${language}-${fixed}`, keys)) ||
|
|
71
|
+
resolveLocale(language, keys)
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const lineAt = (code, index) => code.slice(0, index).split('\n').length;
|
|
76
|
+
const blank = (text) => text.replace(/[^\n]/g, ' ');
|
|
77
|
+
const blankComments = (code) =>
|
|
78
|
+
code
|
|
79
|
+
.replace(/\/\*[\s\S]*?\*\//g, blank)
|
|
80
|
+
.replace(/(^|[^:\\])(\/\/[^\n]*)/g, (_, before, c) => before + blank(c))
|
|
81
|
+
.replace(/<!--[\s\S]*?-->/g, blank);
|
|
82
|
+
|
|
83
|
+
const finding = (file, code, index, value, keys, where) => {
|
|
84
|
+
const near = nearestLocale(value, keys);
|
|
85
|
+
return {
|
|
86
|
+
file,
|
|
87
|
+
line: lineAt(code, index),
|
|
88
|
+
kind: UNKNOWN_LOCALE_KIND,
|
|
89
|
+
component: 'bitboss-ui',
|
|
90
|
+
attr: 'locale',
|
|
91
|
+
message: `${where} \`'${value}'\` is not a locale bitboss-ui ships${near ? ` — did you mean '${near}'?` : ''} Strings fall back to the default locale. The list: \`BuiltinLocaleKey\` (guides/installation-and-plugin-setup.md § Locales).`,
|
|
92
|
+
...(near ? { suggestion: near } : {}),
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/** Every `locale: '…'` / `enabledLocales: [ … ]` literal in `text`, offset by `base`. */
|
|
97
|
+
function optionLiterals(text, base) {
|
|
98
|
+
const out = [];
|
|
99
|
+
for (const match of text.matchAll(/\blocale\s*:\s*(['"])([^'"\n]+)\1/g))
|
|
100
|
+
out.push({
|
|
101
|
+
value: match[2],
|
|
102
|
+
index: base + match.index,
|
|
103
|
+
where: '`locale`',
|
|
104
|
+
});
|
|
105
|
+
for (const match of text.matchAll(/\benabledLocales\s*:\s*\[([^\]]*)\]/g)) {
|
|
106
|
+
const start = base + match.index + match[0].indexOf('[');
|
|
107
|
+
for (const entry of match[1].matchAll(/(['"])([^'"\n]+)\1/g))
|
|
108
|
+
out.push({
|
|
109
|
+
value: entry[2],
|
|
110
|
+
index: start + 1 + entry.index,
|
|
111
|
+
where: '`enabledLocales` entry',
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Locales the library does not ship, in the build config and in
|
|
119
|
+
* `setConfig({ locale })` calls.
|
|
120
|
+
* @param {string} projectRoot
|
|
121
|
+
* @param {{ path: string, relPath: string }[]} files
|
|
122
|
+
* @param {object} manifest
|
|
123
|
+
* @param {(path: string) => string} [read]
|
|
124
|
+
*/
|
|
125
|
+
export function unknownLocaleFindings(
|
|
126
|
+
projectRoot,
|
|
127
|
+
files,
|
|
128
|
+
manifest,
|
|
129
|
+
read = (path) => readFileSync(path, 'utf-8')
|
|
130
|
+
) {
|
|
131
|
+
const keys = shippedKeys(manifest);
|
|
132
|
+
if (!keys.length) return [];
|
|
133
|
+
const findings = [];
|
|
134
|
+
const plugin = readPluginOptions(projectRoot);
|
|
135
|
+
if (plugin?.options) {
|
|
136
|
+
const base = plugin.code.indexOf(plugin.options);
|
|
137
|
+
for (const { value, index, where } of optionLiterals(plugin.options, base))
|
|
138
|
+
if (!resolveLocale(value, keys))
|
|
139
|
+
findings.push(
|
|
140
|
+
finding(plugin.config, plugin.code, index, value, keys, where)
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
for (const { path, relPath } of files) {
|
|
144
|
+
if (!/\.(?:vue|[cm]?[jt]sx?)$/.test(relPath) || relPath === plugin?.config)
|
|
145
|
+
continue;
|
|
146
|
+
let code;
|
|
147
|
+
try {
|
|
148
|
+
code = blankComments(read(path));
|
|
149
|
+
} catch {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (!code.includes('setConfig')) continue;
|
|
153
|
+
const CALL =
|
|
154
|
+
/\bsetConfig\s*\(\s*\{[^}]*?\blocale\s*:\s*(['"])([^'"\n]+)\1/g;
|
|
155
|
+
for (const match of code.matchAll(CALL)) {
|
|
156
|
+
const value = match[2];
|
|
157
|
+
if (resolveLocale(value, keys)) continue;
|
|
158
|
+
const index = match.index + match[0].lastIndexOf(value) - 1;
|
|
159
|
+
findings.push(
|
|
160
|
+
finding(relPath, code, index, value, keys, '`setConfig({ locale })`')
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return findings;
|
|
165
|
+
}
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `redundant-item-accessor` (Q60.24, owner 2026-10-07): with no `item-text`
|
|
3
|
+
* an option reads its own `label`, else its own `text`; with no `item-value`
|
|
4
|
+
* its own `value`. An accessor that names exactly that field restates the
|
|
5
|
+
* default and is cruft: `item-text="label"`, `item-text="text"`,
|
|
6
|
+
* `item-value="value"` (static or `:item-text="'label'"`, kebab or camel) on
|
|
7
|
+
* the option surfaces, and `itemText: 'label'` / `'text'` /
|
|
8
|
+
* `itemValue: 'value'` in a BbDropdown group.
|
|
9
|
+
*
|
|
10
|
+
* Reported only when removing it cannot change what renders:
|
|
11
|
+
* - `item-text="label"` / `item-value="value"`: when the items are not
|
|
12
|
+
* visible in the file (the field is assumed present), or when every visible
|
|
13
|
+
* item is an object literal spelling that field. A primitive item or an
|
|
14
|
+
* object without the field reads differently through the default.
|
|
15
|
+
* - `item-text="text"`: only when every visible item spells `text` and none
|
|
16
|
+
* spells `label` or spreads (`label` would win over `text`); never on items
|
|
17
|
+
* the file cannot see.
|
|
18
|
+
* - A dropdown group stays a mapped group after the removal: its `items` is a
|
|
19
|
+
* function, or it keeps `itemProps`, `max` or another mapper. A group whose
|
|
20
|
+
* only mapper is the redundant one would turn into a hand-written group.
|
|
21
|
+
*
|
|
22
|
+
* The ESLint twin is `bitboss-ui/no-redundant-item-accessor`
|
|
23
|
+
* (eslint-plugin.mjs): both read the findings below; each carries the range
|
|
24
|
+
* the autofix deletes.
|
|
25
|
+
*/
|
|
26
|
+
import { babelParse, parse } from 'vue/compiler-sfc';
|
|
27
|
+
import { libraryAliases, localBbImports } from './local-components.mjs';
|
|
28
|
+
|
|
29
|
+
const NODE_ELEMENT = 1;
|
|
30
|
+
const PROP_ATTRIBUTE = 6;
|
|
31
|
+
const PROP_DIRECTIVE = 7;
|
|
32
|
+
|
|
33
|
+
/** The components whose options read the default chain. */
|
|
34
|
+
export const ACCESSOR_SURFACES = new Set([
|
|
35
|
+
'BbSelect',
|
|
36
|
+
'BbSelectPopover',
|
|
37
|
+
'BbRadioGroup',
|
|
38
|
+
'BbCheckboxGroup',
|
|
39
|
+
'BbSwitchGroup',
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
/** Hosts whose `items` hold BbDropdown groups. */
|
|
43
|
+
const DROPDOWN_HOSTS = new Set(['BbDropdown', 'BbDropdownButton']);
|
|
44
|
+
|
|
45
|
+
/** accessor → the field values that restate its default. */
|
|
46
|
+
const DEFAULT_FIELDS = { itemText: ['label', 'text'], itemValue: ['value'] };
|
|
47
|
+
|
|
48
|
+
export const REDUNDANT_ACCESSOR_MESSAGES = {
|
|
49
|
+
attr: (attr, field) =>
|
|
50
|
+
`\`${attr}\` restates the default: ${
|
|
51
|
+
field === 'value'
|
|
52
|
+
? "an option's value is its own `value`"
|
|
53
|
+
: 'an option reads its own `label`, else its own `text`'
|
|
54
|
+
}. Remove it.`,
|
|
55
|
+
group: (prop, field) =>
|
|
56
|
+
`\`${prop}: '${field}'\` restates the group's default: ${
|
|
57
|
+
field === 'value'
|
|
58
|
+
? "a row's value is its own `value`"
|
|
59
|
+
: 'a row reads its own `label`, else its own `text`'
|
|
60
|
+
}. Remove it.`,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const REDUNDANT_ACCESSOR_HINT =
|
|
64
|
+
'`{ label, value }` and `{ text, value }` items work as they are; an accessor is only for another shape';
|
|
65
|
+
|
|
66
|
+
const pascal = (tag) =>
|
|
67
|
+
tag.includes('-')
|
|
68
|
+
? tag.replace(/(?:^|-)([a-z0-9])/g, (_, c) => c.toUpperCase())
|
|
69
|
+
: tag;
|
|
70
|
+
|
|
71
|
+
const camel = (name) => name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
72
|
+
|
|
73
|
+
const unwrap = (node) => {
|
|
74
|
+
let current = node;
|
|
75
|
+
while (
|
|
76
|
+
current &&
|
|
77
|
+
[
|
|
78
|
+
'TSAsExpression',
|
|
79
|
+
'TSSatisfiesExpression',
|
|
80
|
+
'TSNonNullExpression',
|
|
81
|
+
'TSTypeAssertion',
|
|
82
|
+
'ParenthesizedExpression',
|
|
83
|
+
].includes(current.type)
|
|
84
|
+
)
|
|
85
|
+
current = current.expression;
|
|
86
|
+
return current;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const keyName = (property) =>
|
|
90
|
+
(property.type === 'ObjectProperty' || property.type === 'ObjectMethod') &&
|
|
91
|
+
!property.computed
|
|
92
|
+
? property.key.type === 'Identifier'
|
|
93
|
+
? property.key.name
|
|
94
|
+
: property.key.type === 'StringLiteral'
|
|
95
|
+
? property.key.value
|
|
96
|
+
: undefined
|
|
97
|
+
: undefined;
|
|
98
|
+
|
|
99
|
+
const propertyOf = (object, key) =>
|
|
100
|
+
object.properties.find((p) => keyName(p) === key);
|
|
101
|
+
|
|
102
|
+
/** A string literal (`'label'`, `"label"`, `` `label` ``) → its value. */
|
|
103
|
+
const stringValue = (node) => {
|
|
104
|
+
const value = unwrap(node);
|
|
105
|
+
if (value?.type === 'StringLiteral') return value.value;
|
|
106
|
+
if (
|
|
107
|
+
value?.type === 'TemplateLiteral' &&
|
|
108
|
+
value.expressions.length === 0 &&
|
|
109
|
+
value.quasis.length === 1
|
|
110
|
+
)
|
|
111
|
+
return value.quasis[0].value.cooked;
|
|
112
|
+
return undefined;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The visible items of an array: `null` when the array cannot be judged
|
|
117
|
+
* (not in the file, or mutated), else one entry per element —
|
|
118
|
+
* `{ keys, spread }` for an object literal, `null` for anything else.
|
|
119
|
+
*/
|
|
120
|
+
const shapeOf = (arrayNode) =>
|
|
121
|
+
arrayNode
|
|
122
|
+
? arrayNode.elements.map((raw) => {
|
|
123
|
+
const element = unwrap(raw);
|
|
124
|
+
if (element?.type !== 'ObjectExpression') return null;
|
|
125
|
+
return {
|
|
126
|
+
keys: new Set(element.properties.map(keyName).filter(Boolean)),
|
|
127
|
+
spread: element.properties.some((p) => p.type === 'SpreadElement'),
|
|
128
|
+
};
|
|
129
|
+
})
|
|
130
|
+
: null;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Whether `accessor = field` restates the default for items of this shape.
|
|
134
|
+
* @param {'itemText' | 'itemValue'} accessor
|
|
135
|
+
* @param {string} field
|
|
136
|
+
* @param {null | Array<null | { keys: Set<string>, spread: boolean }>} shape
|
|
137
|
+
*/
|
|
138
|
+
export function restatesDefault(accessor, field, shape) {
|
|
139
|
+
if (!DEFAULT_FIELDS[accessor]?.includes(field)) return false;
|
|
140
|
+
if (accessor === 'itemText' && field === 'text') {
|
|
141
|
+
if (!shape) return false;
|
|
142
|
+
return shape.every(
|
|
143
|
+
(item) =>
|
|
144
|
+
item && !item.spread && !item.keys.has('label') && item.keys.has('text')
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
if (!shape) return true;
|
|
148
|
+
return shape.every((item) => item && item.keys.has(field));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Resolves an `items` expression to an ArrayExpression, or null. */
|
|
152
|
+
const arrayOf = (node, arrays) => {
|
|
153
|
+
const value = unwrap(node);
|
|
154
|
+
if (value?.type === 'ArrayExpression') return value;
|
|
155
|
+
if (value?.type === 'Identifier' && !arrays?.mutated?.has(value.name))
|
|
156
|
+
return arrays?.get(value.name) ?? null;
|
|
157
|
+
return null;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/** The range to delete with an attribute: it and the whitespace before it. */
|
|
161
|
+
const attrRemoval = (code, start, end) => {
|
|
162
|
+
let from = start;
|
|
163
|
+
while (from > 0 && /\s/.test(code[from - 1])) from--;
|
|
164
|
+
return [from, end];
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/** The range to delete with an object property: it and one comma. */
|
|
168
|
+
const propertyRemoval = (code, start, end) => {
|
|
169
|
+
let to = end;
|
|
170
|
+
while (to < code.length && /[ \t]/.test(code[to])) to++;
|
|
171
|
+
if (code[to] === ',') {
|
|
172
|
+
to++;
|
|
173
|
+
// Alone on its line: take the whole line.
|
|
174
|
+
let lineStart = start;
|
|
175
|
+
while (lineStart > 0 && /[ \t]/.test(code[lineStart - 1])) lineStart--;
|
|
176
|
+
let after = to;
|
|
177
|
+
while (after < code.length && /[ \t]/.test(code[after])) after++;
|
|
178
|
+
if (
|
|
179
|
+
(lineStart === 0 || code[lineStart - 1] === '\n') &&
|
|
180
|
+
code[after] === '\n'
|
|
181
|
+
)
|
|
182
|
+
return [lineStart, after + 1];
|
|
183
|
+
while (to < code.length && /[ \t]/.test(code[to])) to++;
|
|
184
|
+
return [start, to];
|
|
185
|
+
}
|
|
186
|
+
// The last property: take the comma before it.
|
|
187
|
+
let from = start;
|
|
188
|
+
while (from > 0 && /\s/.test(code[from - 1])) from--;
|
|
189
|
+
if (code[from - 1] === ',') from--;
|
|
190
|
+
return [from, end];
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const parseExpression = (content) => {
|
|
194
|
+
try {
|
|
195
|
+
const program = babelParse(`(${content})`, {
|
|
196
|
+
sourceType: 'module',
|
|
197
|
+
plugins: ['typescript'],
|
|
198
|
+
}).program;
|
|
199
|
+
return program.body[0]?.expression ?? null;
|
|
200
|
+
} catch {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* @param {string} code a whole SFC (ranges are offsets into it) or a snippet
|
|
207
|
+
* @param {{ templateAst?: any, arrays?: Map<string, any> & { mutated?: Set<string> }, library?: Parameters<typeof localBbImports>[1] }} [options]
|
|
208
|
+
* `arrays`: the file's script-setup array consts (`scriptSetupArrayConsts`)
|
|
209
|
+
* `library`: what counts as the library besides `bitboss-ui` (`isLibrarySource`)
|
|
210
|
+
* @returns {Array<{ component: string, attr: string, kind: 'redundant-item-accessor', line: number, message: string, hint: string, severity: 'warning', range?: [number, number], edit?: { range: [number, number], text: string } }>}
|
|
211
|
+
* `edit` is the autofix as a text replacement (a deletion is `text: ''`), so a
|
|
212
|
+
* later finding can rewrite instead of delete without touching the rule
|
|
213
|
+
*/
|
|
214
|
+
export function redundantItemAccessorFindings(code, options = {}) {
|
|
215
|
+
if (!/item-?(text|value)|itemText|itemValue/i.test(code)) return [];
|
|
216
|
+
let descriptor = null;
|
|
217
|
+
try {
|
|
218
|
+
({ descriptor } = parse(code, { sourceMap: false }));
|
|
219
|
+
} catch {
|
|
220
|
+
descriptor = null;
|
|
221
|
+
}
|
|
222
|
+
const ast = options.templateAst ?? descriptor?.template?.ast;
|
|
223
|
+
if (!ast) return [];
|
|
224
|
+
// Offsets are only trusted on the file's own template AST.
|
|
225
|
+
const ranged =
|
|
226
|
+
!options.templateAst || options.templateAst === descriptor?.template?.ast;
|
|
227
|
+
const arrays = options.arrays;
|
|
228
|
+
const aliases = libraryAliases(code);
|
|
229
|
+
const own = localBbImports(code, options.library);
|
|
230
|
+
const scriptOffset = descriptor?.scriptSetup?.loc.start.offset ?? 0;
|
|
231
|
+
const scriptLine = descriptor?.scriptSetup?.loc.start.line ?? 1;
|
|
232
|
+
const findings = [];
|
|
233
|
+
|
|
234
|
+
const itemsShape = (element) => {
|
|
235
|
+
for (const prop of element.props ?? []) {
|
|
236
|
+
if (prop.type !== PROP_DIRECTIVE || prop.name !== 'bind') continue;
|
|
237
|
+
if (!prop.arg?.isStatic || prop.arg.content !== 'items') continue;
|
|
238
|
+
const expression = parseExpression(prop.exp?.content ?? '');
|
|
239
|
+
return shapeOf(arrayOf(expression, arrays));
|
|
240
|
+
}
|
|
241
|
+
return null;
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const checkSurface = (element, component) => {
|
|
245
|
+
let shape;
|
|
246
|
+
for (const prop of element.props ?? []) {
|
|
247
|
+
let name;
|
|
248
|
+
let field;
|
|
249
|
+
if (prop.type === PROP_ATTRIBUTE) {
|
|
250
|
+
name = camel(prop.name);
|
|
251
|
+
field = prop.value?.content;
|
|
252
|
+
} else if (
|
|
253
|
+
prop.type === PROP_DIRECTIVE &&
|
|
254
|
+
prop.name === 'bind' &&
|
|
255
|
+
prop.arg?.isStatic
|
|
256
|
+
) {
|
|
257
|
+
name = camel(prop.arg.content);
|
|
258
|
+
if (!(name in DEFAULT_FIELDS)) continue;
|
|
259
|
+
field = stringValue(parseExpression(prop.exp?.content ?? ''));
|
|
260
|
+
}
|
|
261
|
+
if (!(name in DEFAULT_FIELDS) || field === undefined) continue;
|
|
262
|
+
shape ??= itemsShape(element);
|
|
263
|
+
if (!restatesDefault(name, field, shape)) continue;
|
|
264
|
+
const attr = code.slice(prop.loc.start.offset, prop.loc.end.offset);
|
|
265
|
+
findings.push({
|
|
266
|
+
component,
|
|
267
|
+
attr: prop.type === PROP_ATTRIBUTE ? prop.name : `:${prop.arg.content}`,
|
|
268
|
+
kind: 'redundant-item-accessor',
|
|
269
|
+
line: prop.loc.start.line,
|
|
270
|
+
message: REDUNDANT_ACCESSOR_MESSAGES.attr(
|
|
271
|
+
ranged ? attr : `${prop.name}="${field}"`,
|
|
272
|
+
field
|
|
273
|
+
),
|
|
274
|
+
hint: REDUNDANT_ACCESSOR_HINT,
|
|
275
|
+
severity: 'warning',
|
|
276
|
+
...(ranged
|
|
277
|
+
? {
|
|
278
|
+
range: [prop.loc.start.offset, prop.loc.end.offset],
|
|
279
|
+
edit: {
|
|
280
|
+
range: attrRemoval(
|
|
281
|
+
code,
|
|
282
|
+
prop.loc.start.offset,
|
|
283
|
+
prop.loc.end.offset
|
|
284
|
+
),
|
|
285
|
+
text: '',
|
|
286
|
+
},
|
|
287
|
+
}
|
|
288
|
+
: {}),
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
/** Groups inside a host's `items` array (script-side or inline). */
|
|
294
|
+
const checkGroups = (array, component, base) => {
|
|
295
|
+
for (const raw of array.elements) {
|
|
296
|
+
const entry = unwrap(raw);
|
|
297
|
+
if (entry?.type !== 'ObjectExpression') continue;
|
|
298
|
+
const itemsProp = propertyOf(entry, 'items');
|
|
299
|
+
const itemsValue =
|
|
300
|
+
itemsProp?.type === 'ObjectMethod'
|
|
301
|
+
? itemsProp
|
|
302
|
+
: unwrap(itemsProp?.value);
|
|
303
|
+
const isFunction =
|
|
304
|
+
itemsValue?.type === 'ObjectMethod' ||
|
|
305
|
+
itemsValue?.type === 'ArrowFunctionExpression' ||
|
|
306
|
+
itemsValue?.type === 'FunctionExpression';
|
|
307
|
+
const childArray = isFunction ? null : arrayOf(itemsValue, arrays);
|
|
308
|
+
const shape = isFunction ? null : shapeOf(childArray);
|
|
309
|
+
const mappers = ['itemText', 'itemValue']
|
|
310
|
+
.map((name) => ({ name, property: propertyOf(entry, name) }))
|
|
311
|
+
.filter((m) => m.property);
|
|
312
|
+
const redundant = mappers.filter(({ name, property }) => {
|
|
313
|
+
if (property.type !== 'ObjectProperty') return false;
|
|
314
|
+
const field = stringValue(property.value);
|
|
315
|
+
return field !== undefined && restatesDefault(name, field, shape);
|
|
316
|
+
});
|
|
317
|
+
const staysMapped =
|
|
318
|
+
isFunction ||
|
|
319
|
+
!!propertyOf(entry, 'itemProps') ||
|
|
320
|
+
!!propertyOf(entry, 'max') ||
|
|
321
|
+
mappers.length > redundant.length;
|
|
322
|
+
// An `items` identifier the file cannot see may be a function, or not.
|
|
323
|
+
if (redundant.length && staysMapped) {
|
|
324
|
+
for (const { name, property } of redundant) {
|
|
325
|
+
const field = stringValue(property.value);
|
|
326
|
+
findings.push({
|
|
327
|
+
component,
|
|
328
|
+
attr: name,
|
|
329
|
+
kind: 'redundant-item-accessor',
|
|
330
|
+
line: base.line + property.loc.start.line - 1,
|
|
331
|
+
message: REDUNDANT_ACCESSOR_MESSAGES.group(name, field),
|
|
332
|
+
hint: REDUNDANT_ACCESSOR_HINT,
|
|
333
|
+
severity: 'warning',
|
|
334
|
+
...(base.offset !== null
|
|
335
|
+
? {
|
|
336
|
+
range: [
|
|
337
|
+
base.offset + property.start,
|
|
338
|
+
base.offset + property.end,
|
|
339
|
+
],
|
|
340
|
+
edit: {
|
|
341
|
+
range: propertyRemoval(
|
|
342
|
+
code,
|
|
343
|
+
base.offset + property.start,
|
|
344
|
+
base.offset + property.end
|
|
345
|
+
),
|
|
346
|
+
text: '',
|
|
347
|
+
},
|
|
348
|
+
}
|
|
349
|
+
: {}),
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (childArray) checkGroups(childArray, component, base);
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const seenArrays = new Set();
|
|
358
|
+
const checkHost = (element, component) => {
|
|
359
|
+
for (const prop of element.props ?? []) {
|
|
360
|
+
if (prop.type !== PROP_DIRECTIVE || prop.name !== 'bind') continue;
|
|
361
|
+
if (!prop.arg?.isStatic || prop.arg.content !== 'items') continue;
|
|
362
|
+
const content = prop.exp?.content ?? '';
|
|
363
|
+
const expression = parseExpression(content);
|
|
364
|
+
const value = unwrap(expression);
|
|
365
|
+
if (value?.type === 'Identifier') {
|
|
366
|
+
const array = arrayOf(value, arrays);
|
|
367
|
+
// A script const bound by two hosts is reported once.
|
|
368
|
+
if (!array || seenArrays.has(array)) continue;
|
|
369
|
+
seenArrays.add(array);
|
|
370
|
+
checkGroups(array, component, {
|
|
371
|
+
offset: ranged && descriptor?.scriptSetup ? scriptOffset : null,
|
|
372
|
+
line: scriptLine,
|
|
373
|
+
});
|
|
374
|
+
} else if (value?.type === 'ArrayExpression') {
|
|
375
|
+
// `(` was prepended to the expression before parsing.
|
|
376
|
+
const start = prop.exp.loc.start.offset - 1;
|
|
377
|
+
checkGroups(value, component, {
|
|
378
|
+
offset: ranged ? start : null,
|
|
379
|
+
line: prop.exp.loc.start.line,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
const walk = (node) => {
|
|
386
|
+
if (!node || typeof node !== 'object') return;
|
|
387
|
+
if (node.type === NODE_ELEMENT && !own.has(node.tag)) {
|
|
388
|
+
const written = pascal(node.tag);
|
|
389
|
+
const component =
|
|
390
|
+
aliases.get(node.tag) ?? aliases.get(written) ?? written;
|
|
391
|
+
if (ACCESSOR_SURFACES.has(component)) checkSurface(node, component);
|
|
392
|
+
if (DROPDOWN_HOSTS.has(component)) checkHost(node, component);
|
|
393
|
+
}
|
|
394
|
+
for (const child of node.children ?? []) walk(child);
|
|
395
|
+
};
|
|
396
|
+
walk(ast);
|
|
397
|
+
return findings;
|
|
398
|
+
}
|