diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 140b59bf0..1fa402b81 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -59,9 +59,29 @@ jobs:
- name: Build
run: pnpm run build
+ # Do not fold this into the step below on the grounds that it checks a
+ # subset. It does — `tsconfig.test.json` extends the build config and
+ # REPLACES its include/exclude rather than merging, so every file here is
+ # also in that project and none is unique to this one.
+ # What differs is scope, not coverage: only that project pulls in
+ # `types/assets.d.ts`, whose `declare module '*.svg'` would mask a missing
+ # declaration in shipped source. This step is the assertion that the
+ # published program type-checks ON ITS OWN, with no test-only ambient
+ # declaration propping it up — the same separation that keeps tests and
+ # stories out of the declaration emit.
+ #
+ # It is also load-bearing in a way the `build` step above is not:
+ # @rollup/plugin-typescript reports semantic errors as WARNINGS, so
+ # `pnpm build` succeeds on source that does not type-check. These two
+ # steps are the only thing between a type error and a published package.
- name: Typecheck
run: pnpm run typecheck
+ # The tests and the stories, which `typecheck` excludes because that
+ # program is also the build's. See bulma-ui/tsconfig.test.json.
+ - name: Typecheck tests and stories
+ run: pnpm run typecheck:tests
+
- name: Test
run: pnpm run test
@@ -177,9 +197,21 @@ jobs:
- name: Build bulma-ui
run: pnpm --filter @allxsmith/bestax-bulma run build
+ # Both projects, for the reason the build-and-test job states: the second
+ # is a strict superset of the first in FILES, but only the first checks
+ # the published program without `types/assets.d.ts` in scope.
- name: Typecheck bulma-ui
run: pnpm --filter @allxsmith/bestax-bulma run typecheck
+ # `pin-react.mjs` pins react/react-dom and never @types/react, so today
+ # both legs read one set of types and this adds no signal the
+ # single-version job lacks — but that is equally true of the step above,
+ # and running one without the other reads as an oversight. If the types
+ # are ever pinned too, this is the leg that would catch a React-18-only
+ # break in a story or a test.
+ - name: Typecheck bulma-ui tests and stories
+ run: pnpm --filter @allxsmith/bestax-bulma run typecheck:tests
+
- name: Test bulma-ui
run: pnpm --filter @allxsmith/bestax-bulma run test
diff --git a/CLAUDE.md b/CLAUDE.md
index f187273cb..dd0066623 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -33,7 +33,8 @@ pnpm all # the pre-PR gate: build, typecheck, test+coverage, bundle:s
pnpm test # jest (bulma-ui + create-bestax + bestax-migrate + bestax-mcp)
pnpm test:coverage # coverage — thresholds live in each package's jest config (see below)
pnpm lint # eslint
-pnpm typecheck # tsc --noEmit
+pnpm typecheck # tsc --noEmit (each package's build program)
+pnpm typecheck:tests # tsc over bulma-ui's tests and stories, which `typecheck` excludes
pnpm format # prettier --write (format:check to verify; covers md/mdx too)
pnpm gen:catalog # regenerate the skills component catalog (CI fails if stale)
pnpm gen:mcp # regenerate the MCP server's data index (CI fails if stale)
@@ -56,6 +57,21 @@ Enforced by CI (`.github/workflows/ci.yml`):
every other jest package 95% (78% branches). `docs` has no jest suite.
- Stale skill catalog fails (`gen:catalog:check`) and a stale MCP index fails
(`gen:mcp:check`); build, typecheck, lint, format, audit.
+- **Tests and stories are type-checked too**, by a second project
+ (`bulma-ui/tsconfig.test.json`, script `typecheck:tests`) — `tsconfig.json` excludes
+ them because it is also the build's program. ts-jest transpiles rather than checks
+ (the repo sets `isolatedModules`), so jest never stands in for this. Two consequences
+ worth knowing: an `@ts-expect-error` in a test or story is live and fails as TS2578
+ once it stops being needed, and in JSX the directive must sit in its OWN single-line
+ `{/* … */}` — TypeScript anchors it to the line a comment STARTS on, so a multi-line
+ one suppresses nothing (#663).
+- **`pnpm build` is not a type gate.** `@rollup/plugin-typescript` reports semantic errors
+ as warnings, so the build succeeds on source that does not type-check. The two `typecheck`
+ scripts are what stands between a type error and a published package, which is why both
+ run in CI even though one reads a subset of the other's files: `tsconfig.test.json`
+ extends the build config and REPLACES its include/exclude rather than merging, so only the
+ narrower `typecheck` checks the shipped program without `types/assets.d.ts` — and its
+ `declare module '*.svg'` — in scope.
- House conventions fail via `pnpm check:conformance` (error messages name the file and fix);
a **React 18/19 matrix** builds and tests bulma-ui on both majors.
diff --git a/bestax-mcp/data/components/Dropdown.json b/bestax-mcp/data/components/Dropdown.json
index ffd681aa9..3c0a8dbfd 100644
--- a/bestax-mcp/data/components/Dropdown.json
+++ b/bestax-mcp/data/components/Dropdown.json
@@ -179,6 +179,15 @@
"summary": "Bulma Dropdown item.",
"component": null,
"props": [
+ {
+ "name": "as",
+ "type": "'a' | 'div' | 'button'",
+ "default": "'a'",
+ "description": "The element type to render.",
+ "required": false,
+ "inherited": false,
+ "deprecated": false
+ },
{
"name": "active",
"type": "boolean",
@@ -197,15 +206,6 @@
"inherited": false,
"deprecated": false
},
- {
- "name": "as",
- "type": "'a' | 'div' | 'button'",
- "default": "'a'",
- "description": "The element type to render.",
- "required": false,
- "inherited": false,
- "deprecated": false
- },
{
"name": "disabled",
"type": "boolean",
@@ -226,7 +226,7 @@
}
],
"extraProps": [],
- "catchAll": "All standard HTML attributes and Bulma helper props",
+ "catchAll": "Remaining props of the element selected by as and Bulma helper props",
"types": []
},
{
diff --git a/bestax-mcp/data/components/Icon.json b/bestax-mcp/data/components/Icon.json
index e40db52be..89ec3936c 100644
--- a/bestax-mcp/data/components/Icon.json
+++ b/bestax-mcp/data/components/Icon.json
@@ -85,6 +85,16 @@
"inherited": true,
"deprecated": false
},
+ {
+ "name": "icon",
+ "type": "string",
+ "default": null,
+ "description": "Legacy icon class string (e.g. `'fas fa-star'`). Only its last segment is read, as the glyph name; the library still comes from `library` or `ConfigProvider`, so `'mdi mdi-rocket'` renders an `fa` class unless the effective library is already `mdi` — set it here or on the provider.",
+ "required": false,
+ "inherited": true,
+ "deprecated": true,
+ "deprecationNote": "Use `name` instead."
+ },
{
"name": "className",
"type": "string",
@@ -123,15 +133,6 @@
"deprecated": false,
"valuesRef": "helpers/valid-values"
},
- {
- "name": "icon",
- "type": "string",
- "default": null,
- "description": "DEPRECATED: Legacy prop, use `name` instead.",
- "required": false,
- "inherited": true,
- "deprecated": false
- },
{
"name": "library",
"type": "'fa' | 'mdi' | 'ion' | 'material-icons' | 'material-symbols'",
@@ -163,10 +164,11 @@
"name": "libraryFeatures",
"type": "string | string[]",
"default": null,
- "description": "**DEPRECATED:** Use `variant` and `features` instead.",
+ "description": "Additional modifiers in the older combined form, parsed into `variant` and `features`.",
"required": false,
"inherited": true,
- "deprecated": false
+ "deprecated": true,
+ "deprecationNote": "Use `variant` and `features` instead."
},
{
"name": "size",
diff --git a/bestax-mcp/data/components/IconText.json b/bestax-mcp/data/components/IconText.json
index 2fc1bd827..ece4f5d5c 100644
--- a/bestax-mcp/data/components/IconText.json
+++ b/bestax-mcp/data/components/IconText.json
@@ -156,6 +156,16 @@
"inherited": true,
"deprecated": false
},
+ {
+ "name": "icon",
+ "type": "string",
+ "default": null,
+ "description": "Legacy icon class string (e.g. `'fas fa-star'`). Only its last segment is read, as the glyph name; the library still comes from `library` or `ConfigProvider`, so `'mdi mdi-rocket'` renders an `fa` class unless the effective library is already `mdi` — set it here or on the provider.",
+ "required": false,
+ "inherited": true,
+ "deprecated": true,
+ "deprecationNote": "Use `name` instead."
+ },
{
"name": "className",
"type": "string",
@@ -194,15 +204,6 @@
"deprecated": false,
"valuesRef": "helpers/valid-values"
},
- {
- "name": "icon",
- "type": "string",
- "default": null,
- "description": "DEPRECATED: Legacy prop, use `name` instead.",
- "required": false,
- "inherited": true,
- "deprecated": false
- },
{
"name": "library",
"type": "'fa' | 'mdi' | 'ion' | 'material-icons' | 'material-symbols'",
@@ -234,10 +235,11 @@
"name": "libraryFeatures",
"type": "string | string[]",
"default": null,
- "description": "**DEPRECATED:** Use `variant` and `features` instead.",
+ "description": "Additional modifiers in the older combined form, parsed into `variant` and `features`.",
"required": false,
"inherited": true,
- "deprecated": false
+ "deprecated": true,
+ "deprecationNote": "Use `variant` and `features` instead."
},
{
"name": "size",
diff --git a/bestax-mcp/data/components/Panel.json b/bestax-mcp/data/components/Panel.json
index 5dfcda03d..3bb78e471 100644
--- a/bestax-mcp/data/components/Panel.json
+++ b/bestax-mcp/data/components/Panel.json
@@ -179,6 +179,16 @@
"inherited": true,
"deprecated": false
},
+ {
+ "name": "icon",
+ "type": "string",
+ "default": null,
+ "description": "Legacy icon class string (e.g. `'fas fa-star'`). Only its last segment is read, as the glyph name; the library still comes from `library` or `ConfigProvider`, so `'mdi mdi-rocket'` renders an `fa` class unless the effective library is already `mdi` — set it here or on the provider.",
+ "required": false,
+ "inherited": true,
+ "deprecated": true,
+ "deprecationNote": "Use `name` instead."
+ },
{
"name": "className",
"type": "string",
@@ -217,15 +227,6 @@
"deprecated": false,
"valuesRef": "helpers/valid-values"
},
- {
- "name": "icon",
- "type": "string",
- "default": null,
- "description": "DEPRECATED: Legacy prop, use `name` instead.",
- "required": false,
- "inherited": true,
- "deprecated": false
- },
{
"name": "library",
"type": "'fa' | 'mdi' | 'ion' | 'material-icons' | 'material-symbols'",
@@ -257,10 +258,11 @@
"name": "libraryFeatures",
"type": "string | string[]",
"default": null,
- "description": "**DEPRECATED:** Use `variant` and `features` instead.",
+ "description": "Additional modifiers in the older combined form, parsed into `variant` and `features`.",
"required": false,
"inherited": true,
- "deprecated": false
+ "deprecated": true,
+ "deprecationNote": "Use `variant` and `features` instead."
},
{
"name": "size",
diff --git a/bestax-mcp/data/skills.json b/bestax-mcp/data/skills.json
index 07b7085ee..206fb6708 100644
--- a/bestax-mcp/data/skills.json
+++ b/bestax-mcp/data/skills.json
@@ -15,7 +15,7 @@
{
"id": "library-contributor",
"file": "references/library-contributor.md",
- "bytes": 17474
+ "bytes": 18369
},
{ "id": "patterns", "file": "references/patterns.md", "bytes": 4601 }
],
@@ -93,12 +93,12 @@
{
"id": "bloomer-prop-map",
"file": "references/bloomer/prop-map.md",
- "bytes": 24475
+ "bytes": 24371
},
{
"id": "bloomer-unmappables",
"file": "references/bloomer/unmappables.md",
- "bytes": 15975
+ "bytes": 16259
},
{
"id": "css-migration",
@@ -118,7 +118,7 @@
{
"id": "rbx-unmappables",
"file": "references/rbx/unmappables.md",
- "bytes": 13300
+ "bytes": 13444
},
{
"id": "react-bulma-components-component-map",
@@ -133,7 +133,7 @@
{
"id": "react-bulma-components-unmappables",
"file": "references/react-bulma-components/unmappables.md",
- "bytes": 9304
+ "bytes": 9448
}
],
"examples": []
diff --git a/bestax-migrate/e2e/as-unions.test.ts b/bestax-migrate/e2e/as-unions.test.ts
index 0bcf2375f..94a296484 100644
--- a/bestax-migrate/e2e/as-unions.test.ts
+++ b/bestax-migrate/e2e/as-unions.test.ts
@@ -41,11 +41,48 @@ const TARGETS = Object.keys(AS_UNIONS).sort();
/** `Level.Item` is reached through the `Level` its mapping target names. */
const importRoots = [...new Set(TARGETS.map(t => t.split('.')[0]))].sort();
+/**
+ * The targets whose props FOLLOW `as` while `as` itself stays narrowed --
+ * bulma-ui's constrained polymorphic components (#663).
+ *
+ * Reading their `as` as a type does not work. Such a component is a pair of
+ * call signatures, generic first and a non-generic derivation overload second,
+ * and `infer` takes the LAST -- which pins `as` to the default element. `AsOf`
+ * reports `'a'` for `Dropdown.Item`, which also renders a `