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 `
` and a + * `