Skip to content

Refactor module exports for ESM/CJS dual support and improve packaging - #48

Merged
hbmartin merged 1 commit into
mainfrom
claude/exports-ci-gating-bugs-7ccccz
Jul 3, 2026
Merged

Refactor module exports for ESM/CJS dual support and improve packaging#48
hbmartin merged 1 commit into
mainfrom
claude/exports-ci-gating-bugs-7ccccz

Conversation

@hbmartin

@hbmartin hbmartin commented Jul 2, 2026

Copy link
Copy Markdown
Owner

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.js and tailwind-preset.cjs, adding TypeScript type definitions, and updating the package.json exports field with proper conditional exports.

Key Changes

  • Tailwind Preset Module Restructuring

    • Moved full Tailwind config implementation from tailwind-preset.js to tailwind-preset.cjs (CommonJS)
    • Changed tailwind-preset.js to an ESM wrapper that re-exports from the CJS file
    • Added TypeScript type definition files (tailwind-preset.d.ts and tailwind-preset.d.cts) for both module formats
  • Package.json Export Configuration

    • Updated exports field to use conditional exports with proper type definitions for each entry point
    • Separated import/require conditions with explicit types and default fields
    • Added new type definition files to the files array for distribution
  • Build Process

    • Updated build script to copy dist/index.d.ts to dist/index.d.cts for CommonJS type support
    • Updated React peer dependencies to ^19.0.0 (React 19 required)
  • CI/CD Improvements

    • Replaced are-the-types-wrong with attw (are-the-types-wrong) in validation
    • Made publint validation strict mode
    • Simplified AI SDK provider compatibility testing by removing error suppression
    • Added packaging validation to publish workflow
  • Documentation & Exports

    • Updated README and docs to reflect that all exports are available from package root (removed subpath export references)
    • Added AIProvider base class to main exports for custom provider extension
    • Updated import examples to use root exports instead of subpath imports
    • Added React 19 requirement note to installation docs
  • Linting Configuration

    • Added generated type definition files to ESLint ignore list

Implementation Details

The dual-module approach ensures compatibility across different JavaScript environments:

  • ESM consumers import from tailwind-preset.js which re-exports the CommonJS implementation
  • CommonJS consumers directly require tailwind-preset.cjs
  • Both formats have proper TypeScript type definitions with appropriate module syntax (.d.ts for ESM, .d.cts for 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

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added a new Tailwind preset with updated semantic color tokens and dark-mode support.
    • Made the provider base export available as a runtime import.
  • Bug Fixes

    • Improved package validation to catch publishing issues earlier.
    • Updated compatibility checks to use the latest provider versions.
  • Documentation

    • Updated setup and example code to use package-root imports.
    • Clarified that React 19 is required.
  • Chores

    • Updated published package metadata and included additional generated artifacts.

- 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
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR consolidates package subpath exports (/providers, /storage) into root-level exports, requires React 19 in peer dependencies, and updates README/docs examples accordingly. It rewrites the Tailwind preset as an inlined CommonJS config with a CSS-variable-injecting plugin, adds .d.cts/.d.ts type declarations, and converts the ESM entry into a re-export wrapper. AIProvider is now exported as a runtime value rather than a type-only export. CI and publish workflows gain stricter publint --strict and attw packaging validation, and the AI SDK compatibility job's provider update flow is simplified.

Sequence Diagram(s)

Not applicable — the changes are configuration, packaging, documentation, and static preset/build updates without multi-component runtime interaction flows.

Compact metadata

  • Files changed: 13
  • Lines changed: approximately +334/-317
  • Estimated review effort: Medium-High (concentrated in tailwind-preset.cjs)

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,
From subpath burrows to the root it flew,
Tailwind tokens dressed in CSS light,
React nineteen now required just right,
And publint strict keeps packages tight. 🐇✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly captures the main refactor to ESM/CJS exports and the packaging improvements in the change set.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/exports-ci-gating-bugs-7ccccz

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread tailwind-preset.cjs
const hexToRgb = (hex) => {
if (typeof hex !== 'string') return undefined;
let h = hex.trim();
if (h.startsWith('rgb(')) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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')) {

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Workflow lacks explicit permissions: block.

Static analysis flags default (broad) GITHUB_TOKEN permissions since no permissions: 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 win

Import the providers used in this snippet.

OpenAIProvider and AnthropicProvider are 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 win

Pin the React install command to 19. npm install react react-dom can pull a newer major and drift from the ^19.0.0 peer dependency. Use react@^19 and react-dom@^19 here.

🤖 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

📥 Commits

Reviewing files that changed from the base of the PR and between 4e494cf and 11d50e6.

📒 Files selected for processing (12)
  • .github/workflows/ci.yml
  • .github/workflows/publish.yml
  • README.md
  • docs/basics.md
  • docs/with-context.md
  • eslint.config.ts
  • package.json
  • src/lib/index.ts
  • tailwind-preset.cjs
  • tailwind-preset.d.cts
  • tailwind-preset.d.ts
  • tailwind-preset.js

Comment thread .github/workflows/ci.yml
Comment on lines +52 to +55
- 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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.

Comment thread tailwind-preset.cjs
Comment on lines +117 to +238
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),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Comment thread tailwind-preset.cjs
Comment on lines +126 to +155
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}`;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread tailwind-preset.cjs
if (typeof hex !== 'string') return undefined;
let h = hex.trim();
if (h.startsWith('rgb(')) {
const m = h.match(/rgba?\(([^)]+)\)/);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Comment thread tailwind-preset.cjs
return undefined;
}
if (!h.startsWith('#')) return undefined;
h = h.slice(1);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Comment thread .github/workflows/ci.yml
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
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

Comment thread tailwind-preset.cjs
'--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),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Comment thread tailwind-preset.cjs
'--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}
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Comment thread .github/workflows/ci.yml
@@ -49,11 +49,10 @@ jobs:
- name: Check for security vulnerabilities

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

@hbmartin
hbmartin merged commit d628450 into main Jul 3, 2026
6 of 8 checks passed
@hbmartin
hbmartin deleted the claude/exports-ci-gating-bugs-7ccccz branch July 3, 2026 13:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants