Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
a498b09
fix(bulma-ui): let Dropdown.Item's props follow its constrained `as`
allxsmith Sep 13, 2026
ef14468
fix(bulma-ui): declare the deprecated `icon` path `Icon` still honours
allxsmith Sep 13, 2026
b3cab26
docs(bulma-ui): fix four stories that render Bulma classes nothing ma…
allxsmith Sep 13, 2026
f526cbc
test(bulma-ui): make the tests and stories say what they mean
allxsmith Sep 13, 2026
5621877
ci: type-check the tests and the stories
allxsmith Sep 13, 2026
be98255
fix(bulma-ui): carry the deprecated `icon` path through every consume…
allxsmith Sep 13, 2026
05d1369
fix(bulma-ui): withhold an href from a Dropdown.Item that is not an a…
allxsmith Sep 13, 2026
44d3f65
ci: fail closed on a turbo task no package implements
allxsmith Sep 13, 2026
d84df39
fix(bulma-ui): test the icon-config shape, and keep one copy of the g…
allxsmith Sep 13, 2026
700055c
fix(bulma-ui): stop a Dropdown.Item button submitting the form it sit…
allxsmith Sep 13, 2026
215a9e1
ci: close two fail-opens in the checks added for this branch
allxsmith Sep 13, 2026
4e30a09
docs: record the new gate where contributors and CI will see it
allxsmith Sep 13, 2026
ce5d411
ci: say why both typecheck steps are there, rather than that one excl…
allxsmith Sep 13, 2026
6c20009
docs: record that the build is not a type gate
allxsmith Sep 13, 2026
95d2b57
fix(bulma-ui): keep the button default against a spread, and tag the …
allxsmith Sep 13, 2026
80d5304
fix(bestax-migrate): keep the href `Dropdown.Item` now takes
allxsmith Sep 13, 2026
7bfaf56
fix(bestax-migrate): read the element before advising an <a> inside it
allxsmith Sep 13, 2026
3b8b5f1
docs(skills): teach that Dropdown.Item takes an href now
allxsmith Sep 13, 2026
5a58e4f
fix(bestax-migrate): apply the nested-anchor rule on the plain-markup…
allxsmith Sep 13, 2026
d605acf
fix(bulma-ui): keep React's own node shapes out of the icon-props branch
allxsmith Sep 13, 2026
4f06694
fix(bestax-migrate): do not advise nesting an <a> inside an element t…
allxsmith Sep 13, 2026
fcc147a
fix(bulma-ui): keep DropdownItemProps accepting every tag it always a…
allxsmith Sep 14, 2026
3bddb43
fix(bestax-migrate): an anchor may not wrap interactive content either
allxsmith Sep 14, 2026
76abf79
fix(bestax-migrate): an anchor cannot wrap an element whose parent is…
allxsmith Sep 14, 2026
cc29e4f
ci: hold a filtered turbo task to the package it is filtered to
allxsmith Sep 14, 2026
60c1c10
test(bulma-ui): cover Panel.Icon's deprecated path and Dropdown's but…
allxsmith Sep 14, 2026
671ef77
Merge remote-tracking branch 'origin/main' into fix/663-typecheck-tes…
allxsmith Sep 14, 2026
a8d924d
fix(bestax-migrate): stop conflating the two directions an anchor can…
allxsmith Sep 14, 2026
bf13f7e
docs(bulma-ui): phrase the Icon gap historically, since this branch c…
allxsmith Sep 14, 2026
ca747be
fix(bestax-migrate): read iframe and label off their own content models
allxsmith Sep 14, 2026
f713185
docs(skills): correct the alias rule the shipped contributor guide st…
allxsmith Sep 14, 2026
42d92be
test(bestax-migrate): cover the context-bound remedy, which nothing r…
allxsmith Sep 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Comment on lines +212 to +213

- name: Test bulma-ui
run: pnpm --filter @allxsmith/bestax-bulma run test

Expand Down
18 changes: 17 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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.

