bitboss-ui 3.0.0-beta.47 → 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 +167 -14
- package/dist/ai/BbAccordion.md +1 -1
- package/dist/ai/BbAvatar.md +1 -1
- package/dist/ai/BbBreadcrumbs.md +65 -65
- package/dist/ai/BbButton.md +28 -8
- package/dist/ai/BbCheckbox.md +0 -2
- package/dist/ai/BbCheckboxGroup.md +13 -20
- package/dist/ai/BbCollapsible.md +1 -1
- package/dist/ai/BbColorPalette.md +1 -1
- package/dist/ai/BbConfirm.md +8 -0
- package/dist/ai/BbConfirmPortal.md +1 -1
- 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 +19 -7
- package/dist/ai/BbHoverCardPortal.md +1 -1
- package/dist/ai/BbNumberInput.md +88 -21
- package/dist/ai/BbPopover.md +47 -40
- package/dist/ai/BbPopoverPortal.md +1 -1
- 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 +107 -106
- package/dist/ai/BbTabsList.md +2 -2
- package/dist/ai/BbTabsPanels.md +1 -1
- package/dist/ai/BbTabsRoot.md +3 -3
- package/dist/ai/BbTooltip.md +29 -13
- package/dist/ai/BbTree.md +14 -11
- package/dist/ai/CommonPopover.md +1 -1
- package/dist/ai/changelog.json +297 -6
- package/dist/ai/components.json +50 -45
- 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 +45 -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 +143 -28
- package/dist/ai/guides/migration/v3-beta-css-classes.md +29 -27
- 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 +4 -4
- 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/BbAccordion.json +1 -1
- 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 +2 -1
- package/dist/ai/manifest/components/BbButton.tree.json +1 -1
- package/dist/ai/manifest/components/BbCheckboxGroup.json +2 -2
- package/dist/ai/manifest/components/BbCollapsible.json +1 -1
- package/dist/ai/manifest/components/BbColorPalette.json +1 -1
- package/dist/ai/manifest/components/BbConfirmPortal.json +5 -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/BbHoverCard.json +1 -1
- package/dist/ai/manifest/components/BbHoverCardPortal.json +2 -2
- 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 +2 -1
- package/dist/ai/manifest/components/BbPopoverPortal.json +2 -2
- 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/BbSelect.tree.json +2 -2
- package/dist/ai/manifest/components/BbSelectPopover.json +2 -2
- package/dist/ai/manifest/components/BbSelectPopover.tree.json +1 -1
- package/dist/ai/manifest/components/BbSwitchGroup.json +2 -2
- package/dist/ai/manifest/components/BbTabs.json +8 -7
- 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 +6 -5
- package/dist/ai/manifest/components/BbTag.tree.json +1 -1
- package/dist/ai/manifest/components/BbTooltip.json +2 -2
- package/dist/ai/manifest/components/ChipsBox.tree.json +1 -1
- package/dist/ai/manifest/components/CommonPopover.json +1 -1
- package/dist/ai/manifest/index.json +2 -2
- package/dist/ai/manifest/meta.json +5 -0
- package/dist/ai/manifest/types.advanced.json +10 -9
- package/dist/ai/manifest/types.api.json +8 -5
- package/dist/ai/manifest/types.wrapper.json +1 -0
- 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 +14 -10
- package/dist/ai/source/BbBadgeButton.md +17 -12
- 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 +84 -38
- 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/BbCollapsible.md +2 -1
- package/dist/ai/source/BbColorInput.md +10 -9
- package/dist/ai/source/BbColorPalette.md +2 -1
- package/dist/ai/source/BbConfirm.md +18 -1
- package/dist/ai/source/BbConfirmPortal.md +20 -6
- 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 +231 -158
- package/dist/ai/source/BbDropdownButton.md +19 -12
- package/dist/ai/source/BbDropdownGroup.md +127 -93
- package/dist/ai/source/BbForm.md +25 -1
- package/dist/ai/source/BbHoverCard.md +30 -3
- package/dist/ai/source/BbHoverCardPortal.md +18 -3
- 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 +185 -12
- package/dist/ai/source/BbPopoverPortal.md +30 -3
- 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 +30 -24
- package/dist/ai/source/BbTabsList.md +29 -23
- package/dist/ai/source/BbTabsPanels.md +29 -23
- package/dist/ai/source/BbTabsRoot.md +28 -22
- 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 +88 -11
- 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 +8 -1
- 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 +88 -70
- package/dist/components/BbButton/toggleRoving.d.ts +6 -0
- package/dist/components/BbButton/toggleRoving.js +45 -0
- package/dist/components/BbButton/types.d.ts +12 -4
- package/dist/components/BbButton/types.js +5 -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/BbCollapsible/types.d.ts +2 -1
- package/dist/components/BbColorInput/BbColorInput.vue_vue_type_script_setup_true_lang.js +69 -69
- package/dist/components/BbColorPalette/types.d.ts +2 -1
- package/dist/components/BbConfirm/BbConfirmPortal.vue.d.ts +2 -5
- package/dist/components/BbConfirm/types.d.ts +15 -0
- 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 +120 -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/BbHoverCard/BbHoverCard.vue_vue_type_script_setup_true_lang.js +145 -142
- package/dist/components/BbHoverCard/BbHoverCardPortal.vue.d.ts +2 -2
- package/dist/components/BbHoverCard/types.d.ts +14 -1
- 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 -162
- package/dist/components/BbPopover/BbPopoverPortal.vue.d.ts +2 -2
- package/dist/components/BbPopover/types.d.ts +26 -1
- 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 +28 -22
- 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 +99 -86
- package/dist/components/BbTooltip/tooltipWarmup.d.ts +23 -0
- package/dist/components/BbTooltip/tooltipWarmup.js +7 -0
- package/dist/components/BbTooltip/types.d.ts +10 -3
- 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/CommonPopover/types.d.ts +2 -1
- 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/directives/directivePortal.d.ts +13 -7
- package/dist/directives/directivePortal.js +1 -4
- 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 +4 -4
- package/dist/index.js +4 -4
- package/dist/llms-full.txt +1914 -1714
- package/dist/llms-medium.txt +76 -46
- package/dist/llms.txt +7 -7
- package/dist/locale-blueprint.json +16 -13
- package/dist/styles.css +2 -2
- package/dist/types/passthrough.d.ts +1 -1
- package/dist/types/ptComponentMap.d.ts +2 -2
- 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 +17 -4
- package/scripts/lib/eslint-plugin.d.ts +9 -1
- package/scripts/lib/eslint-plugin.mjs +256 -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/portal-names.mjs +458 -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/unimported-components.mjs +5 -2
- 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,458 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portal names (Q63.8, owner 2026-10-06): a named portal is a string on one
|
|
3
|
+
* side and a `<…Portal name>` on the other, so a typo, a portal of the wrong
|
|
4
|
+
* family or a forgotten declaration opens nothing, silently. Three findings:
|
|
5
|
+
*
|
|
6
|
+
* - `unknown-portal` (error): a call site names a portal no file declares,
|
|
7
|
+
* with a did-you-mean;
|
|
8
|
+
* - `portal-family-mismatch` (error): the name belongs to the other family
|
|
9
|
+
* (a hover-card portal opened by `v-bb-popover`);
|
|
10
|
+
* - `unused-portal` (warning): a declared portal nothing opens.
|
|
11
|
+
*
|
|
12
|
+
* Call sites: `v-bb-hover-card` / `v-bb-popover` with `"'name'"` or
|
|
13
|
+
* `{ portal: 'name' }`, and `confirm({ portal: 'name' })` in a script.
|
|
14
|
+
* Declarations: `<BbHoverCardPortal | BbPopoverPortal | BbConfirmPortal
|
|
15
|
+
* name="x">`. Everything else is dynamic and says nothing: a `:name` binding
|
|
16
|
+
* leaves its family's call sites unjudged, a non-literal call site (a variable,
|
|
17
|
+
* a template literal, `confirm(options)`) leaves its family's declarations
|
|
18
|
+
* unjudged, and a declared name that appears as ANY string literal in the
|
|
19
|
+
* project counts as opened. A false report costs more than a missed one.
|
|
20
|
+
*
|
|
21
|
+
* The project is scanned once per root (cached, like unimported-components),
|
|
22
|
+
* tests skipped. `check` and the ESLint rules (`bitboss-ui/portal-names`,
|
|
23
|
+
* `bitboss-ui/unused-portal`) share this module.
|
|
24
|
+
*/
|
|
25
|
+
import { readFileSync } from 'node:fs';
|
|
26
|
+
import { parse } from 'vue/compiler-sfc';
|
|
27
|
+
import { walk } from './unimported-components.mjs';
|
|
28
|
+
import { nearestName } from './validate-bb-markup.mjs';
|
|
29
|
+
|
|
30
|
+
/** Each family: its declaration tag, and how its call sites are written. */
|
|
31
|
+
export const PORTAL_FAMILIES = {
|
|
32
|
+
hoverCard: {
|
|
33
|
+
component: 'BbHoverCardPortal',
|
|
34
|
+
directive: 'bb-hover-card',
|
|
35
|
+
opener: 'v-bb-hover-card',
|
|
36
|
+
openWith: (name) => `v-bb-hover-card="'${name}'"`,
|
|
37
|
+
},
|
|
38
|
+
popover: {
|
|
39
|
+
component: 'BbPopoverPortal',
|
|
40
|
+
directive: 'bb-popover',
|
|
41
|
+
opener: 'v-bb-popover',
|
|
42
|
+
openWith: (name) => `v-bb-popover="'${name}'"`,
|
|
43
|
+
},
|
|
44
|
+
confirm: {
|
|
45
|
+
component: 'BbConfirmPortal',
|
|
46
|
+
directive: null,
|
|
47
|
+
opener: 'confirm()',
|
|
48
|
+
openWith: (name) => `confirm({ portal: '${name}' })`,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const FAMILY_BY_COMPONENT = new Map(
|
|
53
|
+
Object.entries(PORTAL_FAMILIES).map(([family, f]) => [f.component, family])
|
|
54
|
+
);
|
|
55
|
+
const FAMILY_BY_DIRECTIVE = new Map(
|
|
56
|
+
Object.entries(PORTAL_FAMILIES)
|
|
57
|
+
.filter(([, f]) => f.directive)
|
|
58
|
+
.map(([family, f]) => [f.directive, family])
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const NODE_ELEMENT = 1;
|
|
62
|
+
const PROP_ATTRIBUTE = 6;
|
|
63
|
+
const PROP_DIRECTIVE = 7;
|
|
64
|
+
|
|
65
|
+
const pascal = (tag) =>
|
|
66
|
+
tag.includes('-')
|
|
67
|
+
? tag.replace(/(?:^|-)([a-z0-9])/g, (_, c) => c.toUpperCase())
|
|
68
|
+
: tag;
|
|
69
|
+
|
|
70
|
+
/** `'name'` / `"name"` as a whole expression, else null. */
|
|
71
|
+
const stringLiteral = (text) =>
|
|
72
|
+
/^\s*(['"])([^'"\\\n]+)\1\s*$/.exec(text ?? '')?.[2] ?? null;
|
|
73
|
+
|
|
74
|
+
const LITERALS = /(['"])([^'"\\\n]{1,120})\1/g;
|
|
75
|
+
|
|
76
|
+
/** Text that may hold a portal site: a quick test before any parse. */
|
|
77
|
+
export const MAY_HAVE_PORTALS =
|
|
78
|
+
/Portal|portal|confirm|bb-popover|bb-hover-card/;
|
|
79
|
+
|
|
80
|
+
/** Every quoted string in a piece of code: a name there may open a portal. */
|
|
81
|
+
const literalsIn = (text, into) => {
|
|
82
|
+
for (const [, , value] of (text ?? '').matchAll(LITERALS)) into.add(value);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* A PascalCase identifier the file imports or declares: a component portal,
|
|
87
|
+
* never a name. Anything else non-literal is dynamic.
|
|
88
|
+
*/
|
|
89
|
+
const isComponentBinding = (identifier, code) =>
|
|
90
|
+
/^[A-Z][\w$]*$/.test(identifier) &&
|
|
91
|
+
(new RegExp(`\\bimport\\b[^;]*\\b${identifier}\\b[^;]*\\bfrom\\b`).test(
|
|
92
|
+
code
|
|
93
|
+
) ||
|
|
94
|
+
new RegExp(`\\b(?:const|let|var|function|class)\\s+${identifier}\\b`).test(
|
|
95
|
+
code
|
|
96
|
+
));
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* What a `portal` value is: `{ name }`, `{ component: true }` or
|
|
100
|
+
* `{ dynamic: true }`.
|
|
101
|
+
*/
|
|
102
|
+
const classifyPortalValue = (text, code) => {
|
|
103
|
+
const name = stringLiteral(text);
|
|
104
|
+
if (name) return { name };
|
|
105
|
+
const trimmed = (text ?? '').trim();
|
|
106
|
+
if (/^[A-Za-z_$][\w$]*$/.test(trimmed) && isComponentBinding(trimmed, code))
|
|
107
|
+
return { component: true };
|
|
108
|
+
return { dynamic: true };
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The `portal` key of an object literal's source: a literal name (with its
|
|
113
|
+
* line offset inside `text`), a component, dynamic, or absent.
|
|
114
|
+
*/
|
|
115
|
+
const portalKey = (text, code) => {
|
|
116
|
+
const keyed = /\bportal\s*:\s*/.exec(text);
|
|
117
|
+
if (keyed) {
|
|
118
|
+
const start = keyed.index + keyed[0].length;
|
|
119
|
+
const rest = text.slice(start);
|
|
120
|
+
const quoted = /^(['"])([^'"\\\n]+)\1/.exec(rest);
|
|
121
|
+
const offset = (text.slice(0, start).match(/\n/g) ?? []).length;
|
|
122
|
+
if (quoted) return { name: quoted[2], offset };
|
|
123
|
+
const ident = /^([A-Za-z_$][\w$]*)\s*(?:[,}\n]|$)/.exec(rest)?.[1];
|
|
124
|
+
if (ident && isComponentBinding(ident, code)) return { component: true };
|
|
125
|
+
return { dynamic: true };
|
|
126
|
+
}
|
|
127
|
+
// `{ portal, portalProps }`: shorthand, a variable.
|
|
128
|
+
if (/[{,]\s*portal\s*[,}]/.test(text)) return { dynamic: true };
|
|
129
|
+
return null;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/** The text of a call's arguments, from the `(` at `open`. */
|
|
133
|
+
const callArguments = (code, open) => {
|
|
134
|
+
let depth = 0;
|
|
135
|
+
let quote = null;
|
|
136
|
+
for (let i = open; i < code.length; i++) {
|
|
137
|
+
const c = code[i];
|
|
138
|
+
if (quote) {
|
|
139
|
+
if (c === '\\') i++;
|
|
140
|
+
else if (c === quote) quote = null;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (c === "'" || c === '"' || c === '`') quote = c;
|
|
144
|
+
else if (c === '(') depth++;
|
|
145
|
+
else if (c === ')' && --depth === 0) return code.slice(open + 1, i);
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const lineAt = (text, index) =>
|
|
151
|
+
(text.slice(0, index).match(/\n/g) ?? []).length;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The script with its comments and template literals blanked (newlines
|
|
155
|
+
* kept, so lines still count): a `confirm({ portal: 'x' })` in a JSDoc
|
|
156
|
+
* example or inside a string is not a call. Quoted strings stay, they are
|
|
157
|
+
* the names.
|
|
158
|
+
*/
|
|
159
|
+
const maskScript = (code) => {
|
|
160
|
+
let out = '';
|
|
161
|
+
let state = null;
|
|
162
|
+
for (let i = 0; i < code.length; i++) {
|
|
163
|
+
const c = code[i];
|
|
164
|
+
const next = code[i + 1];
|
|
165
|
+
const blank = c === '\n' ? '\n' : ' ';
|
|
166
|
+
if (state === 'line') {
|
|
167
|
+
if (c === '\n') {
|
|
168
|
+
state = null;
|
|
169
|
+
out += c;
|
|
170
|
+
} else out += ' ';
|
|
171
|
+
} else if (state === 'block') {
|
|
172
|
+
if (c === '*' && next === '/') {
|
|
173
|
+
state = null;
|
|
174
|
+
out += ' ';
|
|
175
|
+
i++;
|
|
176
|
+
} else out += blank;
|
|
177
|
+
} else if (state === '`') {
|
|
178
|
+
if (c === '\\') {
|
|
179
|
+
out += ' ';
|
|
180
|
+
i++;
|
|
181
|
+
} else if (c === '`') {
|
|
182
|
+
state = null;
|
|
183
|
+
out += c;
|
|
184
|
+
} else out += blank;
|
|
185
|
+
} else if (state === "'" || state === '"') {
|
|
186
|
+
out += c;
|
|
187
|
+
if (c === '\\') {
|
|
188
|
+
out += next ?? '';
|
|
189
|
+
i++;
|
|
190
|
+
} else if (c === state || c === '\n') state = null;
|
|
191
|
+
} else if (c === '/' && next === '/') {
|
|
192
|
+
state = 'line';
|
|
193
|
+
out += ' ';
|
|
194
|
+
i++;
|
|
195
|
+
} else if (c === '/' && next === '*') {
|
|
196
|
+
state = 'block';
|
|
197
|
+
out += ' ';
|
|
198
|
+
i++;
|
|
199
|
+
} else {
|
|
200
|
+
if (c === "'" || c === '"' || c === '`') state = c;
|
|
201
|
+
out += c;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return out;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* `confirm(…)` calls in a script: literal portals become call sites, any other
|
|
209
|
+
* argument that could carry a portal marks the confirm family dynamic.
|
|
210
|
+
*/
|
|
211
|
+
const scriptSites = (source, firstLine, code, sites) => {
|
|
212
|
+
const script = maskScript(source);
|
|
213
|
+
literalsIn(script, sites.literals);
|
|
214
|
+
// A template literal with no interpolation is a plain string too.
|
|
215
|
+
for (const [, value] of source.matchAll(/`([^`$\\\n]{1,120})`/g))
|
|
216
|
+
sites.literals.add(value);
|
|
217
|
+
for (const match of script.matchAll(/\bconfirm\s*\(/g)) {
|
|
218
|
+
const open = match.index + match[0].length - 1;
|
|
219
|
+
const args = callArguments(script, open);
|
|
220
|
+
if (args == null) continue;
|
|
221
|
+
const trimmed = args.trim();
|
|
222
|
+
if (!trimmed || /^(['"`])/.test(trimmed)) continue;
|
|
223
|
+
if (!trimmed.startsWith('{')) {
|
|
224
|
+
sites.dynamicCalls.add('confirm');
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const portal = portalKey(trimmed, code);
|
|
228
|
+
if (!portal || portal.component) continue;
|
|
229
|
+
if (portal.dynamic) {
|
|
230
|
+
sites.dynamicCalls.add('confirm');
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
sites.calls.push({
|
|
234
|
+
family: 'confirm',
|
|
235
|
+
name: portal.name,
|
|
236
|
+
line:
|
|
237
|
+
firstLine +
|
|
238
|
+
lineAt(script, open) +
|
|
239
|
+
lineAt(args, args.indexOf(trimmed)) +
|
|
240
|
+
portal.offset,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
/** A declaration's `name`: a static attribute, a literal binding, or dynamic. */
|
|
246
|
+
const declarationName = (node) => {
|
|
247
|
+
for (const prop of node.props ?? []) {
|
|
248
|
+
if (prop.type === PROP_ATTRIBUTE && prop.name === 'name')
|
|
249
|
+
return { name: prop.value?.content ?? '', line: prop.loc.start.line };
|
|
250
|
+
if (
|
|
251
|
+
prop.type === PROP_DIRECTIVE &&
|
|
252
|
+
prop.name === 'bind' &&
|
|
253
|
+
prop.arg?.content === 'name'
|
|
254
|
+
) {
|
|
255
|
+
const name = stringLiteral(prop.exp?.content);
|
|
256
|
+
return name ? { name, line: prop.loc.start.line } : { dynamic: true };
|
|
257
|
+
}
|
|
258
|
+
// `v-bind="obj"`: the name may be in it.
|
|
259
|
+
if (prop.type === PROP_DIRECTIVE && prop.name === 'bind' && !prop.arg)
|
|
260
|
+
return { dynamic: true };
|
|
261
|
+
}
|
|
262
|
+
return { dynamic: true };
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* The portal sites of one file.
|
|
267
|
+
* @param {string} code
|
|
268
|
+
* @param {string} [path] decides `.vue` vs a script file
|
|
269
|
+
*/
|
|
270
|
+
export function fileSites(code, path = 'x.vue') {
|
|
271
|
+
const sites = {
|
|
272
|
+
declarations: [],
|
|
273
|
+
calls: [],
|
|
274
|
+
dynamicDeclarations: new Set(),
|
|
275
|
+
dynamicCalls: new Set(),
|
|
276
|
+
literals: new Set(),
|
|
277
|
+
};
|
|
278
|
+
if (!path.endsWith('.vue')) {
|
|
279
|
+
if (/\bconfirm\s*\(/.test(code) || /['"]/.test(code))
|
|
280
|
+
scriptSites(code, 1, code, sites);
|
|
281
|
+
return sites;
|
|
282
|
+
}
|
|
283
|
+
let descriptor;
|
|
284
|
+
try {
|
|
285
|
+
({ descriptor } = parse(code, { sourceMap: false }));
|
|
286
|
+
} catch {
|
|
287
|
+
return sites;
|
|
288
|
+
}
|
|
289
|
+
for (const block of [descriptor.script, descriptor.scriptSetup])
|
|
290
|
+
if (block) scriptSites(block.content, block.loc.start.line, code, sites);
|
|
291
|
+
|
|
292
|
+
const walkTemplate = (node) => {
|
|
293
|
+
if (!node || typeof node !== 'object') return;
|
|
294
|
+
if (node.type === NODE_ELEMENT) {
|
|
295
|
+
const family = FAMILY_BY_COMPONENT.get(pascal(node.tag));
|
|
296
|
+
if (family) {
|
|
297
|
+
const declared = declarationName(node);
|
|
298
|
+
if (declared.dynamic) sites.dynamicDeclarations.add(family);
|
|
299
|
+
else sites.declarations.push({ family, ...declared });
|
|
300
|
+
}
|
|
301
|
+
for (const prop of node.props ?? []) {
|
|
302
|
+
if (prop.type !== PROP_DIRECTIVE) continue;
|
|
303
|
+
const exp = prop.exp?.content;
|
|
304
|
+
literalsIn(exp, sites.literals);
|
|
305
|
+
const directiveFamily = FAMILY_BY_DIRECTIVE.get(prop.name);
|
|
306
|
+
if (!directiveFamily || !exp) continue;
|
|
307
|
+
const line = prop.exp.loc?.start?.line ?? prop.loc.start.line;
|
|
308
|
+
const trimmed = exp.trim();
|
|
309
|
+
if (trimmed.startsWith('{')) {
|
|
310
|
+
const portal = portalKey(trimmed, code);
|
|
311
|
+
if (!portal || portal.component) continue;
|
|
312
|
+
if (portal.dynamic) sites.dynamicCalls.add(directiveFamily);
|
|
313
|
+
else
|
|
314
|
+
sites.calls.push({
|
|
315
|
+
family: directiveFamily,
|
|
316
|
+
name: portal.name,
|
|
317
|
+
line: line + lineAt(exp, exp.indexOf(trimmed)) + portal.offset,
|
|
318
|
+
});
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
const value = classifyPortalValue(trimmed, code);
|
|
322
|
+
if (value.dynamic) sites.dynamicCalls.add(directiveFamily);
|
|
323
|
+
else if (value.name)
|
|
324
|
+
sites.calls.push({ family: directiveFamily, name: value.name, line });
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
for (const child of node.children ?? []) walkTemplate(child);
|
|
328
|
+
};
|
|
329
|
+
walkTemplate(descriptor.template?.ast);
|
|
330
|
+
return sites;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const SOURCE = /\.(?:vue|[cm]?[jt]sx?)$/;
|
|
334
|
+
|
|
335
|
+
const cache = new Map();
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Every portal site of a project, scanned once per root (tests skipped).
|
|
339
|
+
* @param {string} projectRoot
|
|
340
|
+
*/
|
|
341
|
+
export function projectPortalIndex(projectRoot) {
|
|
342
|
+
if (cache.has(projectRoot)) return cache.get(projectRoot);
|
|
343
|
+
const index = emptyIndex();
|
|
344
|
+
for (const path of walk(projectRoot)) {
|
|
345
|
+
if (path.endsWith('.d.ts') || !SOURCE.test(path)) continue;
|
|
346
|
+
let code;
|
|
347
|
+
try {
|
|
348
|
+
code = readFileSync(path, 'utf-8');
|
|
349
|
+
} catch {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (!MAY_HAVE_PORTALS.test(code)) {
|
|
353
|
+
// Still a source of string literals for `unused-portal`.
|
|
354
|
+
literalsIn(code, index.literals);
|
|
355
|
+
for (const [, value] of code.matchAll(/`([^`$\\\n]{1,120})`/g))
|
|
356
|
+
index.literals.add(value);
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
addSites(index, fileSites(code, path), path);
|
|
360
|
+
}
|
|
361
|
+
cache.set(projectRoot, index);
|
|
362
|
+
return index;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function emptyIndex() {
|
|
366
|
+
return {
|
|
367
|
+
/** family → name → files */
|
|
368
|
+
declared: new Map(Object.keys(PORTAL_FAMILIES).map((f) => [f, new Map()])),
|
|
369
|
+
dynamicDeclarations: new Set(),
|
|
370
|
+
dynamicCalls: new Set(),
|
|
371
|
+
literals: new Set(),
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function addSites(index, sites, path) {
|
|
376
|
+
for (const { family, name } of sites.declarations) {
|
|
377
|
+
const byName = index.declared.get(family);
|
|
378
|
+
if (!byName.has(name)) byName.set(name, new Set());
|
|
379
|
+
byName.get(name).add(path);
|
|
380
|
+
}
|
|
381
|
+
for (const family of sites.dynamicDeclarations)
|
|
382
|
+
index.dynamicDeclarations.add(family);
|
|
383
|
+
for (const family of sites.dynamicCalls) index.dynamicCalls.add(family);
|
|
384
|
+
for (const value of sites.literals) index.literals.add(value);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** For tests: forget cached project scans. */
|
|
388
|
+
export const clearPortalCache = () => cache.clear();
|
|
389
|
+
|
|
390
|
+
const quoteCall = (family, name) =>
|
|
391
|
+
family === 'confirm'
|
|
392
|
+
? `\`confirm({ portal: '${name}' })\``
|
|
393
|
+
: `\`${PORTAL_FAMILIES[family].openWith(name)}\``;
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* The findings of one file against its project (`index`; `null` judges the
|
|
397
|
+
* file alone). The file's own sites are read fresh, so an editor sees its
|
|
398
|
+
* edits before the cached scan does.
|
|
399
|
+
* @param {string} code
|
|
400
|
+
* @param {string} path
|
|
401
|
+
* @param {ReturnType<typeof projectPortalIndex> | null} index
|
|
402
|
+
*/
|
|
403
|
+
export function portalFindings(code, path, index) {
|
|
404
|
+
const sites = fileSites(code, path);
|
|
405
|
+
if (!sites.calls.length && !sites.declarations.length) return [];
|
|
406
|
+
const merged = emptyIndex();
|
|
407
|
+
if (index) {
|
|
408
|
+
for (const [family, byName] of index.declared)
|
|
409
|
+
for (const [name, files] of byName)
|
|
410
|
+
merged.declared.get(family).set(name, new Set(files));
|
|
411
|
+
for (const family of index.dynamicDeclarations)
|
|
412
|
+
merged.dynamicDeclarations.add(family);
|
|
413
|
+
for (const family of index.dynamicCalls) merged.dynamicCalls.add(family);
|
|
414
|
+
for (const value of index.literals) merged.literals.add(value);
|
|
415
|
+
}
|
|
416
|
+
addSites(merged, sites, path);
|
|
417
|
+
|
|
418
|
+
const findings = [];
|
|
419
|
+
for (const { family, name, line } of sites.calls) {
|
|
420
|
+
const own = merged.declared.get(family);
|
|
421
|
+
if (own.has(name) || merged.dynamicDeclarations.has(family)) continue;
|
|
422
|
+
const f = PORTAL_FAMILIES[family];
|
|
423
|
+
const other = Object.keys(PORTAL_FAMILIES).find(
|
|
424
|
+
(g) => g !== family && merged.declared.get(g).has(name)
|
|
425
|
+
);
|
|
426
|
+
if (other) {
|
|
427
|
+
findings.push({
|
|
428
|
+
component: f.component,
|
|
429
|
+
attr: f.opener,
|
|
430
|
+
kind: 'portal-family-mismatch',
|
|
431
|
+
line,
|
|
432
|
+
message: `\`${name}\` is a \`<${PORTAL_FAMILIES[other].component}>\`, not a \`<${f.component}>\`, so ${quoteCall(family, name)} opens nothing; open it with ${quoteCall(other, name)} or declare \`<${f.component} name="${name}">\``,
|
|
433
|
+
});
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
const guess = nearestName(name, own.keys());
|
|
437
|
+
findings.push({
|
|
438
|
+
component: f.component,
|
|
439
|
+
attr: f.opener,
|
|
440
|
+
kind: 'unknown-portal',
|
|
441
|
+
line,
|
|
442
|
+
message: `${quoteCall(family, name)} opens nothing: no \`<${f.component} name="${name}">\` in this project${guess ? `; did you mean \`${guess}\`?` : ''}`,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
for (const { family, name, line } of sites.declarations) {
|
|
446
|
+
if (merged.dynamicCalls.has(family) || merged.literals.has(name)) continue;
|
|
447
|
+
const f = PORTAL_FAMILIES[family];
|
|
448
|
+
findings.push({
|
|
449
|
+
component: f.component,
|
|
450
|
+
attr: 'name',
|
|
451
|
+
kind: 'unused-portal',
|
|
452
|
+
severity: 'warning',
|
|
453
|
+
line,
|
|
454
|
+
message: `no \`${f.opener}\` opens the portal \`${name}\`; open it with ${quoteCall(family, name)} or remove it`,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
return findings;
|
|
458
|
+
}
|