From 77b1cc8a16262fe88cc4cdc7bf243ec8b2e1791e Mon Sep 17 00:00:00 2001 From: Javier Diaz Date: Sat, 24 Jan 2026 13:25:55 -0600 Subject: [PATCH 1/5] feat: added support for type-safe design tokens --- packages/jacaranda/src/index.test.ts | 173 ++++++++++++++++++++++++++- packages/jacaranda/src/index.ts | 91 +++++++++----- 2 files changed, 234 insertions(+), 30 deletions(-) diff --git a/packages/jacaranda/src/index.test.ts b/packages/jacaranda/src/index.test.ts index c7fdb3b..46625c5 100644 --- a/packages/jacaranda/src/index.test.ts +++ b/packages/jacaranda/src/index.test.ts @@ -1,9 +1,180 @@ import React from 'react'; import { View } from 'react-native'; -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, expectTypeOf } from 'vitest'; import { render } from '@testing-library/react-native'; import { defineTokens } from './'; +describe('Type Safety - TokenPaths', () => { + it('should generate correct token paths for colors', () => { + const config = defineTokens({ + colors: { + white: '#ffffff', + black: '#000000', + primary: '#06b6d4', + }, + }); + + // Type test: verify tokens object has correct structure + expectTypeOf(config.tokens.colors).toEqualTypeOf< + | { + white: string; + black: string; + primary: string; + } + | undefined + >(); + }); + + it('should generate correct token paths for space tokens', () => { + const config = defineTokens({ + space: { + 1: 4, + 2: 8, + 3: 12, + }, + }); + + // Type test: verify tokens object has correct structure + expectTypeOf(config.tokens.space).toEqualTypeOf< + | { + 1: number; + 2: number; + 3: number; + } + | undefined + >(); + }); + + it('should generate correct token paths for multiple categories', () => { + const config = defineTokens({ + colors: { + white: '#ffffff', + primary: '#06b6d4', + }, + space: { + 1: 4, + 2: 8, + }, + fontSize: { + sm: 12, + md: 16, + }, + }); + + // Type test: verify tokens object has all categories + expectTypeOf(config.tokens.colors).toEqualTypeOf< + | { + white: string; + primary: string; + } + | undefined + >(); + + expectTypeOf(config.tokens.space).toEqualTypeOf< + | { + 1: number; + 2: number; + } + | undefined + >(); + + expectTypeOf(config.tokens.fontSize).toEqualTypeOf< + | { + sm: number; + md: number; + } + | undefined + >(); + }); + + it('should work with empty token config', () => { + const config = defineTokens({}); + + // Type test: empty config should still work + expectTypeOf(config.tokens).toEqualTypeOf<{}>(); + }); + + it('should provide type-safe token references in sva', () => { + const { sva } = defineTokens({ + colors: { + primary: '#06b6d4', + secondary: '#8b5cf6', + }, + space: { + 1: 4, + 2: 8, + }, + }); + + // This should compile with valid token references + const buttonStyles = sva({ + base: { + backgroundColor: '$colors.primary', + padding: '$space.1', + }, + variants: { + size: { + small: { padding: '$space.1' }, + large: { padding: '$space.2' }, + }, + }, + }); + + // Verify it works at runtime + const result = buttonStyles({ size: 'small' }); + expect(result).toEqual({ + backgroundColor: '#06b6d4', + padding: 4, + }); + }); + + it('should provide type-safe token references in styled', () => { + const { styled } = defineTokens({ + colors: { + white: '#ffffff', + primary: '#06b6d4', + }, + space: { + 2: 8, + 4: 16, + }, + }); + + // This should compile with valid token references + const StyledView = styled(View)({ + backgroundColor: '$colors.white', + padding: '$space.4', + }); + + // Verify the component renders + const { toJSON } = render(React.createElement(StyledView)); + expect(toJSON()).toBeTruthy(); + }); + + it('should provide readonly tokens object', () => { + const { tokens } = defineTokens({ + colors: { + primary: '#06b6d4', + }, + space: { + 1: 4, + }, + } as const); + + // Type test: tokens should be readonly + expectTypeOf(tokens).toMatchTypeOf< + Readonly<{ + colors?: { primary: string }; + space?: { 1: number }; + }> + >(); + + // Runtime test: tokens should be accessible + expect(tokens.colors?.primary).toBe('#06b6d4'); + expect(tokens.space?.[1]).toBe(4); + }); +}); + describe('sva', () => { it('should return base styles when no variants are provided', () => { const { sva } = defineTokens({}); diff --git a/packages/jacaranda/src/index.ts b/packages/jacaranda/src/index.ts index f5119ac..87d311e 100644 --- a/packages/jacaranda/src/index.ts +++ b/packages/jacaranda/src/index.ts @@ -51,29 +51,36 @@ import { ImageStyle, TextStyle, ViewStyle, StyleProp, StyleSheet } from 'react-n // Define a type that removes token strings from style properties type ResolvedStyle = ViewStyle & TextStyle & ImageStyle; -// StyleObject now extends ResolvedStyle -type StyleObject = { +// StyleObject now extends ResolvedStyle and accepts type-safe token references +// Generic parameter Tokens allows autocomplete and validation of token paths +// Default to TokenConfig for backward compatibility +type StyleObject = { [K in keyof ResolvedStyle]?: | ResolvedStyle[K] - | (string extends ResolvedStyle[K] ? `$${string}` : never) - | (number extends ResolvedStyle[K] ? `$${string}` : never); + | (string extends ResolvedStyle[K] + ? TokenPaths + : number extends ResolvedStyle[K] + ? TokenPaths + : ResolvedStyle[K] extends string | number | undefined + ? TokenPaths + : never); }; // Define the VariantOptions type to ensure type safety in variant definitions -type VariantOptions = { +type VariantOptions = { [P in keyof V]: { - [K in keyof V[P]]: StyleObject; + [K in keyof V[P]]: StyleObject; }; }; // BooleanVariantKey type for 'true' and 'false' string literals type BooleanVariantKey = 'true' | 'false'; -type CompoundVariant = { +type CompoundVariant = { variants: Partial<{ [P in keyof V]: keyof V[P] | boolean | Array; }>; - style: StyleObject; + style: StyleObject; }; // DefaultVariants type @@ -82,10 +89,10 @@ type DefaultVariants = Partial<{ }>; // VariantStyleConfig type -type VariantStyleConfig> = { - base?: StyleObject; +type VariantStyleConfig, Tokens extends TokenConfig = TokenConfig> = { + base?: StyleObject; variants: V; - compoundVariants?: CompoundVariant[]; + compoundVariants?: CompoundVariant[]; defaultVariants?: DefaultVariants; }; @@ -102,7 +109,9 @@ export type VariantProps = T extends (props?: infer P) => StyleProp>(config: VariantStyleConfig) { +function styles, Tokens extends TokenConfig = TokenConfig>( + config: VariantStyleConfig, +) { type VariantProps = { [P in keyof V]: keyof V[P] | boolean | (string & {}) }; type DefaultProps = NonNullable; type Props = OptionalIfHasDefault; @@ -110,7 +119,7 @@ function styles>(config: VariantStyleConfig) { return (props?: Props): StyleProp => { // We'll build up styles in the correct hierarchy: base → variants → compound variants // Base styles (lowest priority) - let stylesObj: StyleObject = { + let stylesObj: StyleObject = { ...(config.base || {}), }; @@ -199,25 +208,44 @@ type TokenConfig = { [K in keyof AllowedTokenCategories]?: Record; }; +// Helper to convert keys (including numeric) to string literals +type KeysToStrings = T extends Record ? `${keyof T & (string | number)}` : never; + +// Generate literal string types for all valid token paths from a config +// Example: For config { colors: { white: '#fff', primary: '#000' }, space: { 1: 4, 2: 8 } } +// Generates: "$colors.white" | "$colors.primary" | "$space.1" | "$space.2" +type TokenPaths = { + [Category in keyof T]: T[Category] extends Record + ? `$${Category & string}.${KeysToStrings}` + : never; +}[keyof T]; + // Helper type to extract component props type ComponentProps = T extends ComponentType ? P : never; -type StyledFunction = >( +type StyledFunction = >( Component: C, ) =>

( - styleObject: StyleObject | ((props: P & Omit, 'style'>) => StyleObject), + styleObject: + | StyleObject + | ((props: P & Omit, 'style'>) => StyleObject), ) => ComponentType< P & Omit, 'style'> & { style?: StyleProp } >; -interface CreateTokensReturn { - sva: typeof styles; - tokens: TokenConfig; - styled: StyledFunction; +interface CreateTokensReturn { + sva: >(config: VariantStyleConfig) => ( + props?: any, + ) => StyleProp; + tokens: Readonly; + styled: StyledFunction; } // Helper to resolve token references in style objects -function resolveTokens(style: StyleObject, tokens: TokenConfig): StyleObject { +function resolveTokens( + style: StyleObject, + tokens: Tokens, +): StyleObject { return Object.entries(style).reduce>( (acc, [key, value]) => { if (typeof value !== 'string' || !value.startsWith('$')) { @@ -228,7 +256,10 @@ function resolveTokens(style: StyleObject, tokens: TokenConfig): StyleObject { const tokenPath = value.slice(1).split('.'); const [category, token] = tokenPath; - const tokenValue = tokens[category as keyof TokenConfig]?.[token]; + const tokenCategory = tokens[category as keyof Tokens] as + | Record + | undefined; + const tokenValue = tokenCategory?.[token]; if (tokenValue !== undefined) { acc[key] = tokenValue; } @@ -236,33 +267,33 @@ function resolveTokens(style: StyleObject, tokens: TokenConfig): StyleObject { return acc; }, {}, - ) as StyleObject; + ) as StyleObject; } /** * Creates a token system and returns the styles function */ -export function defineTokens(tokenConfig: T): CreateTokensReturn { +export function defineTokens(tokenConfig: T): CreateTokensReturn { // Create the tokens object const tokens = Object.entries(tokenConfig).reduce((acc, [category, values]) => { return { ...acc, [category]: values, }; - }, {} as TokenConfig); + }, {} as T); // Create a wrapped styles function that resolves token references - const sva: typeof styles = >(config: VariantStyleConfig) => { + const sva = >(config: VariantStyleConfig) => { // Resolve tokens in base styles const resolvedBase = config.base ? resolveTokens(config.base, tokens) : config.base; // Resolve tokens in variants const resolvedVariants = config.variants ? (Object.entries(config.variants).reduce>((acc, [key, variantGroup]) => { - type VariantGroupType = Record; + type VariantGroupType = Record>; const resolvedGroup = Object.entries(variantGroup as VariantGroupType).reduce< - Record + Record> >((groupAcc, [variantKey, variantStyles]) => { return { ...groupAcc, @@ -295,9 +326,11 @@ export function defineTokens(tokenConfig: T): CreateToken /** * Styled function for creating styled components with token-aware styles */ - const styled: StyledFunction = >(Component: C) => { + const styled: StyledFunction = >(Component: C) => { return

( - styleObject: StyleObject | ((props: P & Omit, 'style'>) => StyleObject), + styleObject: + | StyleObject + | ((props: P & Omit, 'style'>) => StyleObject), ) => { // Create and return a new component that applies the resolved styles const StyledComponent: ComponentType< From b21240ef11ce8a838b9bad1652a2f3c5a142bf61 Mon Sep 17 00:00:00 2001 From: Javier Diaz Date: Sat, 24 Jan 2026 13:28:51 -0600 Subject: [PATCH 2/5] fix: fixed issue generating mjs file --- packages/jacaranda/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/jacaranda/package.json b/packages/jacaranda/package.json index ec371fc..9b0349e 100644 --- a/packages/jacaranda/package.json +++ b/packages/jacaranda/package.json @@ -12,7 +12,7 @@ "scripts": { "build": "pnpm run '/^build:.*/'", "build:cjs": "swc ./src/index.ts --config-file ./.swcrc -o dist/index.js -C module.type=commonjs", - "build:esm": "swc ./src/index.ts --config-file ./.swcrc -o dist/index.js -C module.type=es6", + "build:esm": "swc ./src/index.ts --config-file ./.swcrc -o dist/index.mjs -C module.type=es6", "build:tsc": "tsc --project tsconfig.build.json", "check": "tsc --project tsconfig.json --noEmit", "prepublishOnly": "pnpm build", From 578a491cfb7ee845d14693d02d5f61e99a5d127c Mon Sep 17 00:00:00 2001 From: Javier Diaz Date: Sat, 24 Jan 2026 13:33:53 -0600 Subject: [PATCH 3/5] style: improved code formatting --- packages/jacaranda/src/index.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/jacaranda/src/index.ts b/packages/jacaranda/src/index.ts index 87d311e..fb7c201 100644 --- a/packages/jacaranda/src/index.ts +++ b/packages/jacaranda/src/index.ts @@ -89,7 +89,10 @@ type DefaultVariants = Partial<{ }>; // VariantStyleConfig type -type VariantStyleConfig, Tokens extends TokenConfig = TokenConfig> = { +type VariantStyleConfig< + V extends VariantOptions, + Tokens extends TokenConfig = TokenConfig, +> = { base?: StyleObject; variants: V; compoundVariants?: CompoundVariant[]; @@ -209,7 +212,8 @@ type TokenConfig = { }; // Helper to convert keys (including numeric) to string literals -type KeysToStrings = T extends Record ? `${keyof T & (string | number)}` : never; +type KeysToStrings = + T extends Record ? `${keyof T & (string | number)}` : never; // Generate literal string types for all valid token paths from a config // Example: For config { colors: { white: '#fff', primary: '#000' }, space: { 1: 4, 2: 8 } } @@ -234,9 +238,9 @@ type StyledFunction = ; interface CreateTokensReturn { - sva: >(config: VariantStyleConfig) => ( - props?: any, - ) => StyleProp; + sva: >( + config: VariantStyleConfig, + ) => (props?: any) => StyleProp; tokens: Readonly; styled: StyledFunction; } From 30e28360ff8269dec050ae8a38d2b456eef0b9b7 Mon Sep 17 00:00:00 2001 From: Javier Diaz Date: Sat, 24 Jan 2026 13:34:26 -0600 Subject: [PATCH 4/5] test: fixed typing errors --- packages/jacaranda/src/index.test.ts | 63 +++++++++++----------------- 1 file changed, 24 insertions(+), 39 deletions(-) diff --git a/packages/jacaranda/src/index.test.ts b/packages/jacaranda/src/index.test.ts index 46625c5..a0a5db5 100644 --- a/packages/jacaranda/src/index.test.ts +++ b/packages/jacaranda/src/index.test.ts @@ -15,14 +15,11 @@ describe('Type Safety - TokenPaths', () => { }); // Type test: verify tokens object has correct structure - expectTypeOf(config.tokens.colors).toEqualTypeOf< - | { - white: string; - black: string; - primary: string; - } - | undefined - >(); + expectTypeOf(config.tokens.colors).toMatchTypeOf<{ + white: string; + black: string; + primary: string; + }>(); }); it('should generate correct token paths for space tokens', () => { @@ -35,14 +32,11 @@ describe('Type Safety - TokenPaths', () => { }); // Type test: verify tokens object has correct structure - expectTypeOf(config.tokens.space).toEqualTypeOf< - | { - 1: number; - 2: number; - 3: number; - } - | undefined - >(); + expectTypeOf(config.tokens.space).toMatchTypeOf<{ + 1: number; + 2: number; + 3: number; + }>(); }); it('should generate correct token paths for multiple categories', () => { @@ -62,29 +56,20 @@ describe('Type Safety - TokenPaths', () => { }); // Type test: verify tokens object has all categories - expectTypeOf(config.tokens.colors).toEqualTypeOf< - | { - white: string; - primary: string; - } - | undefined - >(); - - expectTypeOf(config.tokens.space).toEqualTypeOf< - | { - 1: number; - 2: number; - } - | undefined - >(); - - expectTypeOf(config.tokens.fontSize).toEqualTypeOf< - | { - sm: number; - md: number; - } - | undefined - >(); + expectTypeOf(config.tokens.colors).toMatchTypeOf<{ + white: string; + primary: string; + }>(); + + expectTypeOf(config.tokens.space).toMatchTypeOf<{ + 1: number; + 2: number; + }>(); + + expectTypeOf(config.tokens.fontSize).toMatchTypeOf<{ + sm: number; + md: number; + }>(); }); it('should work with empty token config', () => { From 51c5ad2a337eb7980ffa5a8d7efb340a786a3629 Mon Sep 17 00:00:00 2001 From: Javier Diaz Date: Sat, 24 Jan 2026 13:36:53 -0600 Subject: [PATCH 5/5] chore: updated docs --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f76690c..df85b8a 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,6 @@ ![GitHub Issues](https://img.shields.io/github/issues/coderdiaz/jacaranda?style=flat) ![GitHub Stars](https://img.shields.io/github/stars/coderdiaz/jacaranda?style=flat) -> ⚠️ **BETA SOFTWARE**: This library is in active development and not yet recommended for production use. APIs may change without notice. Feel free to try it out and provide feedback! - Provides a way to styling components in React Native with better experience and composability. The key feature is the ability to create multi-variants styles with a type-safe definition inspired by [Stitches](https://stitches.dev/docs/variants) and [CVA](https://cva.style/docs/getting-started/variants) (for React apps). ## Features @@ -27,7 +25,7 @@ Provides a way to styling components in React Native with better experience and - [x] Styled function to styling component using design tokens. - [x] Use `Stylesheet.create` instead a simple objects. - [x] Access to `props` from styles defined into `styled` components. -- [ ] Type-safe registered tokens inside styles. +- [x] Type-safe registered tokens inside styles. - [ ] Default design tokens. ### How to install @@ -197,4 +195,4 @@ const StyledView = styled(View)((props) => ({ })); ``` -Copyright @ 2025 by Javier Diaz +Copyright @ since 2025 by Javier Diaz