Expand Down
20 changes: 10 additions & 10 deletions bestax-mcp/data/components/Dropdown.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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": []
},
{
Expand Down
24 changes: 13 additions & 11 deletions bestax-mcp/data/components/Icon.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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'",
Expand Down Expand Up @@ -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",
Expand Down
24 changes: 13 additions & 11 deletions bestax-mcp/data/components/IconText.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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'",
Expand Down Expand Up @@ -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",
Expand Down
24 changes: 13 additions & 11 deletions bestax-mcp/data/components/Panel.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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'",
Expand Down Expand Up @@ -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",
Expand Down
10 changes: 5 additions & 5 deletions bestax-mcp/data/skills.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
{
"id": "library-contributor",
"file": "references/library-contributor.md",
"bytes": 17474
"bytes": 18369
},
{ "id": "patterns", "file": "references/patterns.md", "bytes": 4601 }
],
Expand Down Expand Up @@ -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",
Expand All @@ -118,7 +118,7 @@
{
"id": "rbx-unmappables",
"file": "references/rbx/unmappables.md",
"bytes": 13300
"bytes": 13444
},
{
"id": "react-bulma-components-component-map",
Expand All @@ -133,7 +133,7 @@
{
"id": "react-bulma-components-unmappables",
"file": "references/react-bulma-components/unmappables.md",
"bytes": 9304
"bytes": 9448
}
],
"examples": []
Expand Down
51 changes: 46 additions & 5 deletions bestax-migrate/e2e/as-unions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<div>` and a
* `<button>`, so an `Exact` row against the union fails while nothing has
* drifted.
*
* So these assert by USE, the same way the href tables below do: every member
* of the row must compile, and every HTML intrinsic outside it must not. That
* is the stronger check of the two -- `Exact` reads one declaration, this one
* puts every tag through the component's own call signature.
*/
const AS_BY_USE = new Set(['Dropdown.Item']);

function buildSource(): string {
const rows = TARGETS.map((target, i) => {
const rows: string[] = [];
for (const [i, target] of TARGETS.entries()) {
if (AS_BY_USE.has(target)) {
const union = AS_UNIONS[target];
for (const value of union) {
rows.push(
`export const y${i}_${value} = <${target} as="${value}">{'x'}</${target}>; // ${target}: as="${value}"`
);
}
for (const tag of HTML_INTRINSICS) {
if (union.includes(tag)) continue;
rows.push(`// @ts-expect-error ${target} does not render a <${tag}>`);
rows.push(
`export const n${i}_${tag} = <${target} as="${tag}">{'x'}</${target}>;`
);
}
continue;
}
const union = AS_UNIONS[target].map(v => `'${v}'`).join(' | ');
return `type _${i} = Assert<Exact<${union}, AsOf<typeof ${target}>>>; // ${target}`;
});
rows.push(
`type _${i} = Assert<Exact<${union}, AsOf<typeof ${target}>>>; // ${target}`
);
}
return [
"import type { JSXElementConstructor } from 'react';",
`import { ${importRoots.join(', ')} } from '@allxsmith/bestax-bulma';`,
Expand Down Expand Up @@ -182,6 +219,7 @@ describe('the href and `as` tables match what the library accepts', () => {
// the comment on the table, which cites the line in bulma-ui.
expect(HREF_TABLE).toEqual({
Button: 'button',
'Dropdown.Item': 'a',
'Level.Item': 'div',
'Menu.Item': 'a',
'Navbar.Item': 'a',
Expand Down Expand Up @@ -505,8 +543,11 @@ describe('HREF_OK names every reachable target that takes an href', () => {
`export const n${i} = <${target} href="#">{'x'}</${target}>;`
);
// "at any `as`" is the claim, so the anchor form has to be checked
// wherever the target has one -- `Dropdown.Item` accepts `as="a"` and
// still declares no `href`, and only the bare form was asserted.
// wherever the target has one; asserting the bare form alone leaves
// `as="a"` unchecked. NOTE no reachable target currently sits in this
// state -- `Dropdown.Item` was the one, until #663 gave it an `href` --
// so this branch is a rule kept against the next such target, not
// coverage this file has today.
if (acceptsAsAnchor(target)) {
rows.push(`// @ts-expect-error ${target} takes no href at as="a"`);
rows.push(
Expand Down
Loading
Loading