bitboss-ui 3.0.0-beta.21 → 3.0.0-beta.22
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/BbBaseRadioIcon.md +6 -10
- package/dist/ai/BbBaseSwitchIcon.md +5 -9
- 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 +13 -23
- package/dist/ai/BbColorPalette.md +23 -35
- package/dist/ai/BbConfirm.md +17 -32
- package/dist/ai/BbDatePickerInput.md +15 -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 +18 -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 +32 -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 +12 -23
- package/dist/ai/BbTextInput.md +20 -26
- package/dist/ai/BbTextarea.md +28 -34
- package/dist/ai/BbToast.md +14 -34
- package/dist/ai/BbTooltip.md +22 -20
- package/dist/ai/BbTree.md +46 -72
- package/dist/ai/changelog.json +169 -62
- package/dist/ai/components.json +1467 -9
- 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 +141 -13
- 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 +253 -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/BbBaseDatePicker.md +5 -5
- package/dist/ai/source/BbBaseDatePickerInput.md +2 -2
- package/dist/ai/source/BbBaseRadio.md +1 -1
- package/dist/ai/source/BbBaseRating.md +5 -5
- package/dist/ai/source/BbBaseSelect.md +1 -1
- package/dist/ai/source/BbBaseSlider.md +5 -5
- package/dist/ai/source/BbBaseSwitch.md +1 -1
- package/dist/ai/source/BbBaseTimePickerInput.md +1 -1
- package/dist/ai/source/BbBreadcrumbs.md +20 -1
- package/dist/ai/source/BbButton.md +2 -2
- 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/BbOffCanvas.md +50 -28
- package/dist/ai/source/BbPagination.md +2 -2
- package/dist/ai/source/BbPopover.md +12 -1
- 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/BbTooltip.md +57 -10
- package/dist/ai/source/ChipsBox.md +1 -1
- package/dist/ai/source/CommaBox.md +1 -1
- 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/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 +1 -1
- package/dist/components/BbBaseInputContainer/BbBaseInputContainer.vue.d.ts +1 -1
- 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 +1 -1
- 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/BbBaseTimePickerInput/BbBaseTimePickerInput.vue_vue_type_script_setup_true_lang.js +1 -1
- 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/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/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/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/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/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/llms-full.txt +1979 -1527
- package/dist/llms-medium.txt +461 -74
- package/dist/llms.txt +1 -1
- 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/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/vite-plugin.d.ts +74 -11
- package/dist/vite.js +292 -253
- 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
package/llms.txt
CHANGED
|
@@ -10,7 +10,7 @@ Note: the `dist/` paths below resolve inside the installed npm package (`node_mo
|
|
|
10
10
|
- [Complete knowledge base](https://llms-bitboss-ui.bitboss.it/dist/llms-full.txt): Everything concatenated (~2 MB). Bulk ingestion only — too large to prompt with.
|
|
11
11
|
- [AI Router](dist/ai/guides/ai-router.md): **Start here** — tiny hop map for any coding agent (which file to open next).
|
|
12
12
|
- [Component index](dist/ai/index.md): Full component catalogue with agent rules, grouped by category. Each entry links a typed contract (props/events/slots/v-model).
|
|
13
|
-
- [Component manifest](dist/ai/components.json): Machine-readable API surface of every component, for programmatic validation.
|
|
13
|
+
- [Component manifest](dist/ai/components.json): Machine-readable API surface of every component, for programmatic validation. Its `types` array lists the public TYPE exports with a `usage` tier (`api` = the ones you reach for) and, on the `api` ones, a `usedBy` array naming the component and prop or event each is the type OF — `BbTableColumn` is `usedBy` `BbTable.columns`, a value you pass, not a tag you write.
|
|
14
14
|
- CLI / MCP: `npx bitboss-ui check` (markup vs components.json), `npx bitboss-ui mcp` (search/get/validate), `npx bitboss-ui ai-init [--mcp]`.
|
|
15
15
|
- [Agent Contract](dist/ai/guides/agent-contract.md): Mandatory rules before writing any component usage code.
|
|
16
16
|
- [Installation & Plugin Setup](dist/ai/guides/installation-and-plugin-setup.md): Canonical installation, style handling, and plugin configuration.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bitboss-ui",
|
|
3
|
-
"version": "3.0.0-beta.
|
|
3
|
+
"version": "3.0.0-beta.22",
|
|
4
4
|
"description": "Vue 3 component library by BitBoss: forms, selects, layout, overlays, and more.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -102,6 +102,7 @@
|
|
|
102
102
|
"@tanstack/vue-virtual": "^3.13.24",
|
|
103
103
|
"@vueuse/core": "^14.3.0",
|
|
104
104
|
"dayjs": "^1.11.20",
|
|
105
|
+
"ignore": "^7.0.9",
|
|
105
106
|
"lodash.deburr": "^4.1.0",
|
|
106
107
|
"lodash.throttle": "^4.1.1",
|
|
107
108
|
"maska": "^3.2.0",
|
|
@@ -148,6 +149,7 @@
|
|
|
148
149
|
"@rollup/plugin-terser": "^1.0.0",
|
|
149
150
|
"@storybook/addon-docs": "^10.4.6",
|
|
150
151
|
"@storybook/vue3-vite": "^10.4.6",
|
|
152
|
+
"@tailwindcss/postcss": "^4.3.3",
|
|
151
153
|
"@types/lodash.deburr": "^4.1.9",
|
|
152
154
|
"@types/lodash.throttle": "^4.1.9",
|
|
153
155
|
"@types/mime": "^4.0.0",
|
|
@@ -161,7 +163,7 @@
|
|
|
161
163
|
"@vitest/browser-playwright": "^4.1.10",
|
|
162
164
|
"@vue/language-core": "~3.1.5",
|
|
163
165
|
"@vue/test-utils": "^2.4.11",
|
|
164
|
-
"autoprefixer": "^10.5.
|
|
166
|
+
"autoprefixer": "^10.5.5",
|
|
165
167
|
"concurrently": "^9.2.1",
|
|
166
168
|
"eslint": "^9.39.4",
|
|
167
169
|
"eslint-config-prettier": "^10.1.8",
|
|
@@ -179,16 +181,16 @@
|
|
|
179
181
|
"playwright": "^1.61.1",
|
|
180
182
|
"postcss": "^8.5.14",
|
|
181
183
|
"postcss-html": "^1.8.1",
|
|
182
|
-
"postcss-
|
|
184
|
+
"postcss-nested": "^8.0.1",
|
|
183
185
|
"postcss-value-parser": "^4.2.0",
|
|
184
186
|
"prettier": "3.8.3",
|
|
185
|
-
"prettier-plugin-tailwindcss": "^0.8.
|
|
187
|
+
"prettier-plugin-tailwindcss": "^0.8.1",
|
|
186
188
|
"query-string": "^9.4.1",
|
|
187
189
|
"rollup-plugin-typescript2": "^0.37.0",
|
|
188
190
|
"storybook": "^10.4.6",
|
|
189
191
|
"stylelint": "^17.11.0",
|
|
190
192
|
"stylelint-order": "^8.1.1",
|
|
191
|
-
"tailwindcss": "^3.
|
|
193
|
+
"tailwindcss": "^4.3.3",
|
|
192
194
|
"typescript": "^6.0.3",
|
|
193
195
|
"vee-validate": "^4.15.1",
|
|
194
196
|
"vite": "^8.0.12",
|
|
@@ -22,6 +22,15 @@ const ROOT = fileURLToPath(new URL('../..', import.meta.url));
|
|
|
22
22
|
/** The one path every validator defaults to. */
|
|
23
23
|
export const MANIFEST_PATH = join(ROOT, 'dist/ai/components.json');
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* `list_changes` reads this, and `generate-docs` does NOT write it — only the
|
|
27
|
+
* full build does, via `scripts/generate-ai-changelog.mjs`. So an unbuilt gate
|
|
28
|
+
* job got a `dist/ai` that looked complete and was not: the MCP server answered
|
|
29
|
+
* `list_changes` with "changelog.json not found", `check:mcp` failed three
|
|
30
|
+
* assertions and then died on the missing release list.
|
|
31
|
+
*/
|
|
32
|
+
export const CHANGELOG_PATH = join(ROOT, 'dist/ai/changelog.json');
|
|
33
|
+
|
|
25
34
|
/**
|
|
26
35
|
* Generate the contract when absent. Cheap (~2.5s) and idempotent, so callers
|
|
27
36
|
* can invoke it unconditionally.
|
|
@@ -30,10 +39,24 @@ export const MANIFEST_PATH = join(ROOT, 'dist/ai/components.json');
|
|
|
30
39
|
* @returns {string} the manifest path.
|
|
31
40
|
*/
|
|
32
41
|
export function ensureManifest(label = 'ensure-manifest') {
|
|
33
|
-
if (existsSync(MANIFEST_PATH))
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
42
|
+
if (!existsSync(MANIFEST_PATH)) {
|
|
43
|
+
console.log(
|
|
44
|
+
`[${label}] dist/ai/components.json missing — running generate-docs…`
|
|
45
|
+
);
|
|
46
|
+
execSync('npm run generate-docs', { cwd: ROOT, stdio: 'inherit' });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Separate check: `generate-docs` writes the contracts but not the
|
|
50
|
+
// changelog, so a tree can have one and not the other.
|
|
51
|
+
if (!existsSync(CHANGELOG_PATH)) {
|
|
52
|
+
console.log(
|
|
53
|
+
`[${label}] dist/ai/changelog.json missing — running generate-ai-changelog…`
|
|
54
|
+
);
|
|
55
|
+
execSync('node scripts/generate-ai-changelog.mjs', {
|
|
56
|
+
cwd: ROOT,
|
|
57
|
+
stdio: 'inherit',
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
38
61
|
return MANIFEST_PATH;
|
|
39
62
|
}
|
|
@@ -28,8 +28,42 @@ export type BitbossUiRuleName =
|
|
|
28
28
|
| 'no-unknown-slots'
|
|
29
29
|
| 'no-active-class-on-root-link'
|
|
30
30
|
| 'no-navigation-attr-fallthrough'
|
|
31
|
+
| 'no-unknown-event'
|
|
32
|
+
| 'no-unknown-token'
|
|
31
33
|
| 'no-reverse-false';
|
|
32
34
|
|
|
35
|
+
/** Options accepted by `bitboss-ui/no-unknown-token`. */
|
|
36
|
+
export interface NoUnknownTokenOptions {
|
|
37
|
+
/**
|
|
38
|
+
* Token names to always permit — for `--bb-*` names you declare in your own
|
|
39
|
+
* stylesheet, which this rule cannot see. The alternative, and usually the
|
|
40
|
+
* better one, is a fallback: `var(--bb-mine, #fff)` is never reported.
|
|
41
|
+
*/
|
|
42
|
+
allowTokens?: string[];
|
|
43
|
+
/**
|
|
44
|
+
* Stylesheet to read the declared token list from. Defaults to the
|
|
45
|
+
* `dist/styles.css` inside the installed package, so a normal install needs
|
|
46
|
+
* no configuration. A path that does not resolve disarms the rule rather
|
|
47
|
+
* than failing: no data, no accusation.
|
|
48
|
+
*/
|
|
49
|
+
stylesPath?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Options accepted by `bitboss-ui/no-unknown-event`. */
|
|
53
|
+
export interface NoUnknownEventOptions {
|
|
54
|
+
/** Component names to skip entirely. */
|
|
55
|
+
allowComponents?: string[];
|
|
56
|
+
/**
|
|
57
|
+
* Event names to always permit, on any component.
|
|
58
|
+
*
|
|
59
|
+
* The escape hatch for a codebase that leans on a specific fallthrough
|
|
60
|
+
* listener on purpose — `['close']` where every dialog wrapper listens for
|
|
61
|
+
* the native `<dialog>` event — without disabling the rule and losing the
|
|
62
|
+
* renamed-event half of it.
|
|
63
|
+
*/
|
|
64
|
+
allowEvents?: string[];
|
|
65
|
+
}
|
|
66
|
+
|
|
33
67
|
/** Options accepted by `bitboss-ui/no-reverse-false`. */
|
|
34
68
|
export interface NoReverseFalseOptions {
|
|
35
69
|
/** Component names to skip entirely. */
|
|
@@ -71,6 +105,7 @@ export const requirePartnerProp: Rule.RuleModule;
|
|
|
71
105
|
export const noUnknownSlots: Rule.RuleModule;
|
|
72
106
|
export const noActiveClassOnRootLink: Rule.RuleModule;
|
|
73
107
|
export const noNavigationAttrFallthrough: Rule.RuleModule;
|
|
108
|
+
export const noUnknownEvent: Rule.RuleModule;
|
|
74
109
|
export const noReverseFalse: Rule.RuleModule;
|
|
75
110
|
|
|
76
111
|
/**
|
|
@@ -50,20 +50,47 @@
|
|
|
50
50
|
* BbBadgeButton), is not an anchor. The attribute renders, inert, and
|
|
51
51
|
* nothing ever warns (Bottega P3-R2, BTG-006: `<BbBadge href>` painted a
|
|
52
52
|
* non-interactive pill).
|
|
53
|
+
* - `no-unknown-event` — an `@event` a Bb* component does not emit. The
|
|
54
|
+
* event twin of `no-navigation-attr-fallthrough`, and the only rule the
|
|
55
|
+
* recommended preset sets to `warn`: an undeclared listener falls through
|
|
56
|
+
* to the root element, so it may be a deliberate native listener rather
|
|
57
|
+
* than a mistake. `<BbDialog @close>` is the case that prompted it — live,
|
|
58
|
+
* because that root is a native `<dialog>`, but outside the contract and
|
|
59
|
+
* unordered against the component's own `hidden`; a consumer's migration
|
|
60
|
+
* report filed it as dead code. `bitboss-ui check` has flagged this since
|
|
61
|
+
* Q10.12 via the shared `checkEvent`; the editor never did, because every
|
|
62
|
+
* rule here read `bind`/`model` and skipped `on`.
|
|
63
|
+
* - `no-unknown-token` — a bare `var(--bb-…)` naming a token the installed
|
|
64
|
+
* package does not declare. The first rule here that reads CSS rather than
|
|
65
|
+
* the template AST, and the first promoted from a `bitboss-ui check`
|
|
66
|
+
* ADVISORY HINT: the same consumer report that filed `no-unknown-event`
|
|
67
|
+
* made the structural point that `check` is what nobody runs while ESLint
|
|
68
|
+
* runs in the editor and in pre-commit. Only this hint crossed over, of the
|
|
69
|
+
* three `check` carries. It is the only one stating a fact rather than a
|
|
70
|
+
* guess — an undefined custom property is invalid-at-computed-value-time,
|
|
71
|
+
* so CSS discards the entire declaration. The hand-rolled avatar and
|
|
72
|
+
* spinner detectors stay in `check` on purpose: they are guesses (a round
|
|
73
|
+
* `<img>` is sometimes a logo), and a rule that fires on correct code
|
|
74
|
+
* spends the trust every other rule here depends on.
|
|
53
75
|
*/
|
|
54
76
|
|
|
55
77
|
import { createRequire } from 'node:module';
|
|
78
|
+
import { dirname, join } from 'node:path';
|
|
79
|
+
import { fileURLToPath } from 'node:url';
|
|
80
|
+
import { findTokenUses, loadKnownTokens } from './hand-roll-hints.mjs';
|
|
56
81
|
import {
|
|
57
82
|
checkNavAttrFallthrough,
|
|
58
83
|
checkPartnerProps,
|
|
59
84
|
createComponentChecker,
|
|
60
85
|
loadManifest,
|
|
61
86
|
NAV_ATTRS,
|
|
87
|
+
unknownComponentFinding,
|
|
62
88
|
PARTNER_PROP_PAIRS,
|
|
63
89
|
toCamel,
|
|
64
90
|
} from './validate-bb-markup.mjs';
|
|
65
91
|
import { matchNameStyle, planAttributeFix } from './migrate-fix.mjs';
|
|
66
92
|
|
|
93
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
67
94
|
const BB_TAG = /^Bb[A-Za-z0-9]+$/;
|
|
68
95
|
/** `bitboss-ui` and its subpath entrypoints (`bitboss-ui/validated`, …). */
|
|
69
96
|
const BITBOSS_IMPORT = /^bitboss-ui(\/|$)/;
|
|
@@ -150,8 +177,15 @@ const noUnknownAttributes = {
|
|
|
150
177
|
},
|
|
151
178
|
],
|
|
152
179
|
messages: {
|
|
153
|
-
|
|
154
|
-
|
|
180
|
+
/*
|
|
181
|
+
* One message with the hint folded in, rather than the bare
|
|
182
|
+
* "not in the manifest" this used to print. The three reasons a `Bb*`
|
|
183
|
+
* tag is unknown want three different answers — a public TYPE written
|
|
184
|
+
* as a tag, a typo, an app-owned component — and the editor is exactly
|
|
185
|
+
* where the reader acts on the answer without opening a terminal. See
|
|
186
|
+
* `unknownComponentFinding`.
|
|
187
|
+
*/
|
|
188
|
+
unknownComponent: '{{message}} — {{hint}}',
|
|
155
189
|
finding: '{{message}}',
|
|
156
190
|
},
|
|
157
191
|
},
|
|
@@ -262,7 +296,7 @@ const noUnknownAttributes = {
|
|
|
262
296
|
context.report({
|
|
263
297
|
node: node.startTag,
|
|
264
298
|
messageId: 'unknownComponent',
|
|
265
|
-
data:
|
|
299
|
+
data: unknownComponentFinding(name, manifest),
|
|
266
300
|
});
|
|
267
301
|
return;
|
|
268
302
|
}
|
|
@@ -919,6 +953,105 @@ const noNavigationAttrFallthrough = {
|
|
|
919
953
|
},
|
|
920
954
|
};
|
|
921
955
|
|
|
956
|
+
/**
|
|
957
|
+
* An `@event` on a `Bb*` component that names nothing the component emits.
|
|
958
|
+
*
|
|
959
|
+
* The check itself is not new — `createComponentChecker`'s `checkEvent` has
|
|
960
|
+
* driven it in `bitboss-ui check` and the MCP `validate` tool since Q10.12.
|
|
961
|
+
* What was new is that the EDITOR never ran it: every rule in this plugin read
|
|
962
|
+
* `bind` and `model` directives and skipped `on` entirely, so the one gate a
|
|
963
|
+
* consuming agent actually works against was the one gate blind to it.
|
|
964
|
+
*
|
|
965
|
+
* That is how `<WhatsAppConnectedCard @close>` shipped. The migration report
|
|
966
|
+
* that found it called the handler dead code; it was not. BbDialog's root is a
|
|
967
|
+
* native `<dialog>`, so the listener bound as a real DOM listener and fired.
|
|
968
|
+
* `close` is in `ELEMENT_DOM_EVENTS` for that reason, and `checkEvent` returns
|
|
969
|
+
* it as a `fallthrough` finding rather than an unknown one.
|
|
970
|
+
*
|
|
971
|
+
* Reported at `warn` in the recommended preset — alone among these rules, all
|
|
972
|
+
* of which are `error`. That asymmetry is the point. An unknown prop is a typo
|
|
973
|
+
* with one correct fix. An undeclared event listener may be a deliberate native
|
|
974
|
+
* listener on a root the author knows, so this rule flags the contract, not a
|
|
975
|
+
* mistake, and leaves `eslint-disable-next-line` as the answer for anyone who
|
|
976
|
+
* meant it.
|
|
977
|
+
* @type {import('eslint').Rule.RuleModule}
|
|
978
|
+
*/
|
|
979
|
+
const noUnknownEvent = {
|
|
980
|
+
meta: {
|
|
981
|
+
type: 'problem',
|
|
982
|
+
docs: {
|
|
983
|
+
description:
|
|
984
|
+
"Flag an `@event` on a Bb* component that the component does not emit. Vue passes an undeclared listener through to the root element, so the markup compiles clean and `vue-tsc` stays quiet outside `strictTemplates`: a renamed event (beta.9's `resize` → `resize:column`) reads as working code, and an element-specific DOM event like `@close` binds to whatever the root happens to be — live on BbDialog, whose root is a native `<dialog>`, inert on BbAlert. A third case is neither: `@success` on BbButton binds the declared `onSuccess` PROP, so it runs, and the rule points at the documented `:on-success` spelling instead. Universal DOM events (`@click`, `@keydown`) are always allowed.",
|
|
985
|
+
recommended: true,
|
|
986
|
+
},
|
|
987
|
+
schema: [
|
|
988
|
+
{
|
|
989
|
+
type: 'object',
|
|
990
|
+
properties: {
|
|
991
|
+
manifestPath: { type: 'string' },
|
|
992
|
+
failClosed: { type: 'boolean' },
|
|
993
|
+
allowComponents: { type: 'array', items: { type: 'string' } },
|
|
994
|
+
allowEvents: { type: 'array', items: { type: 'string' } },
|
|
995
|
+
},
|
|
996
|
+
additionalProperties: false,
|
|
997
|
+
},
|
|
998
|
+
],
|
|
999
|
+
messages: {
|
|
1000
|
+
// Two ids, not one: the fallthrough case is a different claim about
|
|
1001
|
+
// the code, and a consumer suppressing one should not lose the other.
|
|
1002
|
+
unknownEvent: '{{message}} — {{hint}}',
|
|
1003
|
+
fallthroughEvent: '{{message}}. {{hint}}',
|
|
1004
|
+
},
|
|
1005
|
+
},
|
|
1006
|
+
|
|
1007
|
+
create(context) {
|
|
1008
|
+
const options = context.options[0] ?? {};
|
|
1009
|
+
const manifest = getManifest(options.manifestPath, options);
|
|
1010
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
1011
|
+
const services = sourceCode.parserServices;
|
|
1012
|
+
if (!manifest || !services?.defineTemplateBodyVisitor) return {};
|
|
1013
|
+
|
|
1014
|
+
const allowComps = new Set(options.allowComponents ?? []);
|
|
1015
|
+
const allowEvents = new Set(options.allowEvents ?? []);
|
|
1016
|
+
|
|
1017
|
+
return services.defineTemplateBodyVisitor({
|
|
1018
|
+
VElement(node) {
|
|
1019
|
+
const name = toPascalTag(node.rawName);
|
|
1020
|
+
if (!BB_TAG.test(name) || allowComps.has(name)) return;
|
|
1021
|
+
const surface = manifest.get(name);
|
|
1022
|
+
if (!surface) return; // unknown component — no-unknown-attributes owns this
|
|
1023
|
+
|
|
1024
|
+
const checker = createComponentChecker(surface);
|
|
1025
|
+
|
|
1026
|
+
for (const attr of node.startTag.attributes) {
|
|
1027
|
+
if (!attr.directive || attr.key.name.name !== 'on') continue;
|
|
1028
|
+
const arg = attr.key.argument;
|
|
1029
|
+
// `v-on="obj"` (no arg) and `@[expr]` (dynamic) are unbounded.
|
|
1030
|
+
if (!arg || arg.type !== 'VIdentifier') continue;
|
|
1031
|
+
|
|
1032
|
+
// `vue-eslint-parser` splits `@click.stop` into an argument plus
|
|
1033
|
+
// modifiers, so the raw name here is already bare — unlike the
|
|
1034
|
+
// compiler AST `checkEvent`'s other caller walks, which keeps the
|
|
1035
|
+
// dots. `checkEvent` strips them either way; nothing to do here.
|
|
1036
|
+
const rawName = arg.rawName ?? arg.name;
|
|
1037
|
+
if (allowEvents.has(rawName)) continue;
|
|
1038
|
+
|
|
1039
|
+
const finding = checker.checkEvent(rawName);
|
|
1040
|
+
if (!finding) continue;
|
|
1041
|
+
|
|
1042
|
+
context.report({
|
|
1043
|
+
node: attr.key,
|
|
1044
|
+
messageId: finding.fallthrough
|
|
1045
|
+
? 'fallthroughEvent'
|
|
1046
|
+
: 'unknownEvent',
|
|
1047
|
+
data: { message: finding.message, hint: finding.hint ?? '' },
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
},
|
|
1051
|
+
});
|
|
1052
|
+
},
|
|
1053
|
+
};
|
|
1054
|
+
|
|
922
1055
|
/*
|
|
923
1056
|
* The three v3 controls whose `reverse` prop kept its NAME but inverted its
|
|
924
1057
|
* MEANING between v2 and v3. Hardcoded on purpose: this is a fact about one
|
|
@@ -1062,6 +1195,151 @@ const noReverseFalse = {
|
|
|
1062
1195
|
},
|
|
1063
1196
|
};
|
|
1064
1197
|
|
|
1198
|
+
/*
|
|
1199
|
+
* Every `--bb-*` the installed package declares, keyed by resolved stylesheet
|
|
1200
|
+
* path. Same caching shape as `manifestCache`, including the miss: an absent
|
|
1201
|
+
* stylesheet caches an EMPTY set, and the rule reads that as "say nothing".
|
|
1202
|
+
*
|
|
1203
|
+
* No warning on a miss, unlike the manifest. `dist/styles.css` is a BUILD
|
|
1204
|
+
* artifact, absent in a source checkout that has not run `vite build`, so a
|
|
1205
|
+
* console warning here would fire constantly in this repo's own dev loop.
|
|
1206
|
+
* A missing manifest means every rule is silently inactive, which is worth
|
|
1207
|
+
* shouting about; a missing stylesheet disarms exactly one advisory rule.
|
|
1208
|
+
*/
|
|
1209
|
+
const tokenCache = new Map();
|
|
1210
|
+
function getKnownTokens(stylesPath) {
|
|
1211
|
+
const resolved = stylesPath ?? join(HERE, '..', '..', 'dist', 'styles.css');
|
|
1212
|
+
if (!tokenCache.has(resolved))
|
|
1213
|
+
tokenCache.set(resolved, loadKnownTokens(resolved));
|
|
1214
|
+
return tokenCache.get(resolved);
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
/**
|
|
1218
|
+
* A bare `var(--bb-…)` in a `<style>` block naming a token the installed
|
|
1219
|
+
* package does not declare.
|
|
1220
|
+
*
|
|
1221
|
+
* WHY THIS ONE MOVED TO ESLINT AND ITS TWO SIBLINGS DID NOT. `bitboss-ui
|
|
1222
|
+
* check` has carried three advisory hints — this, plus the hand-rolled avatar
|
|
1223
|
+
* and spinner detectors. A consumer's migration report (2026-09-09) made the
|
|
1224
|
+
* structural point that `check` is what nobody runs, while ESLint runs in the
|
|
1225
|
+
* editor and in pre-commit, so a check-only warning does not really exist.
|
|
1226
|
+
*
|
|
1227
|
+
* Only this one crossed over, on the owner's ruling, because only this one is
|
|
1228
|
+
* a FACT rather than a guess. An undeclared custom property is
|
|
1229
|
+
* invalid-at-computed-value-time: CSS discards the WHOLE declaration, so the
|
|
1230
|
+
* rule silently does nothing — there is no reading of `var(--bb-canvas)`, with
|
|
1231
|
+
* no such token and no fallback, that is correct code. The avatar and spinner
|
|
1232
|
+
* detectors are guesses by design (a round `<img>` is sometimes a logo), and a
|
|
1233
|
+
* rule that fires on correct code spends the trust that makes every other rule
|
|
1234
|
+
* here worth reading. They stay in `check`, where they cannot fail a build.
|
|
1235
|
+
*
|
|
1236
|
+
* A FALLBACK DISARMS IT, exactly as in the CLI hint: `var(--bb-canvas, #f8fafc)`
|
|
1237
|
+
* is a deliberate override hook with defined behaviour and nothing silently
|
|
1238
|
+
* disappears. Shared with the CLI through `findTokenUses`, so the two can never
|
|
1239
|
+
* disagree about what counts as a use.
|
|
1240
|
+
*
|
|
1241
|
+
* Reported at `warn`, joining `no-unknown-event`: the token may be one the
|
|
1242
|
+
* consumer means to define in their own stylesheet, which this rule cannot see.
|
|
1243
|
+
* @type {import('eslint').Rule.RuleModule}
|
|
1244
|
+
*/
|
|
1245
|
+
const noUnknownToken = {
|
|
1246
|
+
meta: {
|
|
1247
|
+
type: 'problem',
|
|
1248
|
+
docs: {
|
|
1249
|
+
description:
|
|
1250
|
+
'Flag a bare `var(--bb-…)` naming a token bitboss-ui does not declare. CSS treats an undefined custom property as invalid-at-computed-value-time and throws away the entire declaration, so the rule silently does nothing rather than falling back — an invented token name is invisible at runtime and in review. Supplying a fallback (`var(--bb-x, #fff)`) is defined behaviour and never reported. Reads the token list from the installed `dist/styles.css`, so it can never claim a token is invented when your version declares it; with no stylesheet to read, the rule stays silent.',
|
|
1251
|
+
recommended: true,
|
|
1252
|
+
},
|
|
1253
|
+
schema: [
|
|
1254
|
+
{
|
|
1255
|
+
type: 'object',
|
|
1256
|
+
properties: {
|
|
1257
|
+
stylesPath: { type: 'string' },
|
|
1258
|
+
allowTokens: { type: 'array', items: { type: 'string' } },
|
|
1259
|
+
},
|
|
1260
|
+
additionalProperties: false,
|
|
1261
|
+
},
|
|
1262
|
+
],
|
|
1263
|
+
messages: {
|
|
1264
|
+
unknownToken:
|
|
1265
|
+
'`{{name}}` is not a bitboss-ui token. An undefined custom property invalidates the whole declaration, so this rule silently does nothing at runtime. Check the token list in the design-tokens guide, or supply a fallback (`var({{name}}, …)`) if you define it yourself.',
|
|
1266
|
+
},
|
|
1267
|
+
},
|
|
1268
|
+
|
|
1269
|
+
create(context) {
|
|
1270
|
+
const options = context.options[0] ?? {};
|
|
1271
|
+
const known = getKnownTokens(options.stylesPath);
|
|
1272
|
+
// No stylesheet to compare against — no data, no accusation.
|
|
1273
|
+
if (known.size === 0) return {};
|
|
1274
|
+
|
|
1275
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode();
|
|
1276
|
+
const services = sourceCode.parserServices;
|
|
1277
|
+
if (!services?.getDocumentFragment) return {};
|
|
1278
|
+
const allowTokens = new Set(options.allowTokens ?? []);
|
|
1279
|
+
|
|
1280
|
+
/**
|
|
1281
|
+
* Report every unknown token in one chunk of CSS text. `offset` is that
|
|
1282
|
+
* chunk's own start in the FILE, so a match position inside it maps
|
|
1283
|
+
* straight onto a source location.
|
|
1284
|
+
*/
|
|
1285
|
+
const reportIn = (css, offset) => {
|
|
1286
|
+
for (const use of findTokenUses(css)) {
|
|
1287
|
+
if (known.has(use.name) || allowTokens.has(use.name)) continue;
|
|
1288
|
+
const start = offset + use.index;
|
|
1289
|
+
context.report({
|
|
1290
|
+
loc: {
|
|
1291
|
+
start: sourceCode.getLocFromIndex(start),
|
|
1292
|
+
// `var(` + the name, which is the part worth underlining.
|
|
1293
|
+
end: sourceCode.getLocFromIndex(start + use.name.length + 4),
|
|
1294
|
+
},
|
|
1295
|
+
messageId: 'unknownToken',
|
|
1296
|
+
data: { name: use.name },
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
};
|
|
1300
|
+
|
|
1301
|
+
/*
|
|
1302
|
+
* The two places real CSS lives in an SFC: a `<style>` block, and a
|
|
1303
|
+
* static `style="…"` attribute. Both are scanned, because `check` reads
|
|
1304
|
+
* the whole file and a rule that saw only one of them would put the two
|
|
1305
|
+
* tools back into the disagreement this rule exists to end.
|
|
1306
|
+
*
|
|
1307
|
+
* A BOUND `:style` is deliberately NOT scanned: it is a JS expression,
|
|
1308
|
+
* the token name may be computed, and guessing at string literals inside
|
|
1309
|
+
* it is how a rule starts firing on correct code.
|
|
1310
|
+
*/
|
|
1311
|
+
const walk = (node) => {
|
|
1312
|
+
if (!node || node.type !== 'VElement') return;
|
|
1313
|
+
|
|
1314
|
+
if (node.rawName === 'style') {
|
|
1315
|
+
for (const text of node.children ?? [])
|
|
1316
|
+
if (text.type === 'VText' && typeof text.value === 'string')
|
|
1317
|
+
reportIn(text.value, text.range[0]);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
for (const attr of node.startTag?.attributes ?? []) {
|
|
1321
|
+
if (attr.directive || attr.key?.name !== 'style') continue;
|
|
1322
|
+
const value = attr.value;
|
|
1323
|
+
if (!value || typeof value.value !== 'string') continue;
|
|
1324
|
+
// `range` spans the quotes; the value starts one character in.
|
|
1325
|
+
reportIn(value.value, value.range[0] + 1);
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
for (const child of node.children ?? []) walk(child);
|
|
1329
|
+
};
|
|
1330
|
+
|
|
1331
|
+
return {
|
|
1332
|
+
// The template-body visitors every sibling rule uses cannot see a
|
|
1333
|
+
// `<style>` block; the document fragment is the whole SFC.
|
|
1334
|
+
'Program:exit'() {
|
|
1335
|
+
const fragment = services.getDocumentFragment();
|
|
1336
|
+
if (!fragment) return;
|
|
1337
|
+
for (const child of fragment.children ?? []) walk(child);
|
|
1338
|
+
},
|
|
1339
|
+
};
|
|
1340
|
+
},
|
|
1341
|
+
};
|
|
1342
|
+
|
|
1065
1343
|
const plugin = {
|
|
1066
1344
|
meta: { name: 'bitboss-ui', version: '1.0.0' },
|
|
1067
1345
|
rules: {
|
|
@@ -1071,6 +1349,8 @@ const plugin = {
|
|
|
1071
1349
|
'no-unknown-slots': noUnknownSlots,
|
|
1072
1350
|
'no-active-class-on-root-link': noActiveClassOnRootLink,
|
|
1073
1351
|
'no-navigation-attr-fallthrough': noNavigationAttrFallthrough,
|
|
1352
|
+
'no-unknown-event': noUnknownEvent,
|
|
1353
|
+
'no-unknown-token': noUnknownToken,
|
|
1074
1354
|
'no-reverse-false': noReverseFalse,
|
|
1075
1355
|
},
|
|
1076
1356
|
};
|
|
@@ -1130,6 +1410,20 @@ plugin.configs = {
|
|
|
1130
1410
|
'bitboss-ui/no-unknown-slots': 'error',
|
|
1131
1411
|
'bitboss-ui/no-active-class-on-root-link': 'error',
|
|
1132
1412
|
'bitboss-ui/no-navigation-attr-fallthrough': 'error',
|
|
1413
|
+
// The only `warn` in this preset, deliberately. Every other rule
|
|
1414
|
+
// here describes markup with exactly one correct fix. An
|
|
1415
|
+
// undeclared event listener does not: it may be a considered
|
|
1416
|
+
// native listener on a root the author knows, and erroring on a
|
|
1417
|
+
// real JS event would make the plugin wrong about working code.
|
|
1418
|
+
// Warning states the contract and leaves the choice; the CLI
|
|
1419
|
+
// (`bitboss-ui check`) still reports it the same way it always
|
|
1420
|
+
// has, so nothing silently relaxes in CI.
|
|
1421
|
+
'bitboss-ui/no-unknown-event': 'warn',
|
|
1422
|
+
// Also `warn`, for a different reason: the fact is certain (the
|
|
1423
|
+
// declaration is discarded), but the token may be one the consumer
|
|
1424
|
+
// intends to define in their own stylesheet, which this rule cannot
|
|
1425
|
+
// see. `var(--bb-x, fallback)` is the documented way to say so.
|
|
1426
|
+
'bitboss-ui/no-unknown-token': 'warn',
|
|
1133
1427
|
'bitboss-ui/no-reverse-false': 'error',
|
|
1134
1428
|
},
|
|
1135
1429
|
},
|
|
@@ -1144,6 +1438,8 @@ export {
|
|
|
1144
1438
|
noUnknownSlots,
|
|
1145
1439
|
noActiveClassOnRootLink,
|
|
1146
1440
|
noNavigationAttrFallthrough,
|
|
1441
|
+
noUnknownEvent,
|
|
1442
|
+
noUnknownToken,
|
|
1147
1443
|
noReverseFalse,
|
|
1148
1444
|
resolveOptionalTsParser,
|
|
1149
1445
|
};
|
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
* Hinting about someone else's composable is not this file's job.
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
30
|
+
|
|
29
31
|
/**
|
|
30
32
|
* @typedef {{ line: number, component: string, message: string }} Hint
|
|
31
33
|
*/
|
|
@@ -77,6 +79,53 @@ function detectSpinner(source) {
|
|
|
77
79
|
};
|
|
78
80
|
}
|
|
79
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Every bare `var(--bb-…)` USE in the source, with the offset it starts at.
|
|
84
|
+
*
|
|
85
|
+
* Split out from `detectUnknownTokens` so the ESLint rule
|
|
86
|
+
* (`no-unknown-token`) reports each occurrence at its own location instead of
|
|
87
|
+
* lumping them onto one line the way the CLI hint does. One regex, one
|
|
88
|
+
* fallback rule, two consumers — the two can no longer disagree about what
|
|
89
|
+
* counts as a use.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} source
|
|
92
|
+
* @returns {{ name: string, index: number }[]}
|
|
93
|
+
*/
|
|
94
|
+
export function findTokenUses(source) {
|
|
95
|
+
return (
|
|
96
|
+
[...source.matchAll(/var\(\s*(--bb-[a-z0-9-]+)\s*([,)])/gi)]
|
|
97
|
+
// `,` opens a fallback; `)` closes the function with none.
|
|
98
|
+
.filter((m) => m[2] === ')')
|
|
99
|
+
.map((m) => ({ name: m[1].toLowerCase(), index: m.index }))
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Every `--bb-*` the INSTALLED package declares, read from the stylesheet the
|
|
105
|
+
* consumer actually ships rather than hardcoded, so this can never claim a
|
|
106
|
+
* token is invented when their version does declare it.
|
|
107
|
+
*
|
|
108
|
+
* A missing stylesheet yields an EMPTY set, and every caller treats that as
|
|
109
|
+
* "say nothing". No data, no accusation.
|
|
110
|
+
*
|
|
111
|
+
* @param {string} stylesPath path to the installed `dist/styles.css`
|
|
112
|
+
* @returns {Set<string>}
|
|
113
|
+
*/
|
|
114
|
+
export function loadKnownTokens(stylesPath) {
|
|
115
|
+
if (!stylesPath || !existsSync(stylesPath)) return new Set();
|
|
116
|
+
try {
|
|
117
|
+
return new Set(
|
|
118
|
+
[
|
|
119
|
+
...readFileSync(stylesPath, 'utf-8').matchAll(
|
|
120
|
+
/(--bb-[a-z0-9-]+)\s*:/gi
|
|
121
|
+
),
|
|
122
|
+
].map((m) => m[1].toLowerCase())
|
|
123
|
+
);
|
|
124
|
+
} catch {
|
|
125
|
+
return new Set();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
80
129
|
/**
|
|
81
130
|
* A `var(--bb-…)` name the library does not declare. CSS treats an undefined
|
|
82
131
|
* custom property as invalid-at-computed-value-time and throws away the WHOLE
|
|
@@ -99,12 +148,7 @@ function detectSpinner(source) {
|
|
|
99
148
|
*/
|
|
100
149
|
function detectUnknownTokens(source, knownTokens) {
|
|
101
150
|
if (!knownTokens || knownTokens.size === 0) return null;
|
|
102
|
-
const used = new Set(
|
|
103
|
-
[...source.matchAll(/var\(\s*(--bb-[a-z0-9-]+)\s*([,)])/gi)]
|
|
104
|
-
// `,` opens a fallback; `)` closes the function with none.
|
|
105
|
-
.filter((m) => m[2] === ')')
|
|
106
|
-
.map((m) => m[1].toLowerCase())
|
|
107
|
-
);
|
|
151
|
+
const used = new Set(findTokenUses(source).map((u) => u.name));
|
|
108
152
|
const unknown = [...used].filter((name) => !knownTokens.has(name));
|
|
109
153
|
if (unknown.length === 0) return null;
|
|
110
154
|
return {
|
|
@@ -280,3 +280,45 @@ export const NATIVE_DOM_EVENTS = new Set([
|
|
|
280
280
|
'transitionrun',
|
|
281
281
|
'transitionstart',
|
|
282
282
|
]);
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Real DOM events that fire on only a HANDFUL of element types, so they are
|
|
286
|
+
* deliberately NOT in `NATIVE_DOM_EVENTS` above.
|
|
287
|
+
*
|
|
288
|
+
* These are the reason the event gate can't simply say "unknown event". A
|
|
289
|
+
* listener named here is not a typo and it is not dead code: Vue binds it to
|
|
290
|
+
* whatever the component's root element happens to be, and if that root is the
|
|
291
|
+
* one element that dispatches it, the handler really does run.
|
|
292
|
+
*
|
|
293
|
+
* `<BbDialog @close>` is the case that named this set. BbDialog's root IS a
|
|
294
|
+
* native `<dialog>`, so `@close` binds as a genuine DOM listener and fires on
|
|
295
|
+
* `dialog.close()` — a task queued by the browser, unordered against the
|
|
296
|
+
* component's own `hidden` emit, and dispatched for native closes that never
|
|
297
|
+
* touched `v-model` (a `<form method="dialog">` submit) so the model goes
|
|
298
|
+
* stale. A consuming agent's migration report read it as "a dead `@close`
|
|
299
|
+
* handler shipped"; it was live, mistimed, and outside the component contract.
|
|
300
|
+
*
|
|
301
|
+
* Blanket-allowing them (moving them up into `NATIVE_DOM_EVENTS`) would hide
|
|
302
|
+
* that. Treating them as unknown would call working markup a typo. So they get
|
|
303
|
+
* their own diagnostic — see `checkEvent`'s `fallthrough` finding — and, in
|
|
304
|
+
* ESLint, their own severity.
|
|
305
|
+
*/
|
|
306
|
+
export const ELEMENT_DOM_EVENTS = new Set([
|
|
307
|
+
// <dialog>
|
|
308
|
+
'cancel',
|
|
309
|
+
'close',
|
|
310
|
+
// <details>, <dialog>, popover
|
|
311
|
+
'beforetoggle',
|
|
312
|
+
// <form>
|
|
313
|
+
'formdata',
|
|
314
|
+
// media elements
|
|
315
|
+
'canplay',
|
|
316
|
+
'ended',
|
|
317
|
+
'loadeddata',
|
|
318
|
+
'loadedmetadata',
|
|
319
|
+
'pause',
|
|
320
|
+
'play',
|
|
321
|
+
'playing',
|
|
322
|
+
'timeupdate',
|
|
323
|
+
'volumechange',
|
|
324
|
+
]);
|