bitboss-ui 3.0.0-beta.21 → 3.0.0-beta.23
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 +55 -3
- package/bin/bitboss-ui-mcp.mjs +419 -9
- package/bin/bitboss-ui.mjs +293 -22
- package/dist/ai/BbAccordion.md +21 -23
- package/dist/ai/BbAlert.md +6 -12
- package/dist/ai/BbAvatar.md +60 -24
- package/dist/ai/BbBadge.md +12 -20
- package/dist/ai/BbBaseButton.md +14 -16
- package/dist/ai/BbBaseCheckboxIcon.md +6 -10
- package/dist/ai/BbBaseColorInput.md +1 -0
- package/dist/ai/BbBaseDatePickerInput.md +1 -0
- package/dist/ai/BbBaseNumberInput.md +1 -0
- package/dist/ai/BbBaseRadioIcon.md +6 -10
- package/dist/ai/BbBaseSelect.md +1 -0
- package/dist/ai/BbBaseSwitchIcon.md +5 -9
- package/dist/ai/BbBaseTag.md +1 -0
- package/dist/ai/BbBaseTextInput.md +2 -1
- package/dist/ai/BbBaseTextarea.md +1 -0
- package/dist/ai/BbBaseTimePickerInput.md +1 -0
- package/dist/ai/BbBreadcrumbs.md +23 -34
- package/dist/ai/BbButton.md +35 -47
- package/dist/ai/BbCheckbox.md +21 -37
- package/dist/ai/BbCheckboxGroup.md +13 -13
- package/dist/ai/BbCollapsible.md +14 -26
- package/dist/ai/BbColorInput.md +27 -23
- package/dist/ai/BbColorPalette.md +23 -35
- package/dist/ai/BbConfirm.md +17 -32
- package/dist/ai/BbDatePickerInput.md +29 -21
- package/dist/ai/BbDialog.md +159 -62
- package/dist/ai/BbDropdown.md +21 -25
- package/dist/ai/BbDropdownButton.md +13 -30
- package/dist/ai/BbDropzone.md +9 -11
- package/dist/ai/BbIcon.md +44 -33
- package/dist/ai/BbIndicator.md +2 -2
- package/dist/ai/BbNumberInput.md +32 -20
- package/dist/ai/BbOffCanvas.md +42 -42
- package/dist/ai/BbPagination.md +21 -23
- package/dist/ai/BbPopover.md +15 -17
- package/dist/ai/BbProgress.md +40 -51
- package/dist/ai/BbRadio.md +9 -9
- package/dist/ai/BbRadioGroup.md +7 -11
- package/dist/ai/BbRating.md +8 -12
- package/dist/ai/BbSelect.md +46 -36
- package/dist/ai/BbSelectPopover.md +19 -31
- package/dist/ai/BbSlider.md +15 -19
- package/dist/ai/BbSmoothHeight.md +16 -31
- package/dist/ai/BbSpinner.md +118 -74
- package/dist/ai/BbSwitch.md +25 -18
- package/dist/ai/BbSwitchGroup.md +9 -14
- package/dist/ai/BbTable.md +102 -55
- package/dist/ai/BbTabs.md +59 -82
- package/dist/ai/BbTag.md +26 -23
- package/dist/ai/BbTextInput.md +63 -27
- package/dist/ai/BbTextarea.md +42 -34
- package/dist/ai/BbTimePickerInput.md +14 -0
- package/dist/ai/BbToast.md +14 -34
- package/dist/ai/BbTooltip.md +22 -20
- package/dist/ai/BbTree.md +46 -72
- package/dist/ai/CommonInputWrapper.md +1 -0
- package/dist/ai/changelog.json +170 -62
- package/dist/ai/components.json +1678 -11
- package/dist/ai/composables/useConfirm.md +5 -2
- package/dist/ai/composables/useToast.md +4 -2
- package/dist/ai/guides/agent-contract.md +7 -4
- package/dist/ai/guides/ai-router.md +37 -22
- package/dist/ai/guides/component-picker.md +23 -18
- package/dist/ai/guides/design-language.md +151 -21
- package/dist/ai/guides/design-tokens.md +29 -4
- package/dist/ai/guides/icons-policy.md +46 -21
- package/dist/ai/guides/installation-and-plugin-setup.md +286 -17
- package/dist/ai/guides/migration/components/bb-rating.md +26 -1
- package/dist/ai/guides/migration/components/bb-tooltip.md +18 -0
- package/dist/ai/guides/migration/v2-to-v3.md +50 -23
- package/dist/ai/index.md +1 -1
- package/dist/ai/recipes/inertia/approvals-inbox.md +3 -3
- package/dist/ai/recipes/inertia/inline-edit-workspace.md +2 -2
- package/dist/ai/recipes/inertia/onboarding.md +2 -2
- package/dist/ai/recipes/inertia/ownership-atlas.md +6 -6
- package/dist/ai/recipes/inertia/record-form.md +5 -5
- package/dist/ai/recipes/inertia/records-workspace.md +4 -4
- package/dist/ai/recipes/inertia/upload-center.md +6 -6
- package/dist/ai/recipes/inertia/wizard-form.md +5 -5
- package/dist/ai/recipes/nuxt/approvals-inbox.md +3 -3
- package/dist/ai/recipes/nuxt/inline-edit-workspace.md +2 -2
- package/dist/ai/recipes/nuxt/onboarding.md +2 -2
- package/dist/ai/recipes/nuxt/record-form.md +5 -5
- package/dist/ai/recipes/nuxt/records-workspace.md +4 -4
- package/dist/ai/recipes/nuxt/upload-center.md +6 -6
- package/dist/ai/recipes/nuxt/wizard-form.md +5 -5
- package/dist/ai/recipes/vue/approvals-inbox.md +3 -3
- package/dist/ai/recipes/vue/inline-edit-workspace.md +2 -2
- package/dist/ai/recipes/vue/onboarding.md +2 -2
- package/dist/ai/recipes/vue/record-form.md +10 -10
- package/dist/ai/recipes/vue/records-workspace.md +4 -4
- package/dist/ai/recipes/vue/upload-center.md +6 -6
- package/dist/ai/recipes/vue/wizard-form.md +5 -5
- package/dist/ai/source/BbBadge.md +1 -1
- package/dist/ai/source/BbBaseCheckbox.md +1 -1
- package/dist/ai/source/BbBaseColorInput.md +7 -0
- package/dist/ai/source/BbBaseDatePicker.md +5 -5
- package/dist/ai/source/BbBaseDatePickerInput.md +9 -2
- package/dist/ai/source/BbBaseNumberInput.md +7 -0
- package/dist/ai/source/BbBaseRadio.md +1 -1
- package/dist/ai/source/BbBaseRating.md +5 -5
- package/dist/ai/source/BbBaseSelect.md +8 -1
- package/dist/ai/source/BbBaseSlider.md +5 -5
- package/dist/ai/source/BbBaseSwitch.md +1 -1
- package/dist/ai/source/BbBaseTag.md +7 -0
- package/dist/ai/source/BbBaseTextInput.md +7 -0
- package/dist/ai/source/BbBaseTextarea.md +7 -0
- package/dist/ai/source/BbBaseTimePickerInput.md +8 -1
- package/dist/ai/source/BbBreadcrumbs.md +20 -1
- package/dist/ai/source/BbButton.md +2 -2
- package/dist/ai/source/BbColorInput.md +14 -0
- package/dist/ai/source/BbDatePickerInput.md +14 -0
- package/dist/ai/source/BbDialog.md +39 -8
- package/dist/ai/source/BbDropdown.md +1 -1
- package/dist/ai/source/BbDropzone.md +5 -5
- package/dist/ai/source/BbIcon.md +23 -10
- package/dist/ai/source/BbNumberInput.md +14 -0
- package/dist/ai/source/BbOffCanvas.md +50 -28
- package/dist/ai/source/BbPagination.md +2 -2
- package/dist/ai/source/BbPopover.md +12 -1
- package/dist/ai/source/BbSelect.md +14 -0
- package/dist/ai/source/BbSelectPopover.md +45 -7
- package/dist/ai/source/BbSpinner.md +2 -2
- package/dist/ai/source/BbTable.md +11 -11
- package/dist/ai/source/BbTag.md +14 -0
- package/dist/ai/source/BbTextInput.md +14 -0
- package/dist/ai/source/BbTextarea.md +14 -0
- package/dist/ai/source/BbTimePickerInput.md +14 -0
- package/dist/ai/source/BbTooltip.md +57 -10
- package/dist/ai/source/ChipsBox.md +1 -1
- package/dist/ai/source/CommaBox.md +1 -1
- package/dist/ai/source/CommonInputWrapper.md +128 -33
- package/dist/ai/source/GuidesComponent.md +14 -7
- package/dist/ai/source/OptionsContainer.md +1 -1
- package/dist/components/BbBadge/BadgeBodyContent.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/components/BbBadge/BbBadge.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/components/BbBaseCheckbox/BbBaseCheckbox.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/components/BbBaseColorInput/BbBaseColorInput.vue_vue_type_script_setup_true_lang.js +4 -1
- package/dist/components/BbBaseColorInput/types.d.ts +5 -0
- package/dist/components/BbBaseDatePicker/BbBaseDatePicker.vue_vue_type_script_setup_true_lang.js +4 -4
- package/dist/components/BbBaseDatePicker/BbBaseDatePickerInputDaySelector.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/components/BbBaseDatePickerInput/BbBaseDatePickerInput.vue_vue_type_script_setup_true_lang.js +5 -2
- package/dist/components/BbBaseDatePickerInput/types.d.ts +5 -0
- package/dist/components/BbBaseInputContainer/BbBaseInputContainer.vue.d.ts +1 -1
- package/dist/components/BbBaseNumberInput/BbBaseNumberInput.vue_vue_type_script_setup_true_lang.js +4 -1
- package/dist/components/BbBaseNumberInput/types.d.ts +5 -0
- package/dist/components/BbBaseRadio/BbBaseRadio.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/components/BbBaseRating/BbBaseRating.vue_vue_type_script_setup_true_lang.js +3 -3
- package/dist/components/BbBaseSelect/BbBaseSelect.vue_vue_type_script_setup_true_lang.js +4 -1
- package/dist/components/BbBaseSelect/types.d.ts +5 -0
- package/dist/components/BbBaseSlider/BbBaseSlider.vue_vue_type_script_setup_true_lang.js +3 -3
- package/dist/components/BbBaseSwitch/BbBaseSwitch.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/components/BbBaseTag/BbBaseTag.vue_vue_type_script_setup_true_lang.js +4 -1
- package/dist/components/BbBaseTag/types.d.ts +5 -0
- package/dist/components/BbBaseTextInput/BbBaseTextInput.vue_vue_type_script_setup_true_lang.js +4 -1
- package/dist/components/BbBaseTextInput/types.d.ts +5 -0
- package/dist/components/BbBaseTextarea/BbBaseTextarea.vue_vue_type_script_setup_true_lang.js +4 -1
- package/dist/components/BbBaseTextarea/types.d.ts +5 -0
- package/dist/components/BbBaseTimePickerInput/BbBaseTimePickerInput.vue_vue_type_script_setup_true_lang.js +5 -2
- package/dist/components/BbBaseTimePickerInput/types.d.ts +5 -0
- package/dist/components/BbBreadcrumbs/BbBreadcrumbs.vue_vue_type_script_setup_true_lang.js +71 -71
- package/dist/components/BbButton/BbButton.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/components/BbColorInput/BbColorInput.vue_vue_type_script_setup_true_lang.js +5 -2
- package/dist/components/BbColorInput/types.d.ts +12 -0
- package/dist/components/BbDatePickerInput/BbDatePickerInput.vue_vue_type_script_setup_true_lang.js +4 -2
- package/dist/components/BbDatePickerInput/types.d.ts +12 -0
- package/dist/components/BbDialog/BbDialog.vue_vue_type_script_setup_true_lang.js +102 -97
- package/dist/components/BbDropdown/AdaptiveDropdown.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/components/BbDropdown/BbDropdown.vue_vue_type_script_setup_true_lang.js +58 -58
- package/dist/components/BbDropzone/BbDropzone.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/components/BbIcon/BbIcon.vue_vue_type_script_setup_true_lang.js +33 -35
- package/dist/components/BbIcon/iconCache.d.ts +30 -0
- package/dist/components/BbIcon/iconCache.js +21 -3
- package/dist/components/BbNumberInput/BbNumberInput.vue_vue_type_script_setup_true_lang.js +5 -2
- package/dist/components/BbNumberInput/types.d.ts +12 -0
- package/dist/components/BbOffCanvas/BbOffCanvas.vue_vue_type_script_setup_true_lang.js +57 -56
- package/dist/components/BbPagination/BbPagination.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/components/BbPopover/BbPopover.vue_vue_type_script_setup_true_lang.js +138 -138
- package/dist/components/BbSelect/BbSelect.vue_vue_type_script_setup_true_lang.js +4 -2
- package/dist/components/BbSelect/types.d.ts +12 -0
- package/dist/components/BbSelectPopover/BbSelectPopover.vue_vue_type_script_setup_true_lang.js +43 -43
- package/dist/components/BbSpinner/BbSpinner.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/components/BbSpinner/types.d.ts +1 -1
- package/dist/components/BbTable/BbTable.vue_vue_type_script_setup_true_lang.js +8 -8
- package/dist/components/BbTable/BbTableDataRow.js +2 -2
- package/dist/components/BbTag/BbTag.vue_vue_type_script_setup_true_lang.js +5 -2
- package/dist/components/BbTag/types.d.ts +12 -0
- package/dist/components/BbTextInput/BbTextInput.vue_vue_type_script_setup_true_lang.js +5 -2
- package/dist/components/BbTextInput/types.d.ts +12 -0
- package/dist/components/BbTextarea/BbTextarea.vue_vue_type_script_setup_true_lang.js +5 -2
- package/dist/components/BbTextarea/types.d.ts +12 -0
- package/dist/components/BbTimePickerInput/BbTimePickerInput.vue_vue_type_script_setup_true_lang.js +4 -2
- package/dist/components/BbTimePickerInput/types.d.ts +12 -0
- package/dist/components/BbToast/BbToastRegion.vue_vue_type_script_setup_true_lang.js +62 -62
- package/dist/components/BbTooltip/BbTooltip.vue_vue_type_script_setup_true_lang.js +81 -78
- package/dist/components/BbTooltip/types.d.ts +8 -1
- package/dist/components/ChipsBox/ChipsBox.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/components/CommaBox/CommaBox.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/components/CommonInputWrapper/CommonInputWrapper.vue.d.ts +1 -0
- package/dist/components/CommonInputWrapper/CommonInputWrapper.vue_vue_type_script_setup_true_lang.js +4 -2
- package/dist/components/CommonInputWrapper/types.d.ts +10 -0
- package/dist/components/OptionsContainer/OptionsContainer.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/composables/useConfirm.d.ts +22 -0
- package/dist/composables/useConfirm.js +43 -40
- package/dist/composables/useSafeAreaInternal.js +1 -1
- package/dist/composables/useToast.d.ts +16 -0
- package/dist/composables/useToast.js +20 -16
- package/dist/composables/useViewportWidth.d.ts +20 -6
- package/dist/composables/useViewportWidth.js +8 -7
- package/dist/icon-scan.d.ts +276 -0
- package/dist/icon-scan.js +485 -0
- package/dist/icons-whitelist.d.ts +5 -4
- package/dist/index.d.ts +2 -0
- package/dist/input-variants.d.ts +18 -0
- package/dist/llms-full.txt +2173 -1537
- package/dist/llms-medium.txt +504 -82
- package/dist/llms.txt +1 -1
- package/dist/nuxt-module.d.ts +1 -0
- package/dist/nuxt.js +10 -7
- package/dist/plugin.js +1 -0
- package/dist/runtime/nuxt-plugin.js +2 -1
- package/dist/styles.css +1 -1
- package/dist/types/Config.d.ts +18 -0
- package/dist/types/InputVariant.d.ts +22 -0
- package/dist/utils/cssLayers.d.ts +91 -0
- package/dist/utils/cssLayers.js +43 -0
- package/dist/utils/injectLibraryStyles.d.ts +3 -1
- package/dist/utils/injectLibraryStyles.js +24 -23
- package/dist/validated/BbColorInput.vue_vue_type_script_setup_true_lang.js +1 -0
- package/dist/validated/BbDatePickerInput.vue_vue_type_script_setup_true_lang.js +1 -0
- package/dist/validated/BbNumberInput.vue_vue_type_script_setup_true_lang.js +1 -0
- package/dist/validated/BbSelect.vue_vue_type_script_setup_true_lang.js +1 -0
- package/dist/validated/BbTag.vue_vue_type_script_setup_true_lang.js +1 -0
- package/dist/validated/BbTextInput.vue_vue_type_script_setup_true_lang.js +1 -0
- package/dist/validated/BbTextarea.vue_vue_type_script_setup_true_lang.js +1 -0
- package/dist/validated/BbTimePickerInput.vue_vue_type_script_setup_true_lang.js +1 -0
- package/dist/validated/index.d.ts +1 -0
- package/dist/vite-plugin.d.ts +89 -12
- package/dist/vite.js +331 -281
- package/llms.txt +1 -1
- package/package.json +7 -5
- package/scripts/lib/ensure-manifest.mjs +28 -5
- package/scripts/lib/eslint-plugin.d.ts +35 -0
- package/scripts/lib/eslint-plugin.mjs +299 -3
- package/scripts/lib/hand-roll-hints.mjs +50 -6
- package/scripts/lib/html-attributes.mjs +42 -0
- package/scripts/lib/mcp-config.mjs +38 -34
- package/scripts/lib/public-type-exports.mjs +246 -0
- package/scripts/lib/validate-bb-markup.mjs +205 -12
|
@@ -14,10 +14,22 @@
|
|
|
14
14
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
15
15
|
import { homedir } from 'node:os';
|
|
16
16
|
import { dirname, join, resolve } from 'node:path';
|
|
17
|
-
import { fileURLToPath } from 'node:url';
|
|
18
17
|
|
|
19
18
|
export const MCP_SERVER_NAME = 'bitboss-ui';
|
|
20
|
-
const COMMAND = '
|
|
19
|
+
const COMMAND = 'node';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Path to the installed CLI, relative to the project root.
|
|
23
|
+
*
|
|
24
|
+
* Relative on purpose: harnesses spawn a stdio server with the project root as
|
|
25
|
+
* its working directory, and a relative path is what lets a committed
|
|
26
|
+
* `.mcp.json` work on a teammate's machine. An absolute path would pin the
|
|
27
|
+
* config to whoever ran `ai-init`.
|
|
28
|
+
*
|
|
29
|
+
* Forward slashes even on Windows — Node accepts them, and JSON configs are
|
|
30
|
+
* shared across platforms.
|
|
31
|
+
*/
|
|
32
|
+
export const MCP_BIN_PATH = 'node_modules/bitboss-ui/bin/bitboss-ui.mjs';
|
|
21
33
|
|
|
22
34
|
/**
|
|
23
35
|
* The MCP server's own runtime needs, declared as OPTIONAL peers rather than
|
|
@@ -58,39 +70,31 @@ export function missingMcpPeers(root) {
|
|
|
58
70
|
}
|
|
59
71
|
|
|
60
72
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
73
|
+
* Invoke the INSTALLED binary directly, never `npx` (2026-09-09, superseding
|
|
74
|
+
* the 2026-08-06 pinned-npx ruling — see below).
|
|
75
|
+
*
|
|
76
|
+
* The 2026-08-06 review pinned the spec to this package's own version because
|
|
77
|
+
* MCP hosts spawn the entry with non-TTY stdio, where `npx` installs without
|
|
78
|
+
* prompting: an unpinned `npx bitboss-ui mcp` would silently download and
|
|
79
|
+
* execute whatever `bitboss-ui@latest` resolved to, and answer from a
|
|
80
|
+
* different version's knowledge base than the project uses.
|
|
81
|
+
*
|
|
82
|
+
* The pin fixed that and broke the server outright. `npx bitboss-ui@x.y.z mcp`
|
|
83
|
+
* runs the package OUT OF THE NPX CACHE, and the MCP entry resolves its
|
|
84
|
+
* optional peers (`@modelcontextprotocol/sdk`, `zod`) relative to the package
|
|
85
|
+
* it is running from. The cache directory never has them, so the server cannot
|
|
86
|
+
* start no matter what the project has installed — reported from a consumer
|
|
87
|
+
* project with both peers present.
|
|
88
|
+
*
|
|
89
|
+
* A path to the local binary satisfies the original ruling MORE completely
|
|
90
|
+
* than the pin did: it cannot reach the registry at all, so there is nothing
|
|
91
|
+
* to download and no `latest` to drift to. It also retires the pin's own
|
|
92
|
+
* maintenance problem, where a config written at one version kept naming that
|
|
93
|
+
* version after an upgrade, letting an agent query a manifest that disagreed
|
|
94
|
+
* with the code it was writing against. The binary on disk IS the installed
|
|
95
|
+
* version, by construction.
|
|
70
96
|
*/
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
const pkg = JSON.parse(
|
|
74
|
-
readFileSync(
|
|
75
|
-
join(
|
|
76
|
-
dirname(fileURLToPath(import.meta.url)),
|
|
77
|
-
'..',
|
|
78
|
-
'..',
|
|
79
|
-
'package.json'
|
|
80
|
-
),
|
|
81
|
-
'utf-8'
|
|
82
|
-
)
|
|
83
|
-
);
|
|
84
|
-
if (pkg.name === MCP_SERVER_NAME && typeof pkg.version === 'string') {
|
|
85
|
-
return `${MCP_SERVER_NAME}@${pkg.version}`;
|
|
86
|
-
}
|
|
87
|
-
} catch {
|
|
88
|
-
/* fall through to the unpinned spec */
|
|
89
|
-
}
|
|
90
|
-
return MCP_SERVER_NAME;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const ARGS = [resolveNpxSpec(), 'mcp'];
|
|
97
|
+
const ARGS = [MCP_BIN_PATH, 'mcp'];
|
|
94
98
|
|
|
95
99
|
const stdioEntry = () => ({ command: COMMAND, args: [...ARGS] });
|
|
96
100
|
const vscodeEntry = () => ({
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The library's public TYPE exports, read from `src/index.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Two problems this solves, both reported from a consumer app:
|
|
5
|
+
*
|
|
6
|
+
* 1. `BbTableColumn`, `BbDropdownItem`, `BbTabsItem` and `BbBreadcrumbsItem`
|
|
7
|
+
* are real, documented exports — but they are TYPES, not components. They
|
|
8
|
+
* were absent from every machine-readable surface, so `get_component` could
|
|
9
|
+
* not describe them and `check` treated `<BbDropdownItem>` as an unknown
|
|
10
|
+
* component whose only offered remedy was `allowComponents` — which is
|
|
11
|
+
* exactly wrong: the library owns the name, the markup is the mistake, and
|
|
12
|
+
* allowlisting the tag silences every prop check on it forever.
|
|
13
|
+
*
|
|
14
|
+
* 2. The manifest printed prop types under names a consumer cannot import.
|
|
15
|
+
* `BbDropdown.items` read `Item[]` (its module-local name; the export is
|
|
16
|
+
* `BbDropdownItem`) and `BbTable.columns` read `InternalColumn<T>[]` — a
|
|
17
|
+
* type alias declared inside `BbTable.vue` and exported nowhere at all.
|
|
18
|
+
*
|
|
19
|
+
* Both are fixed at the source: the export list is emitted into
|
|
20
|
+
* `components.json` (so the CLI, the ESLint rules and the MCP server can all
|
|
21
|
+
* answer for a type name), and the same list rewrites extracted type strings
|
|
22
|
+
* to the importable spelling.
|
|
23
|
+
*
|
|
24
|
+
* Parsing is done with the TypeScript AST rather than a regex: the export
|
|
25
|
+
* blocks carry JSDoc between the braces (`@deprecated` markers on the v2
|
|
26
|
+
* aliases), which a brace-matching regex reads as export names.
|
|
27
|
+
*/
|
|
28
|
+
import ts from 'typescript';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @typedef {object} PublicTypeExport
|
|
32
|
+
* @property {string} name the importable name (`BbDropdownItem`)
|
|
33
|
+
* @property {string} local the name inside its module (`Item`)
|
|
34
|
+
* @property {string} module the specifier it is re-exported from
|
|
35
|
+
* @property {string|null} component owning component, when `module` is a
|
|
36
|
+
* component types file (`./components/BbTable/types` → `BbTable`)
|
|
37
|
+
* @property {boolean} deprecated `@deprecated` in the export's own JSDoc
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/** `./components/BbTable/types` → `BbTable`; anything else → null. */
|
|
41
|
+
function componentFromModule(moduleSpecifier) {
|
|
42
|
+
const match = /^\.\/components\/([A-Za-z0-9_]+)\/types$/.exec(
|
|
43
|
+
moduleSpecifier
|
|
44
|
+
);
|
|
45
|
+
return match ? match[1] : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Every `export type { … } from '…'` name in the given `src/index.ts` source.
|
|
50
|
+
*
|
|
51
|
+
* Only type-only re-exports count. A value export (`export { bitbossUiPlugin }`)
|
|
52
|
+
* is a runtime binding, not a type a consumer would ever write as a tag, and
|
|
53
|
+
* mixing the two would let `useToast` answer a "did you mean a type?" question
|
|
54
|
+
* with a composable.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} source contents of `src/index.ts`
|
|
57
|
+
* @returns {PublicTypeExport[]}
|
|
58
|
+
*/
|
|
59
|
+
export function parsePublicTypeExports(source) {
|
|
60
|
+
const file = ts.createSourceFile(
|
|
61
|
+
'index.ts',
|
|
62
|
+
source,
|
|
63
|
+
ts.ScriptTarget.Latest,
|
|
64
|
+
/* setParentNodes */ true
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
/** @type {PublicTypeExport[]} */
|
|
68
|
+
const exports = [];
|
|
69
|
+
|
|
70
|
+
for (const statement of file.statements) {
|
|
71
|
+
if (!ts.isExportDeclaration(statement)) continue;
|
|
72
|
+
// `export type { … }` — or `export { type X }`, handled per-specifier below.
|
|
73
|
+
const declarationIsTypeOnly = statement.isTypeOnly;
|
|
74
|
+
const clause = statement.exportClause;
|
|
75
|
+
if (!clause || !ts.isNamedExports(clause)) continue;
|
|
76
|
+
const moduleSpecifier =
|
|
77
|
+
statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier)
|
|
78
|
+
? statement.moduleSpecifier.text
|
|
79
|
+
: '';
|
|
80
|
+
|
|
81
|
+
for (const specifier of clause.elements) {
|
|
82
|
+
if (!declarationIsTypeOnly && !specifier.isTypeOnly) continue;
|
|
83
|
+
const name = specifier.name.text;
|
|
84
|
+
const local = specifier.propertyName?.text ?? name;
|
|
85
|
+
exports.push({
|
|
86
|
+
name,
|
|
87
|
+
local,
|
|
88
|
+
module: moduleSpecifier,
|
|
89
|
+
component: componentFromModule(moduleSpecifier),
|
|
90
|
+
deprecated: hasDeprecatedTag(specifier, source),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return exports;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Whether the JSDoc immediately before a specifier carries `@deprecated`.
|
|
100
|
+
*
|
|
101
|
+
* The comment sits inside the braces, attached to nothing the AST calls a
|
|
102
|
+
* documented node, so `ts.getJSDocTags` finds nothing — the leading trivia of
|
|
103
|
+
* the specifier is where it actually lives.
|
|
104
|
+
*/
|
|
105
|
+
function hasDeprecatedTag(specifier, source) {
|
|
106
|
+
const ranges =
|
|
107
|
+
ts.getLeadingCommentRanges(source, specifier.getFullStart()) ?? [];
|
|
108
|
+
return ranges.some((range) =>
|
|
109
|
+
/@deprecated/.test(source.slice(range.pos, range.end))
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Module-local name → importable name, for one module.
|
|
115
|
+
*
|
|
116
|
+
* Scoped per module on purpose: `Item` means `BbDropdownItem` inside
|
|
117
|
+
* `components/BbDropdown/types` and `BbBreadcrumbsItem` inside
|
|
118
|
+
* `components/BbBreadcrumbs/types`. A global map would have to pick one and
|
|
119
|
+
* would mislabel the other.
|
|
120
|
+
*
|
|
121
|
+
* A deprecated alias never wins: `BbTabsItem` and `BbTabItem` are the same
|
|
122
|
+
* type, and the manifest should print the name we want written today.
|
|
123
|
+
*
|
|
124
|
+
* @param {PublicTypeExport[]} exports
|
|
125
|
+
* @param {string} moduleSpecifier
|
|
126
|
+
* @returns {Map<string, string>}
|
|
127
|
+
*/
|
|
128
|
+
export function localToPublicNames(exports, moduleSpecifier) {
|
|
129
|
+
/** @type {Map<string, PublicTypeExport>} */
|
|
130
|
+
const chosen = new Map();
|
|
131
|
+
for (const entry of exports) {
|
|
132
|
+
if (entry.module !== moduleSpecifier) continue;
|
|
133
|
+
const held = chosen.get(entry.local);
|
|
134
|
+
// First one wins, EXCEPT that a live name always displaces a deprecated
|
|
135
|
+
// one: `BbTableColumn` is re-exported as the deprecated `TableColumn`
|
|
136
|
+
// first and under its own name second, and source order alone would
|
|
137
|
+
// print the spelling we are trying to retire.
|
|
138
|
+
if (!held || (held.deprecated && !entry.deprecated)) {
|
|
139
|
+
chosen.set(entry.local, entry);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return new Map([...chosen].map(([local, entry]) => [local, entry.name]));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The type-parameter names an SFC declares in `<script setup generic="…">`.
|
|
147
|
+
*
|
|
148
|
+
* These must never be rewritten. `BbSelectPopover` declares `generic="Item =
|
|
149
|
+
* any"` while `BbDropdown` exports a TYPE whose module-local name is also
|
|
150
|
+
* `Item` — rewriting by name alone would one day turn a type VARIABLE into
|
|
151
|
+
* `BbDropdownItem` and quietly publish a contract that says the wrong thing.
|
|
152
|
+
* No component collides today; this is what keeps that true.
|
|
153
|
+
*
|
|
154
|
+
* @param {string} source the .vue file
|
|
155
|
+
* @returns {Set<string>}
|
|
156
|
+
*/
|
|
157
|
+
export function sfcGenericParameters(source) {
|
|
158
|
+
const match = /<script[^>]*\bgeneric=("([^"]*)"|'([^']*)')/.exec(source);
|
|
159
|
+
const declaration = match?.[2] ?? match?.[3];
|
|
160
|
+
if (!declaration) return new Set();
|
|
161
|
+
|
|
162
|
+
/** Split on top-level commas only: `T extends Record<string, any>` is one. */
|
|
163
|
+
const parts = [];
|
|
164
|
+
let depth = 0;
|
|
165
|
+
let current = '';
|
|
166
|
+
for (const char of declaration) {
|
|
167
|
+
if (char === '<' || char === '{' || char === '(' || char === '[') depth++;
|
|
168
|
+
else if (char === '>' || char === '}' || char === ')' || char === ']')
|
|
169
|
+
depth--;
|
|
170
|
+
if (char === ',' && depth === 0) {
|
|
171
|
+
parts.push(current);
|
|
172
|
+
current = '';
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
current += char;
|
|
176
|
+
}
|
|
177
|
+
parts.push(current);
|
|
178
|
+
|
|
179
|
+
return new Set(
|
|
180
|
+
parts
|
|
181
|
+
.map((part) => /^\s*([A-Za-z_$][A-Za-z0-9_$]*)/.exec(part)?.[1])
|
|
182
|
+
.filter((name) => Boolean(name))
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Rewrite bare identifiers in a printed type string through `aliases`.
|
|
188
|
+
*
|
|
189
|
+
* Word-boundary matching only — never a substring: `Item` must not touch
|
|
190
|
+
* `BbTreeItem`, and `Meta` must not touch `Metadata`. Generic arguments,
|
|
191
|
+
* unions and array suffixes all survive because only the identifier tokens are
|
|
192
|
+
* replaced (`Array<InternalColumn<K>>` → `Array<BbTableColumn<K>>`).
|
|
193
|
+
*
|
|
194
|
+
* @param {string} type
|
|
195
|
+
* @param {Map<string, string>} aliases
|
|
196
|
+
* @returns {string}
|
|
197
|
+
*/
|
|
198
|
+
export function rewriteTypeNames(type, aliases) {
|
|
199
|
+
if (!type || aliases.size === 0) return type;
|
|
200
|
+
return type.replace(/\b[A-Za-z_$][A-Za-z0-9_$]*\b/g, (identifier) => {
|
|
201
|
+
// Never rewrite a property name (`{ key: Item }` is fine, `foo.Item` is
|
|
202
|
+
// a qualified name whose right side is not ours to rename).
|
|
203
|
+
return aliases.get(identifier) ?? identifier;
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Type names declared inside an SFC's `<script>` blocks.
|
|
209
|
+
*
|
|
210
|
+
* These are the names that must never reach a published prop type. A type in a
|
|
211
|
+
* component's `types.ts` that is not exported is a DECISION — `OffCanvasSize`
|
|
212
|
+
* is `'xs' | 'sm' | …`, an app writes `size="lg"`, and exporting a name to
|
|
213
|
+
* spell a string literal would be a permanent compatibility promise bought for
|
|
214
|
+
* no capability. A type declared in the .vue file is not a decision: it is
|
|
215
|
+
* plumbing that escaped, and `BbTable.columns` published `InternalColumn<T>[]`
|
|
216
|
+
* for months because nothing looked.
|
|
217
|
+
*
|
|
218
|
+
* The script blocks are extracted before parsing rather than handing the whole
|
|
219
|
+
* .vue to TypeScript: the template is not TypeScript, and while the parser
|
|
220
|
+
* tolerates it, what it makes of `<div v-if="x < y">` is not worth relying on.
|
|
221
|
+
*
|
|
222
|
+
* @param {string} source the .vue file
|
|
223
|
+
* @returns {Set<string>}
|
|
224
|
+
*/
|
|
225
|
+
export function sfcDeclaredTypeNames(source) {
|
|
226
|
+
/** @type {Set<string>} */
|
|
227
|
+
const names = new Set();
|
|
228
|
+
for (const [, script] of source.matchAll(
|
|
229
|
+
/<script\b[^>]*>([\s\S]*?)<\/script>/g
|
|
230
|
+
)) {
|
|
231
|
+
const file = ts.createSourceFile(
|
|
232
|
+
'sfc.ts',
|
|
233
|
+
script,
|
|
234
|
+
ts.ScriptTarget.Latest,
|
|
235
|
+
true
|
|
236
|
+
);
|
|
237
|
+
const visit = (node) => {
|
|
238
|
+
if (ts.isTypeAliasDeclaration(node) || ts.isInterfaceDeclaration(node)) {
|
|
239
|
+
names.add(node.name.text);
|
|
240
|
+
}
|
|
241
|
+
ts.forEachChild(node, visit);
|
|
242
|
+
};
|
|
243
|
+
ts.forEachChild(file, visit);
|
|
244
|
+
}
|
|
245
|
+
return names;
|
|
246
|
+
}
|
|
@@ -17,7 +17,11 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
17
17
|
import { dirname, join } from 'node:path';
|
|
18
18
|
import { fileURLToPath } from 'node:url';
|
|
19
19
|
import { parse } from 'vue/compiler-sfc';
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
ELEMENT_DOM_EVENTS,
|
|
22
|
+
isFallthroughAttr,
|
|
23
|
+
NATIVE_DOM_EVENTS,
|
|
24
|
+
} from './html-attributes.mjs';
|
|
21
25
|
|
|
22
26
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
23
27
|
const DEFAULT_MANIFEST = join(HERE, '../../dist/ai/components.json');
|
|
@@ -156,6 +160,61 @@ export function nearestName(key, candidates) {
|
|
|
156
160
|
return best;
|
|
157
161
|
}
|
|
158
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Event names a consumer reaches for, mapped to the names our components
|
|
165
|
+
* actually emit, in preference order.
|
|
166
|
+
*
|
|
167
|
+
* Edit distance is the wrong tool for events. Props are misspelled, so
|
|
168
|
+
* `hideLabl` → `hideLabel` works; events are mis-VOCABULARIZED — someone writes
|
|
169
|
+
* the word their previous library used, or the word the interaction is called
|
|
170
|
+
* in English. `close` to `hide` is three edits and `close` to `hidden` is five,
|
|
171
|
+
* so `nearestName` finds nothing and the hint degrades to dumping every emitted
|
|
172
|
+
* event. That is exactly the case that shipped: `<BbDialog @close>` got
|
|
173
|
+
* "emitted events: hidden, hide, show, shown, update:modelValue" — a menu, not
|
|
174
|
+
* an answer.
|
|
175
|
+
*
|
|
176
|
+
* Only the resolved candidates that a component REALLY emits are offered, so
|
|
177
|
+
* this map can list generously without inventing events on components that
|
|
178
|
+
* don't have them.
|
|
179
|
+
*/
|
|
180
|
+
const EVENT_SYNONYMS = new Map([
|
|
181
|
+
// `hidden` before `hide`: the native `close` event these authors are
|
|
182
|
+
// currently getting fires at the END of the close, so `hidden` is the
|
|
183
|
+
// suggestion that preserves the timing they already have. `cancel` keeps
|
|
184
|
+
// `hide` first for the same reason — it fires when close is REQUESTED.
|
|
185
|
+
['close', ['hidden', 'hide', 'update:modelValue']],
|
|
186
|
+
['closed', ['hidden', 'hide']],
|
|
187
|
+
['dismiss', ['hidden', 'hide']],
|
|
188
|
+
['dismissed', ['hidden', 'hide']],
|
|
189
|
+
['hidden', ['hide']],
|
|
190
|
+
['open', ['show', 'shown', 'update:modelValue']],
|
|
191
|
+
['opened', ['shown', 'show']],
|
|
192
|
+
['reveal', ['show', 'shown']],
|
|
193
|
+
['shown', ['show']],
|
|
194
|
+
['toggle', ['update:modelValue']],
|
|
195
|
+
['change', ['update:modelValue']],
|
|
196
|
+
['changed', ['update:modelValue']],
|
|
197
|
+
['update', ['update:modelValue']],
|
|
198
|
+
['input', ['update:modelValue']],
|
|
199
|
+
['cancel', ['hide', 'hidden']],
|
|
200
|
+
]);
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* The event this component emits that the author most likely meant by `name`:
|
|
204
|
+
* a spelling near-miss first (`resizecolumn` → `resize:column`), then the
|
|
205
|
+
* vocabulary map above. Null when neither has an answer.
|
|
206
|
+
* @param {string} name event name exactly as written, modifiers stripped
|
|
207
|
+
* @param {Set<string>} declared events this component emits
|
|
208
|
+
*/
|
|
209
|
+
export function nearestEventName(name, declared) {
|
|
210
|
+
const near = nearestName(name, declared);
|
|
211
|
+
if (near) return near;
|
|
212
|
+
for (const candidate of EVENT_SYNONYMS.get(name) ?? []) {
|
|
213
|
+
if (declared.has(candidate)) return candidate;
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
159
218
|
/**
|
|
160
219
|
* The opposite-polarity spellings of a boolean prop name.
|
|
161
220
|
*
|
|
@@ -286,7 +345,11 @@ const INTENT_ALIASES = {
|
|
|
286
345
|
* @typedef {{ name: string, new?: string | null, since?: string, migration?: string, silent?: boolean }} RemovedEntry
|
|
287
346
|
* @typedef {{ name: string, props: Array<{ name: string }>, models: Array<{ name: string }>, referenceOnly?: boolean, removed?: RemovedEntry[], renamed?: Array<{ from: string, to: string }> }} ComponentSurface
|
|
288
347
|
* @typedef {Map<string, ComponentSurface>} ManifestMap
|
|
289
|
-
*
|
|
348
|
+
* `fallthrough` marks a finding that describes markup which is not a typo and
|
|
349
|
+
* may well be doing something today — a native listener reaching the root
|
|
350
|
+
* element — but is outside the component's contract. It is the flag a caller
|
|
351
|
+
* uses to report at a lower severity; see `checkEvent`.
|
|
352
|
+
* @typedef {{ component: string, attr: string, line?: number, kind: 'unknown-prop' | 'unknown-model' | 'removed-prop' | 'unknown-slot' | 'unknown-event', message: string, hint?: string, fallthrough?: boolean }} Finding
|
|
290
353
|
*/
|
|
291
354
|
|
|
292
355
|
/**
|
|
@@ -316,6 +379,20 @@ export function manifestFromComponents(input) {
|
|
|
316
379
|
const list = Array.isArray(input) ? input : (input.components ?? []);
|
|
317
380
|
/** @type {ManifestMap} */
|
|
318
381
|
const map = new Map();
|
|
382
|
+
/*
|
|
383
|
+
* Public TYPE exports, carried as a property on the map rather than as
|
|
384
|
+
* entries in it. `manifest.get(name)` means "this is a component I can
|
|
385
|
+
* check the props of" at nine call sites across the ESLint rules and the
|
|
386
|
+
* doc scanners; a type answering that lookup would make every one of them
|
|
387
|
+
* validate attributes against an empty surface. The property is invisible
|
|
388
|
+
* to `get`, to iteration, and to those call sites.
|
|
389
|
+
*/
|
|
390
|
+
map.types = new Map(
|
|
391
|
+
(Array.isArray(input) ? [] : (input.types ?? [])).map((entry) => [
|
|
392
|
+
entry.name,
|
|
393
|
+
entry,
|
|
394
|
+
])
|
|
395
|
+
);
|
|
319
396
|
for (const entry of list) {
|
|
320
397
|
map.set(entry.name, {
|
|
321
398
|
name: entry.name,
|
|
@@ -401,7 +478,7 @@ export function validateVueSnippet(code, manifest, options = {}) {
|
|
|
401
478
|
}
|
|
402
479
|
|
|
403
480
|
/**
|
|
404
|
-
* @typedef {{ attr: string, kind: 'unknown-prop' | 'unknown-model' | 'removed-prop' | 'unknown-slot' | 'unknown-event', message: string, hint?: string }} AttrFinding
|
|
481
|
+
* @typedef {{ attr: string, kind: 'unknown-prop' | 'unknown-model' | 'removed-prop' | 'unknown-slot' | 'unknown-event', message: string, hint?: string, fallthrough?: boolean }} AttrFinding
|
|
405
482
|
*/
|
|
406
483
|
|
|
407
484
|
/**
|
|
@@ -673,15 +750,37 @@ export function createComponentChecker(surface, options = {}) {
|
|
|
673
750
|
* unknown component events only under `strictTemplates`. A consumer's
|
|
674
751
|
* migration report found it.
|
|
675
752
|
*
|
|
676
|
-
* Accepted without consulting the manifest: any
|
|
753
|
+
* Accepted without consulting the manifest: any UNIVERSAL DOM event, since
|
|
677
754
|
* Vue passes an undeclared listener through to the root element —
|
|
678
755
|
* `@click` on BbButton is good markup even though BbButton emits only
|
|
679
756
|
* `update:modelValue`.
|
|
680
757
|
*
|
|
758
|
+
* That blanket pass used to cover every DOM event we listed, and it hid
|
|
759
|
+
* a second bug class. An event that fires on only a few element types —
|
|
760
|
+
* `close`, `cancel`, `play` — falls through the same way, but whether it
|
|
761
|
+
* ever fires depends on what the component's root element happens to be,
|
|
762
|
+
* which is an internal detail no consumer should be reasoning about.
|
|
763
|
+
* `<BbDialog @close>` works, because that root is a native `<dialog>`;
|
|
764
|
+
* `<BbAlert @close>` is inert. Both compile clean and neither is a typo.
|
|
765
|
+
* Those names (`ELEMENT_DOM_EVENTS`) get their own `fallthrough` finding
|
|
766
|
+
* so the caller can say "this is not the contract" instead of the
|
|
767
|
+
* flatly-wrong "this event does not exist" — the wording that led one
|
|
768
|
+
* migration report to file a live `@close` handler as dead code.
|
|
769
|
+
*
|
|
681
770
|
* Silent when the component declares no events at all: that is true of
|
|
682
771
|
* reference-only surfaces and of anything whose manifest entry predates
|
|
683
772
|
* event extraction, and guessing there would cry wolf — the same guard
|
|
684
773
|
* `checkSlot` uses.
|
|
774
|
+
*
|
|
775
|
+
* A third case is neither of those: `@x` where the component declares a
|
|
776
|
+
* PROP called `onX`. Vue compiles every `@x` to an `onX` vnode entry, and
|
|
777
|
+
* prop resolution claims it before listeners are ever considered — so the
|
|
778
|
+
* handler runs, bound to the prop. BbButton's Inertia visit callbacks are
|
|
779
|
+
* exactly this shape (`onSuccess`, `onFinish`, `onCancelToken`, …), which
|
|
780
|
+
* is why `<BbButton href @success>` has always worked. It is not a typo
|
|
781
|
+
* and not a fall-through; it is the documented `:on-success` prop reached
|
|
782
|
+
* by a spelling the guides tell you not to use, because it silently
|
|
783
|
+
* breaks the moment someone adds a modifier or a second handler.
|
|
685
784
|
*/
|
|
686
785
|
checkEvent(rawName) {
|
|
687
786
|
if (!rawName) return null;
|
|
@@ -700,14 +799,48 @@ export function createComponentChecker(surface, options = {}) {
|
|
|
700
799
|
if (name.startsWith('update:') && modelKeys.has(toCamel(name.slice(7))))
|
|
701
800
|
return null;
|
|
702
801
|
|
|
703
|
-
|
|
802
|
+
// `@success` → the declared `onSuccess` PROP. Real, working markup in
|
|
803
|
+
// the one spelling the guides steer away from.
|
|
804
|
+
const camel = toCamel(name);
|
|
805
|
+
const callbackProp = `on${camel.charAt(0).toUpperCase()}${camel.slice(1)}`;
|
|
806
|
+
if (propKeys.has(callbackProp)) {
|
|
807
|
+
return {
|
|
808
|
+
attr: `@${rawName}`,
|
|
809
|
+
kind: 'unknown-event',
|
|
810
|
+
fallthrough: true,
|
|
811
|
+
message:
|
|
812
|
+
`${surface.name} does not emit \`${name}\`, but it declares \`${callbackProp}\` as a PROP, ` +
|
|
813
|
+
'so Vue binds this handler to that prop and it does run. Prefer the prop spelling: ' +
|
|
814
|
+
'the listener form takes no modifiers, and a second handler for the same name silently ' +
|
|
815
|
+
'becomes an array the prop is not typed for',
|
|
816
|
+
hint: `use \`:${toKebab(callbackProp)}="…"\``,
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const near = nearestEventName(name, declared);
|
|
821
|
+
const suggestion = near
|
|
822
|
+
? `did you mean \`@${near}\`?`
|
|
823
|
+
: `emitted events: ${[...declared].sort().join(', ')}`;
|
|
824
|
+
|
|
825
|
+
if (ELEMENT_DOM_EVENTS.has(name)) {
|
|
826
|
+
return {
|
|
827
|
+
attr: `@${rawName}`,
|
|
828
|
+
kind: 'unknown-event',
|
|
829
|
+
fallthrough: true,
|
|
830
|
+
message:
|
|
831
|
+
`${surface.name} does not emit \`${name}\`, so this listener falls through to its root ` +
|
|
832
|
+
"element as a native DOM listener. It fires on the element's own schedule, not the " +
|
|
833
|
+
"component's, and only if that root happens to be an element that dispatches " +
|
|
834
|
+
`\`${name}\` — which is internal and can change without a breaking release`,
|
|
835
|
+
hint: suggestion,
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
|
|
704
839
|
return {
|
|
705
840
|
attr: `@${rawName}`,
|
|
706
841
|
kind: 'unknown-event',
|
|
707
842
|
message: `event \`${name}\` is not emitted by ${surface.name}`,
|
|
708
|
-
hint:
|
|
709
|
-
? `did you mean \`@${near}\`?`
|
|
710
|
-
: `emitted events: ${[...declared].sort().join(', ')}`,
|
|
843
|
+
hint: suggestion,
|
|
711
844
|
};
|
|
712
845
|
},
|
|
713
846
|
checkSlot(rawName) {
|
|
@@ -1126,6 +1259,69 @@ function compilerItemsMayBeAsync(node) {
|
|
|
1126
1259
|
return false; // `items` not bound at all — defaults to `[]`, definitely sync.
|
|
1127
1260
|
}
|
|
1128
1261
|
|
|
1262
|
+
/**
|
|
1263
|
+
* `BbTabs` → `BbTabs'`, `BbTable` → `BbTable's`. Shared so the CLI, the ESLint
|
|
1264
|
+
* rule and the MCP server all form the same sentence about a component.
|
|
1265
|
+
* @param {string} owner
|
|
1266
|
+
*/
|
|
1267
|
+
export const possessive = (owner) =>
|
|
1268
|
+
owner.endsWith('s') ? `${owner}'` : `${owner}'s`;
|
|
1269
|
+
|
|
1270
|
+
/**
|
|
1271
|
+
* Message + hint for a `Bb*` tag that is not a component.
|
|
1272
|
+
*
|
|
1273
|
+
* Three different situations wore one message, and the hint offered the same
|
|
1274
|
+
* remedy to all three — allowlist it — which is right for exactly one:
|
|
1275
|
+
*
|
|
1276
|
+
* - **A public TYPE written as a tag.** `BbTableColumn`, `BbDropdownItem` and
|
|
1277
|
+
* friends are real exports, so a consumer reasonably assumes they are
|
|
1278
|
+
* components. They are not: they type a prop. Allowlisting is the worst
|
|
1279
|
+
* available answer, because the markup IS wrong and the allowlist then
|
|
1280
|
+
* silences every real check on that tag forever. Reported from a consumer app
|
|
1281
|
+
* that took the hint.
|
|
1282
|
+
* - **A typo.** `BbButtn` is one edit from `BbButton`, and the prop checker has
|
|
1283
|
+
* suggested nearest names for years while this one dead-ended at "check the
|
|
1284
|
+
* spelling".
|
|
1285
|
+
* - **An app-owned component** that happens to start with `Bb`. The one case
|
|
1286
|
+
* the allowlist is for — now stated with what it costs.
|
|
1287
|
+
*
|
|
1288
|
+
* @param {string} name
|
|
1289
|
+
* @param {ManifestMap} manifest
|
|
1290
|
+
* @returns {{ message: string, hint: string }}
|
|
1291
|
+
*/
|
|
1292
|
+
export function unknownComponentFinding(name, manifest) {
|
|
1293
|
+
const type = manifest.types?.get(name);
|
|
1294
|
+
if (type) {
|
|
1295
|
+
const use = type.usedBy?.[0];
|
|
1296
|
+
const where = use
|
|
1297
|
+
? `it types ${possessive(use.component)} \`${use.name}\` ${use.surface}`
|
|
1298
|
+
: type.component
|
|
1299
|
+
? `it belongs to ${type.component}`
|
|
1300
|
+
: 'it is part of the public API surface, not the component set';
|
|
1301
|
+
const importName = type.replacedBy ?? name;
|
|
1302
|
+
const deprecation = type.replacedBy
|
|
1303
|
+
? ` \`${name}\` is a deprecated alias — import \`${type.replacedBy}\`.`
|
|
1304
|
+
: '';
|
|
1305
|
+
return {
|
|
1306
|
+
message: `\`${name}\` is a type export, not a component`,
|
|
1307
|
+
hint:
|
|
1308
|
+
`${where}, so pass it as a value instead of writing it as a tag: ` +
|
|
1309
|
+
`\`import type { ${importName} } from 'bitboss-ui'\`.${deprecation} ` +
|
|
1310
|
+
'Do NOT allowlist it — the library owns this name.',
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
const suggestion = nearestName(name, manifest.keys());
|
|
1315
|
+
return {
|
|
1316
|
+
message: `component \`${name}\` is not in the bitboss-ui manifest`,
|
|
1317
|
+
hint:
|
|
1318
|
+
(suggestion ? `Did you mean \`${suggestion}\`? ` : '') +
|
|
1319
|
+
'Check the spelling against dist/ai/index.md Consumer API. If your app owns this component, allowlist it: ' +
|
|
1320
|
+
`\`bitboss-ui.allowComponents: ["${name}"]\` in package.json, or \`--allow-component ${name}\` — ` +
|
|
1321
|
+
'which skips the tag entirely, so no prop, event or slot on it is ever checked again.',
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1129
1325
|
/**
|
|
1130
1326
|
* Validate one `<Bb…>` element node against the manifest surface.
|
|
1131
1327
|
* @param {{ tag: string, props: Array<Record<string, any>> }} node
|
|
@@ -1155,10 +1351,7 @@ function validateElement(node, manifest, findings) {
|
|
|
1155
1351
|
attr: '',
|
|
1156
1352
|
line: tagLine,
|
|
1157
1353
|
kind: 'unknown-prop',
|
|
1158
|
-
|
|
1159
|
-
hint:
|
|
1160
|
-
'Check the spelling against dist/ai/index.md Consumer API. If your app owns this component, allowlist it: ' +
|
|
1161
|
-
`\`bitboss-ui.allowComponents: ["${name}"]\` in package.json, or \`--allow-component ${name}\`.`,
|
|
1354
|
+
...unknownComponentFinding(name, manifest),
|
|
1162
1355
|
});
|
|
1163
1356
|
return;
|
|
1164
1357
|
}
|