Refactor module exports for ESM/CJS dual support and improve packaging - #48
Conversation
- Split the root export's types per import/require condition and emit dist/index.d.cts so CJS consumers no longer get ESM-flavored types (attw 'Masquerading as ESM'). - Fix the tailwind preset for "type": "module": the CommonJS implementation now lives in tailwind-preset.cjs and tailwind-preset.js is a real ESM wrapper (importing the preset previously threw 'module is not defined' under strict Node ESM resolution). Add type declarations for the preset entrypoint. - Export AIProvider as a value from the root so the documented 'extends AIProvider' pattern works; it was previously type-only. - Update README and docs to import from the package root instead of the nonexistent /providers and /storage subpaths, which fail to resolve. - Make publint (--strict) and are-the-types-wrong blocking in CI: the step previously ran with continue-on-error and also called a nonexistent 'are-the-types-wrong' binary (the CLI is 'attw'), so it always failed silently. Add the same packaging validation to the publish workflow, and remove the '|| true' escape hatches from the compatibility job. - Tighten react/react-dom peer ranges to ^19.0.0 to match what the library is developed and tested against. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Hep5APJQXgjsg3ku9WZEaK
📝 WalkthroughWalkthroughThis PR consolidates package subpath exports ( Sequence Diagram(s)Not applicable — the changes are configuration, packaging, documentation, and static preset/build updates without multi-component runtime interaction flows. Compact metadata
Related issues: None specified Related PRs: None specified Suggested labels: breaking-change, documentation, tailwind, ci Suggested reviewers: hbmartin Poem A rabbit hopped through paths anew, 🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Code Review
This pull request simplifies the package API by exposing all advanced APIs, providers, and storage adapters directly from the package root instead of subpath exports. It also updates the peer dependencies to require React 19, restructures the Tailwind preset to support both ESM and CommonJS with appropriate TypeScript declaration files, and updates the documentation accordingly. One issue was identified in the Tailwind preset's hexToRgb helper, where the guard h.startsWith('rgb(') will fail to match rgba values, even though the subsequent regular expression supports them. It is recommended to update the guard to h.startsWith('rgb') to prevent falling back to default colors for rgba inputs.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| const hexToRgb = (hex) => { | ||
| if (typeof hex !== 'string') return undefined; | ||
| let h = hex.trim(); | ||
| if (h.startsWith('rgb(')) { |
There was a problem hiding this comment.
The check h.startsWith('rgb(') will fail for rgba(...) colors (e.g., rgba(255, 255, 255, 0.5)), causing them to be ignored and fall back to the default colors. Since the regular expression /rgba?\(([^)]+)\)/ already correctly handles both rgb and rgba, we should update the guard to check for rgb more broadly (e.g., h.startsWith('rgb')).
if (h.startsWith('rgb')) {
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
.github/workflows/ci.yml (1)
1-9: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winWorkflow lacks explicit
permissions:block.Static analysis flags default (broad)
GITHUB_TOKENpermissions since nopermissions:key is set at workflow or job level. Add least-privilege permissions (e.g.,contents: read) at the top level.🔒 Proposed fix
name: CI + +permissions: + contents: read🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.github/workflows/ci.yml around lines 1 - 9, The CI workflow is missing an explicit permissions policy, so the default GITHUB_TOKEN scope is too broad. Add a top-level permissions block in the workflow near the existing on/jobs section and set least-privilege access, using the workflow name/trigger area in ci.yml as the place to update. Keep the permissions minimal for the jobs in this workflow, such as read-only repository contents unless a specific job needs more.Source: Linters/SAST tools
README.md (1)
226-237: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winImport the providers used in this snippet.
OpenAIProviderandAnthropicProviderare instantiated here but never imported, so the example will not compile as written.♻️ Proposed fix
import { + AnthropicProvider, + OpenAIProvider, ProviderRegistry, } from 'ai-sdk-react-model-picker';🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@README.md` around lines 226 - 237, The README example uses OpenAIProvider and AnthropicProvider without importing them, so update the snippet to include the missing provider imports alongside ProviderRegistry. Make sure the example references the correct provider class names exactly as used in the registry.register calls so the sample compiles as written.docs/basics.md (1)
14-19: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winPin the React install command to 19.
npm install react react-domcan pull a newer major and drift from the^19.0.0peer dependency. Usereact@^19andreact-dom@^19here.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/basics.md` around lines 14 - 19, The React install command is too loose and can drift past the intended peer dependency version. Update the installation guidance in the docs to pin both react and react-dom to React 19, and keep the rest of the setup unchanged. Use the install example near the React requirements section so it clearly matches the expected version range.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/ci.yml:
- Around line 52-55: The packaging validation logic is duplicated between the CI
workflow and the publish workflow, so update the Validate packaging step in the
ci workflow to use a shared source of truth instead of inlining the same
commands. Extract the publint/attw checks into a reusable pnpm script or
composite action and have both workflows call that shared entrypoint, using the
existing Validate packaging step as the place to switch over.
In `@tailwind-preset.cjs`:
- Around line 117-238: The custom color parsing in hexToRgb only supports hex
and rgb()/rgba() values, so theme colors defined with hsl() or oklch() are
ignored and fall back silently. Replace or extend hexToRgb in
tailwind-preset.cjs with a parser that supports the full CSS color spectrum, and
keep toRgb and the token generation logic unchanged so existing fallbacks still
work when parsing truly fails.
- Around line 126-155: The hexToRgb helper in tailwind-preset.cjs incorrectly
checks only for strings starting with rgb(, so rgba(...) inputs fall through and
get ignored even though the regex already supports them. Update the prefix check
inside hexToRgb to accept both rgb( and rgba( so the existing parsing logic can
handle rgba overrides correctly, and keep the undefined fallback only for truly
unsupported formats.
---
Outside diff comments:
In @.github/workflows/ci.yml:
- Around line 1-9: The CI workflow is missing an explicit permissions policy, so
the default GITHUB_TOKEN scope is too broad. Add a top-level permissions block
in the workflow near the existing on/jobs section and set least-privilege
access, using the workflow name/trigger area in ci.yml as the place to update.
Keep the permissions minimal for the jobs in this workflow, such as read-only
repository contents unless a specific job needs more.
In `@docs/basics.md`:
- Around line 14-19: The React install command is too loose and can drift past
the intended peer dependency version. Update the installation guidance in the
docs to pin both react and react-dom to React 19, and keep the rest of the setup
unchanged. Use the install example near the React requirements section so it
clearly matches the expected version range.
In `@README.md`:
- Around line 226-237: The README example uses OpenAIProvider and
AnthropicProvider without importing them, so update the snippet to include the
missing provider imports alongside ProviderRegistry. Make sure the example
references the correct provider class names exactly as used in the
registry.register calls so the sample compiles as written.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 5e890f74-eca2-4b37-a4dc-14ce6ccdd696
📒 Files selected for processing (12)
.github/workflows/ci.yml.github/workflows/publish.ymlREADME.mddocs/basics.mddocs/with-context.mdeslint.config.tspackage.jsonsrc/lib/index.tstailwind-preset.cjstailwind-preset.d.ctstailwind-preset.d.tstailwind-preset.js
| - name: Validate packaging | ||
| run: | | ||
| pnpm exec publint | ||
| pnpm exec are-the-types-wrong --pack . | ||
| continue-on-error: true | ||
| pnpm exec publint --strict | ||
| pnpm exec attw --pack . --exclude-entrypoints styles.css |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Packaging validation step duplicated with publish.yml.
The identical publint --strict + attw --pack . --exclude-entrypoints styles.css block also appears in .github/workflows/publish.yml (Lines 57-60). Consider extracting into a reusable/composite action or a shared pnpm script to avoid drift between CI and publish checks.
🧰 Tools
🪛 zizmor (1.26.1)
[warning] 10-55: overly broad permissions (excessive-permissions): default permissions used due to no permissions: block
(excessive-permissions)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/ci.yml around lines 52 - 55, The packaging validation
logic is duplicated between the CI workflow and the publish workflow, so update
the Validate packaging step in the ci workflow to use a shared source of truth
instead of inlining the same commands. Extract the publint/attw checks into a
reusable pnpm script or composite action and have both workflows call that
shared entrypoint, using the existing Validate packaging step as the place to
switch over.
| const resolve = (path, fallback) => { | ||
| try { | ||
| const v = theme(path); | ||
| return v === undefined ? fallback : v; | ||
| } catch { | ||
| return fallback; | ||
| } | ||
| }; | ||
|
|
||
| const hexToRgb = (hex) => { | ||
| if (typeof hex !== 'string') return undefined; | ||
| let h = hex.trim(); | ||
| if (h.startsWith('rgb(')) { | ||
| const m = h.match(/rgba?\(([^)]+)\)/); | ||
| if (m) { | ||
| const parts = m[1].split(/[\s,\/]+/).filter(Boolean); | ||
| const [r, g, b] = parts.map((n) => parseInt(n, 10)); | ||
| if (Number.isFinite(r) && Number.isFinite(g) && Number.isFinite(b)) { | ||
| return `${r} ${g} ${b}`; | ||
| } | ||
| } | ||
| return undefined; | ||
| } | ||
| if (!h.startsWith('#')) return undefined; | ||
| h = h.slice(1); | ||
| if (h.length === 3) { | ||
| h = h.split('').map((c) => c + c).join(''); | ||
| } else if (h.length === 4) { // #RGBA | ||
| h = h.split('').map((c, i) => (i < 3 ? c + c : '')).join(''); | ||
| } else if (h.length === 8) { // #RRGGBBAA | ||
| h = h.slice(0, 6); | ||
| } | ||
| const num = parseInt(h, 16); | ||
| if (!Number.isFinite(num)) return undefined; | ||
| const r = (num >> 16) & 255; | ||
| const g = (num >> 8) & 255; | ||
| const b = num & 255; | ||
| return `${r} ${g} ${b}`; | ||
| }; | ||
|
|
||
| const toRgb = (val, fallbackHex) => hexToRgb(val) || hexToRgb(fallbackHex); | ||
|
|
||
| const fallbacks = { | ||
| white: '#ffffff', | ||
| black: '#000000', | ||
| gray100: resolve('colors.gray.100', '#f3f4f6'), | ||
| gray200: resolve('colors.gray.200', '#e5e7eb'), | ||
| gray300: resolve('colors.gray.300', '#d1d5db'), | ||
| gray700: resolve('colors.gray.700', '#374151'), | ||
| gray800: resolve('colors.gray.800', '#1f2937'), | ||
| gray900: resolve('colors.gray.900', '#111827'), | ||
| zinc900: resolve('colors.zinc.900', '#18181b'), | ||
| zinc100: resolve('colors.zinc.100', '#f4f4f5'), | ||
| blue500: resolve('colors.blue.500', '#3b82f6'), | ||
| blue600: resolve('colors.blue.600', '#2563eb'), | ||
| red500: resolve('colors.red.500', '#ef4444'), | ||
| red600: resolve('colors.red.600', '#dc2626'), | ||
| green500: resolve('colors.green.500', '#22c55e'), | ||
| green600: resolve('colors.green.600', '#16a34a'), | ||
| amber500: resolve('colors.amber.500', '#f59e0b'), | ||
| amber600: resolve('colors.amber.600', '#d97706'), | ||
| }; | ||
|
|
||
| const primary = toRgb(resolve('colors.primary.500', fallbacks.blue500), fallbacks.blue500); | ||
| const primaryHover = toRgb(resolve('colors.primary.600', fallbacks.blue600), fallbacks.blue600); | ||
|
|
||
| const destructive = toRgb(resolve('colors.destructive.DEFAULT', fallbacks.red500), fallbacks.red500) || toRgb(fallbacks.red500, fallbacks.red500); | ||
| const destructiveHover = toRgb(resolve('colors.destructive.hover', fallbacks.red600), fallbacks.red600) || toRgb(fallbacks.red600, fallbacks.red600); | ||
| const success = toRgb(resolve('colors.success.DEFAULT', fallbacks.green500), fallbacks.green500) || toRgb(fallbacks.green500, fallbacks.green500); | ||
| const successHover = toRgb(resolve('colors.success.hover', fallbacks.green600), fallbacks.green600) || toRgb(fallbacks.green600, fallbacks.green600); | ||
| const warning = toRgb(resolve('colors.warning.DEFAULT', fallbacks.amber500), fallbacks.amber500) || toRgb(fallbacks.amber500, fallbacks.amber500); | ||
| const warningHover = toRgb(resolve('colors.warning.hover', fallbacks.amber600), fallbacks.amber600) || toRgb(fallbacks.amber600, fallbacks.amber600); | ||
|
|
||
| addBase({ | ||
| ':root': { | ||
| // Core semantic tokens (RGB triplets) | ||
| '--mp-background-rgb': toRgb(resolve('colors.background.DEFAULT', fallbacks.white), fallbacks.white), | ||
| '--mp-background-secondary-rgb': toRgb(resolve('colors.background.secondary', fallbacks.gray100), fallbacks.gray100), | ||
| '--mp-foreground-rgb': toRgb(resolve('colors.foreground.DEFAULT', fallbacks.gray900), fallbacks.gray900), | ||
| '--mp-foreground-secondary-rgb': toRgb(resolve('colors.foreground.secondary', fallbacks.gray700), fallbacks.gray700), | ||
| '--mp-primary-rgb': primary, | ||
| '--mp-primary-hover-rgb': primaryHover, | ||
| '--mp-primary-foreground-rgb': toRgb(resolve('colors.primary.foreground', fallbacks.white), fallbacks.white), | ||
| '--mp-border-rgb': toRgb(resolve('colors.border.DEFAULT', fallbacks.gray200), fallbacks.gray200), | ||
| '--mp-border-muted-rgb': toRgb(resolve('colors.border.muted', fallbacks.gray300), fallbacks.gray300), | ||
| '--mp-muted-rgb': toRgb(resolve('colors.foreground.muted', fallbacks.gray700), fallbacks.gray700), | ||
| '--mp-destructive-rgb': destructive, | ||
| '--mp-destructive-hover-rgb': destructiveHover, | ||
| '--mp-destructive-foreground-rgb': toRgb(resolve('colors.destructive.foreground', fallbacks.white), fallbacks.white), | ||
| '--mp-success-rgb': success, | ||
| '--mp-success-hover-rgb': successHover, | ||
| '--mp-success-foreground-rgb': toRgb(resolve('colors.success.foreground', fallbacks.white), fallbacks.white), | ||
| '--mp-warning-rgb': warning, | ||
| '--mp-warning-hover-rgb': warningHover, | ||
| '--mp-warning-foreground-rgb': toRgb(resolve('colors.warning.foreground', fallbacks.black), fallbacks.black), | ||
| '--mp-accent-rgb': toRgb(resolve('colors.accent.DEFAULT', fallbacks.gray100), fallbacks.gray100), | ||
| '--mp-accent-hover-rgb': toRgb(resolve('colors.accent.hover', fallbacks.gray200), fallbacks.gray200), | ||
| '--mp-accent-foreground-rgb': toRgb(resolve('colors.accent.foreground', fallbacks.gray700), fallbacks.gray700), | ||
| '--mp-border-radius': '0.375rem', | ||
| // Backwards-compat: also set non -rgb vars to the same triplets | ||
| '--mp-background': toRgb(resolve('colors.background.DEFAULT', fallbacks.white), fallbacks.white), | ||
| '--mp-background-secondary': toRgb(resolve('colors.background.secondary', fallbacks.gray100), fallbacks.gray100), | ||
| '--mp-foreground': toRgb(resolve('colors.foreground.DEFAULT', fallbacks.gray900), fallbacks.gray900), | ||
| '--mp-foreground-secondary': toRgb(resolve('colors.foreground.secondary', fallbacks.gray700), fallbacks.gray700), | ||
| '--mp-primary': primary, | ||
| '--mp-primary-hover': primaryHover, | ||
| '--mp-primary-foreground': toRgb(resolve('colors.primary.foreground', fallbacks.white), fallbacks.white), | ||
| '--mp-border': toRgb(resolve('colors.border.DEFAULT', fallbacks.gray200), fallbacks.gray200), | ||
| '--mp-border-muted': toRgb(resolve('colors.border.muted', fallbacks.gray300), fallbacks.gray300), | ||
| '--mp-muted': toRgb(resolve('colors.foreground.muted', fallbacks.gray700), fallbacks.gray700), | ||
| '--mp-destructive': destructive, | ||
| '--mp-destructive-hover': destructiveHover, | ||
| '--mp-destructive-foreground': toRgb(resolve('colors.destructive.foreground', fallbacks.white), fallbacks.white), | ||
| '--mp-success': success, | ||
| '--mp-success-hover': successHover, | ||
| '--mp-success-foreground': toRgb(resolve('colors.success.foreground', fallbacks.white), fallbacks.white), | ||
| '--mp-warning': warning, | ||
| '--mp-warning-hover': warningHover, | ||
| '--mp-warning-foreground': toRgb(resolve('colors.warning.foreground', fallbacks.black), fallbacks.black), | ||
| '--mp-accent': toRgb(resolve('colors.accent.DEFAULT', fallbacks.gray100), fallbacks.gray100), | ||
| '--mp-accent-hover': toRgb(resolve('colors.accent.hover', fallbacks.gray200), fallbacks.gray200), | ||
| '--mp-accent-foreground': toRgb(resolve('colors.accent.foreground', fallbacks.gray700), fallbacks.gray700), |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔵 Trivial | 🏗️ Heavy lift
Hand-rolled color parser can't handle hsl()/oklch() theme values.
hexToRgb only understands hex and rgb()/rgba() strings. If a consumer's Tailwind theme defines colors via hsl(...) or oklch(...) (a very common pattern for CSS-variable-driven themes, similar to the semantic token naming used here), the parser returns undefined and the plugin silently falls back to the hardcoded default instead of the consumer's actual color — with no warning that the override was ignored.
Consider using a small, well-tested color-parsing library (e.g. culori or color2k) that supports the full CSS color spectrum instead of maintaining a custom regex/hex parser.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tailwind-preset.cjs` around lines 117 - 238, The custom color parsing in
hexToRgb only supports hex and rgb()/rgba() values, so theme colors defined with
hsl() or oklch() are ignored and fall back silently. Replace or extend hexToRgb
in tailwind-preset.cjs with a parser that supports the full CSS color spectrum,
and keep toRgb and the token generation logic unchanged so existing fallbacks
still work when parsing truly fails.
| const hexToRgb = (hex) => { | ||
| if (typeof hex !== 'string') return undefined; | ||
| let h = hex.trim(); | ||
| if (h.startsWith('rgb(')) { | ||
| const m = h.match(/rgba?\(([^)]+)\)/); | ||
| if (m) { | ||
| const parts = m[1].split(/[\s,\/]+/).filter(Boolean); | ||
| const [r, g, b] = parts.map((n) => parseInt(n, 10)); | ||
| if (Number.isFinite(r) && Number.isFinite(g) && Number.isFinite(b)) { | ||
| return `${r} ${g} ${b}`; | ||
| } | ||
| } | ||
| return undefined; | ||
| } | ||
| if (!h.startsWith('#')) return undefined; | ||
| h = h.slice(1); | ||
| if (h.length === 3) { | ||
| h = h.split('').map((c) => c + c).join(''); | ||
| } else if (h.length === 4) { // #RGBA | ||
| h = h.split('').map((c, i) => (i < 3 ? c + c : '')).join(''); | ||
| } else if (h.length === 8) { // #RRGGBBAA | ||
| h = h.slice(0, 6); | ||
| } | ||
| const num = parseInt(h, 16); | ||
| if (!Number.isFinite(num)) return undefined; | ||
| const r = (num >> 16) & 255; | ||
| const g = (num >> 8) & 255; | ||
| const b = num & 255; | ||
| return `${r} ${g} ${b}`; | ||
| }; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
rgba(...) values silently fail to parse.
h.startsWith('rgb(') excludes strings starting with rgba(, even though the regex right below (/rgba?\(([^)]+)\)/) is written to support both. Any consumer overriding a color with rgba(...) will have hexToRgb return undefined for the real value and silently fall back to the hardcoded default, without any indication that their override was ignored.
🐛 Proposed fix
- if (h.startsWith('rgb(')) {
+ if (h.startsWith('rgb(') || h.startsWith('rgba(')) {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const hexToRgb = (hex) => { | |
| if (typeof hex !== 'string') return undefined; | |
| let h = hex.trim(); | |
| if (h.startsWith('rgb(')) { | |
| const m = h.match(/rgba?\(([^)]+)\)/); | |
| if (m) { | |
| const parts = m[1].split(/[\s,\/]+/).filter(Boolean); | |
| const [r, g, b] = parts.map((n) => parseInt(n, 10)); | |
| if (Number.isFinite(r) && Number.isFinite(g) && Number.isFinite(b)) { | |
| return `${r} ${g} ${b}`; | |
| } | |
| } | |
| return undefined; | |
| } | |
| if (!h.startsWith('#')) return undefined; | |
| h = h.slice(1); | |
| if (h.length === 3) { | |
| h = h.split('').map((c) => c + c).join(''); | |
| } else if (h.length === 4) { // #RGBA | |
| h = h.split('').map((c, i) => (i < 3 ? c + c : '')).join(''); | |
| } else if (h.length === 8) { // #RRGGBBAA | |
| h = h.slice(0, 6); | |
| } | |
| const num = parseInt(h, 16); | |
| if (!Number.isFinite(num)) return undefined; | |
| const r = (num >> 16) & 255; | |
| const g = (num >> 8) & 255; | |
| const b = num & 255; | |
| return `${r} ${g} ${b}`; | |
| }; | |
| const hexToRgb = (hex) => { | |
| if (typeof hex !== 'string') return undefined; | |
| let h = hex.trim(); | |
| if (h.startsWith('rgb(') || h.startsWith('rgba(')) { | |
| const m = h.match(/rgba?\(([^)]+)\)/); | |
| if (m) { | |
| const parts = m[1].split(/[\s,\/]+/).filter(Boolean); | |
| const [r, g, b] = parts.map((n) => parseInt(n, 10)); | |
| if (Number.isFinite(r) && Number.isFinite(g) && Number.isFinite(b)) { | |
| return `${r} ${g} ${b}`; | |
| } | |
| } | |
| return undefined; | |
| } | |
| if (!h.startsWith('#')) return undefined; | |
| h = h.slice(1); | |
| if (h.length === 3) { | |
| h = h.split('').map((c) => c + c).join(''); | |
| } else if (h.length === 4) { // `#RGBA` | |
| h = h.split('').map((c, i) => (i < 3 ? c + c : '')).join(''); | |
| } else if (h.length === 8) { // `#RRGGBBAA` | |
| h = h.slice(0, 6); | |
| } | |
| const num = parseInt(h, 16); | |
| if (!Number.isFinite(num)) return undefined; | |
| const r = (num >> 16) & 255; | |
| const g = (num >> 8) & 255; | |
| const b = num & 255; | |
| return `${r} ${g} ${b}`; | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tailwind-preset.cjs` around lines 126 - 155, The hexToRgb helper in
tailwind-preset.cjs incorrectly checks only for strings starting with rgb(, so
rgba(...) inputs fall through and get ignored even though the regex already
supports them. Update the prefix check inside hexToRgb to accept both rgb( and
rgba( so the existing parsing logic can handle rgba overrides correctly, and
keep the undefined fallback only for truly unsupported formats.
There was a problem hiding this comment.
6 issues found across 12 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="tailwind-preset.cjs">
<violation number="1" location="tailwind-preset.cjs:130">
P2: `rgba(...)` color values are skipped here because the guard only checks for `rgb(`, even though the regex already supports `rgba?`. That causes valid rgba theme overrides to be ignored and replaced by fallback colors.</violation>
<violation number="2" location="tailwind-preset.cjs:141">
P2: This parser currently treats non-`rgb(...)` and non-hex color strings as unsupported, so overrides provided as `hsl(...)` or `oklch(...)` get dropped and silently fall back to default colors. Expanding parsing support (or delegating to a CSS color parser) would prevent consumer theme tokens from being ignored.</violation>
<violation number="3" location="tailwind-preset.cjs:241">
P3: Dark mode block does not override `--mp-muted-rgb`, while it overrides `--mp-foreground-rgb` and `--mp-foreground-secondary-rgb`. The muted foreground may not visually adapt to dark backgrounds, potentially making some text hard to read.</violation>
<violation number="4" location="tailwind-preset.cjs:259">
P3: Stray `}` character at the end of a line comment. Looks like a copy-paste artifact: `// Keep primary/semantic status colors, but consumers can override with their own dark tokens}` — the closing brace doesn't affect execution but adds noise and can confuse readers.</violation>
</file>
<file name=".github/workflows/ci.yml">
<violation number="1" location=".github/workflows/ci.yml:52">
P3: The packaging validation commands are duplicated in both CI and publish workflows, which makes it easy for the two checks to drift over time. Moving this block to a shared pnpm script or reusable action would keep the validation behavior consistent in both pipelines.</violation>
<violation number="2" location=".github/workflows/ci.yml:82">
P2: The `pnpm add --no-save` step for AI SDK providers no longer tolerates transient failures. Since these packages are optional peer dependencies, a registry or resolution issue (e.g. a new version pulling in an incompatible peer dep, or a transient network failure) will abort the entire compatibility job and block the build — even though the versions already installed via `pnpm install --frozen-lockfile` would work fine. Consider restoring `|| true` on the install command so a best-effort upgrade doesn't gate the build; the build step itself can still surface real compatibility issues without suppression.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| if (typeof hex !== 'string') return undefined; | ||
| let h = hex.trim(); | ||
| if (h.startsWith('rgb(')) { | ||
| const m = h.match(/rgba?\(([^)]+)\)/); |
There was a problem hiding this comment.
P2: rgba(...) color values are skipped here because the guard only checks for rgb(, even though the regex already supports rgba?. That causes valid rgba theme overrides to be ignored and replaced by fallback colors.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tailwind-preset.cjs, line 130:
<comment>`rgba(...)` color values are skipped here because the guard only checks for `rgb(`, even though the regex already supports `rgba?`. That causes valid rgba theme overrides to be ignored and replaced by fallback colors.</comment>
<file context>
@@ -1 +1,263 @@
+ if (typeof hex !== 'string') return undefined;
+ let h = hex.trim();
+ if (h.startsWith('rgb(')) {
+ const m = h.match(/rgba?\(([^)]+)\)/);
+ if (m) {
+ const parts = m[1].split(/[\s,\/]+/).filter(Boolean);
</file context>
| return undefined; | ||
| } | ||
| if (!h.startsWith('#')) return undefined; | ||
| h = h.slice(1); |
There was a problem hiding this comment.
P2: This parser currently treats non-rgb(...) and non-hex color strings as unsupported, so overrides provided as hsl(...) or oklch(...) get dropped and silently fall back to default colors. Expanding parsing support (or delegating to a CSS color parser) would prevent consumer theme tokens from being ignored.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tailwind-preset.cjs, line 141:
<comment>This parser currently treats non-`rgb(...)` and non-hex color strings as unsupported, so overrides provided as `hsl(...)` or `oklch(...)` get dropped and silently fall back to default colors. Expanding parsing support (or delegating to a CSS color parser) would prevent consumer theme tokens from being ignored.</comment>
<file context>
@@ -1 +1,263 @@
+ return undefined;
+ }
+ if (!h.startsWith('#')) return undefined;
+ h = h.slice(1);
+ if (h.length === 3) {
+ h = h.split('').map((c) => c + c).join('');
</file context>
| pnpm add --no-save @ai-sdk/openai @ai-sdk/anthropic @ai-sdk/google || true | ||
| continue-on-error: true | ||
| - name: Update AI SDK providers to latest | ||
| run: pnpm add --no-save @ai-sdk/openai @ai-sdk/anthropic @ai-sdk/google |
There was a problem hiding this comment.
P2: The pnpm add --no-save step for AI SDK providers no longer tolerates transient failures. Since these packages are optional peer dependencies, a registry or resolution issue (e.g. a new version pulling in an incompatible peer dep, or a transient network failure) will abort the entire compatibility job and block the build — even though the versions already installed via pnpm install --frozen-lockfile would work fine. Consider restoring || true on the install command so a best-effort upgrade doesn't gate the build; the build step itself can still surface real compatibility issues without suppression.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/ci.yml, line 82:
<comment>The `pnpm add --no-save` step for AI SDK providers no longer tolerates transient failures. Since these packages are optional peer dependencies, a registry or resolution issue (e.g. a new version pulling in an incompatible peer dep, or a transient network failure) will abort the entire compatibility job and block the build — even though the versions already installed via `pnpm install --frozen-lockfile` would work fine. Consider restoring `|| true` on the install command so a best-effort upgrade doesn't gate the build; the build step itself can still surface real compatibility issues without suppression.</comment>
<file context>
@@ -79,19 +78,8 @@ jobs:
- pnpm add --no-save @ai-sdk/openai @ai-sdk/anthropic @ai-sdk/google || true
- continue-on-error: true
+ - name: Update AI SDK providers to latest
+ run: pnpm add --no-save @ai-sdk/openai @ai-sdk/anthropic @ai-sdk/google
- - name: Build with AI SDK providers
</file context>
| run: pnpm add --no-save @ai-sdk/openai @ai-sdk/anthropic @ai-sdk/google | |
| run: pnpm add --no-save @ai-sdk/openai @ai-sdk/anthropic @ai-sdk/google || true |
| '--mp-accent-foreground': toRgb(resolve('colors.accent.foreground', fallbacks.gray700), fallbacks.gray700), | ||
| }, | ||
| '.dark, [data-theme="dark"]': { | ||
| '--mp-background-rgb': toRgb(resolve('colors.background.dark', fallbacks.zinc900), fallbacks.zinc900), |
There was a problem hiding this comment.
P3: Dark mode block does not override --mp-muted-rgb, while it overrides --mp-foreground-rgb and --mp-foreground-secondary-rgb. The muted foreground may not visually adapt to dark backgrounds, potentially making some text hard to read.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tailwind-preset.cjs, line 241:
<comment>Dark mode block does not override `--mp-muted-rgb`, while it overrides `--mp-foreground-rgb` and `--mp-foreground-secondary-rgb`. The muted foreground may not visually adapt to dark backgrounds, potentially making some text hard to read.</comment>
<file context>
@@ -1 +1,263 @@
+ '--mp-accent-foreground': toRgb(resolve('colors.accent.foreground', fallbacks.gray700), fallbacks.gray700),
+ },
+ '.dark, [data-theme="dark"]': {
+ '--mp-background-rgb': toRgb(resolve('colors.background.dark', fallbacks.zinc900), fallbacks.zinc900),
+ '--mp-background-secondary-rgb': toRgb(resolve('colors.background.darkSecondary', fallbacks.gray800), fallbacks.gray800),
+ '--mp-foreground-rgb': toRgb(resolve('colors.foreground.dark', fallbacks.zinc100), fallbacks.zinc100),
</file context>
| '--mp-accent': toRgb(resolve('colors.accent.dark', fallbacks.gray800), fallbacks.gray800), | ||
| '--mp-accent-hover': toRgb(resolve('colors.accent.darkHover', fallbacks.gray700), fallbacks.gray700), | ||
| // Keep primary/semantic status colors, but consumers can override with their own dark tokens} | ||
| }, |
There was a problem hiding this comment.
P3: Stray } character at the end of a line comment. Looks like a copy-paste artifact: // Keep primary/semantic status colors, but consumers can override with their own dark tokens} — the closing brace doesn't affect execution but adds noise and can confuse readers.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tailwind-preset.cjs, line 259:
<comment>Stray `}` character at the end of a line comment. Looks like a copy-paste artifact: `// Keep primary/semantic status colors, but consumers can override with their own dark tokens}` — the closing brace doesn't affect execution but adds noise and can confuse readers.</comment>
<file context>
@@ -1 +1,263 @@
+ '--mp-accent': toRgb(resolve('colors.accent.dark', fallbacks.gray800), fallbacks.gray800),
+ '--mp-accent-hover': toRgb(resolve('colors.accent.darkHover', fallbacks.gray700), fallbacks.gray700),
+ // Keep primary/semantic status colors, but consumers can override with their own dark tokens}
+ },
+ });
+ },
</file context>
| @@ -49,11 +49,10 @@ jobs: | |||
| - name: Check for security vulnerabilities | |||
There was a problem hiding this comment.
P3: The packaging validation commands are duplicated in both CI and publish workflows, which makes it easy for the two checks to drift over time. Moving this block to a shared pnpm script or reusable action would keep the validation behavior consistent in both pipelines.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/ci.yml, line 52:
<comment>The packaging validation commands are duplicated in both CI and publish workflows, which makes it easy for the two checks to drift over time. Moving this block to a shared pnpm script or reusable action would keep the validation behavior consistent in both pipelines.</comment>
<file context>
@@ -49,11 +49,10 @@ jobs:
run: pnpm audit --audit-level=moderate
- - name: Validate package.json
+ - name: Validate packaging
run: |
- pnpm exec publint
</file context>
Summary
This PR restructures the module export system to properly support both ESM and CommonJS consumers while improving type definitions and packaging validation. The main changes involve swapping the implementation between
tailwind-preset.jsandtailwind-preset.cjs, adding TypeScript type definitions, and updating the package.json exports field with proper conditional exports.Key Changes
Tailwind Preset Module Restructuring
tailwind-preset.jstotailwind-preset.cjs(CommonJS)tailwind-preset.jsto an ESM wrapper that re-exports from the CJS filetailwind-preset.d.tsandtailwind-preset.d.cts) for both module formatsPackage.json Export Configuration
exportsfield to use conditional exports with proper type definitions for each entry pointtypesanddefaultfieldsfilesarray for distributionBuild Process
dist/index.d.tstodist/index.d.ctsfor CommonJS type support^19.0.0(React 19 required)CI/CD Improvements
are-the-types-wrongwithattw(are-the-types-wrong) in validationpublintvalidation strict modeDocumentation & Exports
AIProviderbase class to main exports for custom provider extensionLinting Configuration
Implementation Details
The dual-module approach ensures compatibility across different JavaScript environments:
tailwind-preset.jswhich re-exports the CommonJS implementationtailwind-preset.cjs.d.tsfor ESM,.d.ctsfor CJS)The conditional exports in package.json ensure tooling and bundlers correctly resolve the appropriate format based on the consumer's module system.
https://claude.ai/code/session_01Hep5APJQXgjsg3ku9WZEaK
Summary by CodeRabbit
New Features
Bug Fixes
Documentation
Chores