From eebefe880c1d729aa9552c75d138d10772ee9dba Mon Sep 17 00:00:00 2001 From: Hubert Bieszczad Date: Tue, 28 Oct 2025 10:48:07 +0100 Subject: [PATCH 1/3] feat: serializer refactor --- .../uniwind/src/core/native/native-utils.ts | 8 +- .../src/core/native/parsers/transforms.ts | 12 +- packages/uniwind/src/core/types.ts | 2 +- .../addMetaToStylesTemplate.ts | 24 +-- packages/uniwind/src/metro/compileVirtual.ts | 9 +- packages/uniwind/src/metro/processor/css.ts | 42 +++- .../uniwind/src/metro/processor/functions.ts | 32 ++- packages/uniwind/src/metro/processor/rn.ts | 17 +- .../uniwind/src/metro/stylesheet/index.ts | 2 - .../metro/stylesheet/serializeStylesheet.ts | 191 ------------------ packages/uniwind/src/metro/utils/common.ts | 32 ++- packages/uniwind/src/metro/utils/index.ts | 1 + packages/uniwind/src/metro/utils/serialize.ts | 115 +++++++++++ 13 files changed, 226 insertions(+), 261 deletions(-) rename packages/uniwind/src/metro/{stylesheet => }/addMetaToStylesTemplate.ts (88%) delete mode 100644 packages/uniwind/src/metro/stylesheet/index.ts delete mode 100644 packages/uniwind/src/metro/stylesheet/serializeStylesheet.ts create mode 100644 packages/uniwind/src/metro/utils/serialize.ts diff --git a/packages/uniwind/src/core/native/native-utils.ts b/packages/uniwind/src/core/native/native-utils.ts index 52aaba78..affd49ac 100644 --- a/packages/uniwind/src/core/native/native-utils.ts +++ b/packages/uniwind/src/core/native/native-utils.ts @@ -1,7 +1,9 @@ import { formatHex, formatHex8, interpolate, parse } from 'culori' import type { UniwindRuntime } from '../types' -export const colorMix = (color: string, mixColor: string, weight: number) => { +export const colorMix = (color: string, mixColor: string, weight: number | string) => { + const parsedWeight = typeof weight === 'string' ? parseFloat(weight) / 100 : weight + // Change alpha if (mixColor === '#00000000') { const parsedColor = parse(color) @@ -12,11 +14,11 @@ export const colorMix = (color: string, mixColor: string, weight: number) => { return formatHex8({ ...parsedColor, - alpha: weight, + alpha: parsedWeight * (parsedColor.alpha ?? 1), }) } - return formatHex(interpolate([mixColor, color])(weight)) + return formatHex(interpolate([mixColor, color])(parsedWeight)) } export function lightDark(this: UniwindRuntime, light: string, dark: string) { diff --git a/packages/uniwind/src/core/native/parsers/transforms.ts b/packages/uniwind/src/core/native/parsers/transforms.ts index f4faeec9..99e4d2c6 100644 --- a/packages/uniwind/src/core/native/parsers/transforms.ts +++ b/packages/uniwind/src/core/native/parsers/transforms.ts @@ -15,6 +15,14 @@ const transforms = [ 'perspective', ] +const processTransform = (transform: string, value: any) => { + if (transform.startsWith('scale') && typeof value === 'string') { + return parseFloat(value.replace('%', '')) / 100 + } + + return value +} + export const parseTransformsMutation = (styles: Record) => { const transformTokens = typeof styles.transform === 'string' ? styles.transform @@ -34,13 +42,13 @@ export const parseTransformsMutation = (styles: Record) => { const transformValue = token.slice(transform.length + 1, -1) - transformsResult.push({ [transform]: transformValue }) + transformsResult.push({ [transform]: processTransform(transform, transformValue) }) } } // Transforms outside of transform - { rotate: '45deg' } if (styles[transform] !== undefined) { - transformsResult.push({ [transform]: styles[transform] }) + transformsResult.push({ [transform]: processTransform(transform, styles[transform]) }) delete styles[transform] } } diff --git a/packages/uniwind/src/core/types.ts b/packages/uniwind/src/core/types.ts index 50982266..f6fd21a7 100644 --- a/packages/uniwind/src/core/types.ts +++ b/packages/uniwind/src/core/types.ts @@ -50,7 +50,7 @@ export type UniwindRuntime = { hairlineWidth: number pixelRatio: (value: number) => number fontScale: (value: number) => number - colorMix: (color: string, mixColor: string, weight: number) => string + colorMix: (color: string, mixColor: string, weight: number | string) => string cubicBezier: (x1: number, y1: number, x2: number, y2: number) => string lightDark: (light: string, dark: string) => string } diff --git a/packages/uniwind/src/metro/stylesheet/addMetaToStylesTemplate.ts b/packages/uniwind/src/metro/addMetaToStylesTemplate.ts similarity index 88% rename from packages/uniwind/src/metro/stylesheet/addMetaToStylesTemplate.ts rename to packages/uniwind/src/metro/addMetaToStylesTemplate.ts index b4d800c2..09bf1a7a 100644 --- a/packages/uniwind/src/metro/stylesheet/addMetaToStylesTemplate.ts +++ b/packages/uniwind/src/metro/addMetaToStylesTemplate.ts @@ -1,19 +1,7 @@ -import { StyleDependency } from '../../types' -import { ProcessorBuilder } from '../processor' -import { Platform, StyleSheetTemplate } from '../types' -import { isDefined, toCamelCase } from '../utils' - -const simpleSerialize = (value: any): string => { - if (Array.isArray(value)) { - return [ - '[', - value.map(simpleSerialize).join(', '), - ']', - ].join('') - } - - return JSON.stringify(value) -} +import { StyleDependency } from '../types' +import { ProcessorBuilder } from './processor' +import { Platform, StyleSheetTemplate } from './types' +import { isDefined, serialize, toCamelCase } from './utils' const extractVarsFromString = (value: string) => { const thisIndexes = [...value.matchAll(/this\[/g)].map(m => m.index) @@ -50,7 +38,7 @@ export const addMetaToStylesTemplate = (Processor: ProcessorBuilder, currentPlat const entries = Object.entries(rest) .flatMap(([property, value]) => Processor.RN.cssToRN(property, value)) - .map(([property, value]) => [property, `function() { return ${simpleSerialize(value)} }`]) + .map(([property, value]) => [`"${property}"`, `function() { return ${serialize(value)} }`]) if (platform && platform !== Platform.Native && platform !== currentPlatform) { return null @@ -110,7 +98,7 @@ export const addMetaToStylesTemplate = (Processor: ProcessorBuilder, currentPlat native: platform !== null, dependencies, index, - className, + className: `"${className}"`, active, focus, disabled, diff --git a/packages/uniwind/src/metro/compileVirtual.ts b/packages/uniwind/src/metro/compileVirtual.ts index 8d5ef072..7508dd25 100644 --- a/packages/uniwind/src/metro/compileVirtual.ts +++ b/packages/uniwind/src/metro/compileVirtual.ts @@ -1,8 +1,9 @@ import { compile } from '@tailwindcss/node' +import { addMetaToStylesTemplate } from './addMetaToStylesTemplate' import { polyfillWeb } from './polyfillWeb' import { ProcessorBuilder } from './processor' -import { addMetaToStylesTemplate, serializeJS } from './stylesheet' import { Platform, Polyfills } from './types' +import { serializeJSObject } from './utils' type CompileVirtualConfig = { cssPath: string @@ -28,11 +29,11 @@ export const compileVirtual = async ({ candidates, css, cssPath, platform, theme Processor.transform(tailwindCSS) - const stylesheet = serializeJS( + const stylesheet = serializeJSObject( addMetaToStylesTemplate(Processor, platform), (key, value) => `"${key}": ${value}`, ) - const vars = serializeJS( + const vars = serializeJSObject( Processor.vars, (key, value) => `get "${key}"() { return ${value} }`, ) @@ -40,7 +41,7 @@ export const compileVirtual = async ({ candidates, css, cssPath, platform, theme Object.entries(Processor.scopedVars) .map(([scopedVarsName, scopedVars]) => [ scopedVarsName, - serializeJS(scopedVars, (key, value) => `get "${key}"() { return ${value} }`), + serializeJSObject(scopedVars, (key, value) => `get "${key}"() { return ${value} }`), ]), ) const serializedScopedVars = Object.entries(scopedVars) diff --git a/packages/uniwind/src/metro/processor/css.ts b/packages/uniwind/src/metro/processor/css.ts index 8f6c6dbd..9ef554b2 100644 --- a/packages/uniwind/src/metro/processor/css.ts +++ b/packages/uniwind/src/metro/processor/css.ts @@ -1,7 +1,7 @@ import { OverflowKeyword } from 'lightningcss' import { Logger } from '../logger' import { DeclarationValues } from '../types' -import { isDefined, pipe } from '../utils' +import { isDefined, pipe, roundToPrecision, shouldBeSerialized } from '../utils' import type { ProcessorBuilder } from './processor' export class CSS { @@ -10,6 +10,34 @@ export class CSS { constructor(private readonly Processor: ProcessorBuilder) {} processValue(declarationValue: DeclarationValues): any { + const processedValue = this.getProcessedValue(declarationValue) + + if (typeof processedValue === 'string') { + return this.makeSafeForSerialization(processedValue) + } + + if (typeof processedValue === 'object' && processedValue !== null) { + return Object.fromEntries( + Object.entries(processedValue).map(([key, value]) => { + if (typeof value === 'string') { + return [ + key, + this.makeSafeForSerialization(value), + ] + } + + return [ + key, + value, + ] + }), + ) + } + + return processedValue + } + + private getProcessedValue(declarationValue: DeclarationValues): any { if (typeof declarationValue !== 'object') { return declarationValue } @@ -473,4 +501,16 @@ export class CSS { ].filter(Boolean).join(' '), ) } + + private makeSafeForSerialization(value: string) { + if (shouldBeSerialized(value)) { + return value + } + + if (value.endsWith('%')) { + return `"${roundToPrecision(parseFloat(value), 2)}%"` + } + + return `"${value}"` + } } diff --git a/packages/uniwind/src/metro/processor/functions.ts b/packages/uniwind/src/metro/processor/functions.ts index 5beaec6b..2b7d80d2 100644 --- a/packages/uniwind/src/metro/processor/functions.ts +++ b/packages/uniwind/src/metro/processor/functions.ts @@ -1,6 +1,6 @@ import { CalcFor_DimensionPercentageFor_LengthValue, CalcFor_Length, CssColor, Function as FunctionType } from 'lightningcss' import { Logger } from '../logger' -import { percentageToFloat, pipe } from '../utils' +import { pipe } from '../utils' import type { ProcessorBuilder } from './processor' export class Functions { @@ -54,7 +54,7 @@ export class Functions { } if (fn.name === 'max') { - return `Math.max( ${this.Processor.CSS.processValue(fn.arguments)} )` + return `Math.max(${this.Processor.CSS.processValue(fn.arguments)})` } if (fn.name === 'linear-gradient') { @@ -110,11 +110,11 @@ export class Functions { } if (fn.name === 'pixelRatio') { - return `rt.pixelRatio( ${this.Processor.CSS.processValue(fn.arguments)} )` + return `rt.pixelRatio(${this.Processor.CSS.processValue(fn.arguments)})` } if (fn.name === 'fontScale') { - return `rt.fontScale( ${this.Processor.CSS.processValue(fn.arguments)} )` + return `rt.fontScale(${this.Processor.CSS.processValue(fn.arguments)})` } this.logger.error(`Unsupported function - ${fn.name}`) @@ -131,12 +131,12 @@ export class Functions { | CalcFor_Length, ) { if (!Array.isArray(value)) { - return `Math.${name} ( ${this.processCalc(value)} )` + return `Math.${name}(${this.processCalc(value)})` } const values = value.map(x => this.processCalc(x)).join(' , ') - return `Math.${name}( ${values} )` + return `Math.${name}(${values})` } private tryEval(value: string) { @@ -168,25 +168,19 @@ export class Functions { } private processColorMix(fn: FunctionType) { - const tokens = pipe(this.Processor.CSS.processValue(fn.arguments))( - String, - x => x.replace(/\](?!\s*[+-/*])(?!\s)/g, '] '), - x => x.replace(/,/g, ''), - x => x.split(' '), - ) - - const color = tokens.find(token => token.startsWith('#') || token.startsWith('this[`--color-')) - const percentage = percentageToFloat(tokens.find(token => token.endsWith('%')) ?? '50%') - const mixColor = tokens.reverse().find(token => token.startsWith('#') || token.startsWith('this[`--color-')) + const tokens: Array = fn.arguments.map(arg => this.Processor.CSS.processValue(arg)).reverse() + const color = tokens.at(3) + const alpha = tokens.at(2) + const mixColor = tokens.at(0) return [ - 'rt.colorMix( ', + 'rt.colorMix(', color, ', ', mixColor, ', ', - percentage, - ' )', + alpha, + ')', ].join('') } } diff --git a/packages/uniwind/src/metro/processor/rn.ts b/packages/uniwind/src/metro/processor/rn.ts index 74490da9..f6d721ac 100644 --- a/packages/uniwind/src/metro/processor/rn.ts +++ b/packages/uniwind/src/metro/processor/rn.ts @@ -1,4 +1,4 @@ -import { addMissingSpaces, isDefined, percentageToFloat, pipe, toCamelCase } from '../utils' +import { addMissingSpaces, isDefined, pipe, toCamelCase } from '../utils' import type { ProcessorBuilder } from './processor' const cssToRNMap: Record Record> = { @@ -30,21 +30,6 @@ const cssToRNMap: Record Record> = { overflow: value, } }, - '--tw-scale-x': (value: string) => { - return { - '--tw-scale-x': percentageToFloat(value), - } - }, - '--tw-scale-y': (value: string) => { - return { - '--tw-scale-y': percentageToFloat(value), - } - }, - '--tw-scale-z': (value: string) => { - return { - '--tw-scale-z': percentageToFloat(value), - } - }, backdropFilter: () => ({}), backgroundImage: value => ({ experimental_backgroundImage: value, diff --git a/packages/uniwind/src/metro/stylesheet/index.ts b/packages/uniwind/src/metro/stylesheet/index.ts deleted file mode 100644 index 322dc875..00000000 --- a/packages/uniwind/src/metro/stylesheet/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './addMetaToStylesTemplate' -export * from './serializeStylesheet' diff --git a/packages/uniwind/src/metro/stylesheet/serializeStylesheet.ts b/packages/uniwind/src/metro/stylesheet/serializeStylesheet.ts deleted file mode 100644 index cba82ee0..00000000 --- a/packages/uniwind/src/metro/stylesheet/serializeStylesheet.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { Logger } from '../logger' -import { addMissingSpaces, isNumber, pipe, smartSplit } from '../utils' - -const FN_DECLARATION = 'function() { return' - -const isJSExpression = (value: string) => - [ - value.includes('this'), - value.includes('rt.'), - value.includes('function() {'), - /\s([-+/*])\s/.test(value), - ].some(Boolean) - -const isValidJSValue = (value: string) => { - try { - // eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func - new Function(`const test = ${value}`) - - return true - } catch { - return false - } -} - -const isFunction = (value: string) => /^[A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z][A-Za-z0-9_]*)*\s*\(.*\)$/.test(value) - -const toJSExpression = (value: string): string => { - if (!isJSExpression(value)) { - if (value.startsWith('"')) { - return value - } - - // Percentage regex, round to 3 decimal places - if (/^\s*[+-]?(?:\d+(?:\.\d+)?|\.\d+)\s*%\s*$/.test(value)) { - const roundedPercentage = Number(value.replace('%', '')).toFixed(3).replace(/\.?0+$/, '') - - return `"${roundedPercentage}%"` - } - - if (isNumber(value)) { - return value - } - - return `"${value.trim()}"` - } - - if (!value.includes(FN_DECLARATION)) { - if (isFunction(value)) { - const [fnName] = value.split('(') - - if (fnName === undefined) { - return value - } - - const args = pipe(value)( - x => x - .replace(fnName, '') - .replace('(', '') - .slice(0, -1) - .trim(), - x => smartSplit(x, /[,\s]+/), - x => x.map(token => { - if (token.endsWith(',')) { - return token.slice(0, -1) - } - - return token - }), - x => x.map(toJSExpression), - ) - - return [ - fnName, - '(', - args.join(','), - ')', - ].join('') - } - - if (!isValidJSValue(value)) { - const tokens = smartSplit(value).map(token => { - if (isNumber(token)) { - return token - } - - if (isFunction(token)) { - return toJSExpression(token) - } - - const parsedToken = pipe(token)( - x => x.replace(',', ''), - x => { - if (x.includes('??')) { - return x.split(' ?? ').map(toJSExpression).join(' ?? ') - } - - return toJSExpression(x) - }, - ) - - if (parsedToken.startsWith('"')) { - return [ - parsedToken.slice(1, -1), - token.includes(',') ? ',' : '', - ].join('') - } - - return [ - '${', - parsedToken, - '}', - token.includes(',') ? ',' : '', - ].join('') - }) - - return `\`${tokens.join(' ')}\`` - } - - return value - } - - const [, after] = value.split(FN_DECLARATION) - - if (after === undefined) { - return value - } - - try { - return `${FN_DECLARATION} ${serialize(JSON.parse(after.replace('}', '')))} }` - } catch { - return `${FN_DECLARATION} ${serialize(after.replace('}', ''))} }` - } -} - -const serialize = (value: any): string => { - switch (typeof value) { - case 'object': { - if (Array.isArray(value)) { - return [ - '[', - value.map(serialize).join(', '), - ']', - ].join('') - } - - if (value === null) { - return 'null' - } - - return [ - '({', - Object.entries(value).map(([key, value]) => { - // Always serialize className as string - if (key === 'className') { - return `"${key}": "${String(value)}"` - } - - return `"${key}": ${serialize(value)}` - }).join(', '), - '})', - ].join('') - } - case 'string': - return toJSExpression(addMissingSpaces(value)) - default: - return String(value) - } -} - -export const serializeJS = (stylesheet: Record, serializer: (key: string, value: string) => string) => { - // eslint-disable-next-line prefer-template - const serializedStylesheet = Object.entries(stylesheet).map(([key, value]) => { - const stringifiedValue = isNumber(value) - ? String(value) - : serialize(value) - - return serializer(key, stringifiedValue) - }).join(',') + ',' - - const js = `${serializedStylesheet}` - - try { - // eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func - new Function(`function validateJS() { const obj = ({ ${js} }) }`) - } catch { - Logger.error('Failed to create virtual js') - return '' - } - - return js -} diff --git a/packages/uniwind/src/metro/utils/common.ts b/packages/uniwind/src/metro/utils/common.ts index a163b276..f662f10f 100644 --- a/packages/uniwind/src/metro/utils/common.ts +++ b/packages/uniwind/src/metro/utils/common.ts @@ -40,10 +40,6 @@ export const smartSplit = (str: string, separator = ' ' as string | RegExp) => { ) } -export const percentageToFloat = (value: string) => { - return Number(value.replace('%', '')) / 100 -} - export const addMissingSpaces = (str: string) => pipe(str)( x => x.trim(), @@ -53,3 +49,31 @@ export const addMissingSpaces = (str: string) => ) export const uniq = (arr: Array) => Array.from(new Set(arr)) + +export const isValidJSValue = (jsValueString: string) => { + try { + // eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func + new Function(`const test = ${jsValueString}`) + + return true + } catch { + return false + } +} + +export const shouldBeSerialized = (value: string) => { + if (value.includes('-')) { + return value.split('-').some(shouldBeSerialized) + } + + return [ + isNumber(value), + value.startsWith('this['), + value.startsWith('rt.'), + /[*/+-]/.test(value), + value.includes('"'), + value.includes(' '), + ].some(Boolean) +} + +export const roundToPrecision = (value: number, precision: number) => parseFloat(value.toFixed(precision)) diff --git a/packages/uniwind/src/metro/utils/index.ts b/packages/uniwind/src/metro/utils/index.ts index 89a3196b..0dee13a6 100644 --- a/packages/uniwind/src/metro/utils/index.ts +++ b/packages/uniwind/src/metro/utils/index.ts @@ -1 +1,2 @@ export * from './common' +export * from './serialize' diff --git a/packages/uniwind/src/metro/utils/serialize.ts b/packages/uniwind/src/metro/utils/serialize.ts new file mode 100644 index 00000000..60dd995d --- /dev/null +++ b/packages/uniwind/src/metro/utils/serialize.ts @@ -0,0 +1,115 @@ +import { Logger } from '../logger' +import { isNumber, isValidJSValue, pipe, roundToPrecision, smartSplit } from './common' + +const parseStringValue = (value: string) => { + if (isValidJSValue(value)) { + return value + } + + if (value.startsWith('function')) { + return value + } + + const tokens = smartSplit(value) + const parsedTokens = tokens.map(token => { + // String literals + if (token.startsWith('"')) { + return token.replace(/"/g, '') + } + + // Plain words + if (/^[a-z]+$/i.test(token.replace(/,/g, ''))) { + return token + } + + // Numbers + if (isNumber(token)) { + return token + } + + // Expressions that need to be wrapped with ${} + const endsWithComma = token.endsWith(',') + + return [ + '${', + endsWithComma ? token.slice(0, -1) : token, + '}', + endsWithComma ? ',' : '', + ].join('') + }) + + return [ + '`', + parsedTokens.join(' '), + '`', + ].join('') +} + +export const serialize = (value: any): string => { + const typeOfValue = typeof value + + switch (typeOfValue) { + case 'string': + return parseStringValue(value) + case 'symbol': + return String(value) + case 'number': + case 'bigint': + return String(roundToPrecision(value, 2)) + case 'boolean': + return value.toString() + case 'object': + if (value === null) { + return 'null' + } + + if (Array.isArray(value)) { + return [ + '[', + value.map(serialize).join(', '), + ']', + ].join('') + } + + return [ + '{', + Object.entries(value).map(([key, val]) => { + const serializedKey = isNumber(key) ? key : `"${key}"` + + return `${serializedKey}: ${serialize(val)}` + }).join(', '), + '}', + ].join('') + case 'function': + case 'undefined': + return '' + default: + typeOfValue satisfies never + return '' + } +} + +export const serializeJSObject = (obj: Record, serializer: (key: string, value: string) => string) => { + const serializedObject = pipe(obj)( + Object.entries, + entries => entries.map(([key, value]) => serializer(key, serialize(value))), + entries => entries.join(','), + result => { + if (result === '') { + return '' + } + + return `${result},` + }, + ) + + try { + // eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func + new Function(`function validateJS() { const obj = ({ ${serializedObject} }) }`) + } catch { + Logger.error('Failed to create virtual js') + return '' + } + + return serializedObject +} From 0bf1d69bfaae14a84adaa97fa7678dac4b396541 Mon Sep 17 00:00:00 2001 From: Hubert Bieszczad Date: Tue, 28 Oct 2025 11:25:02 +0100 Subject: [PATCH 2/3] chore: fix issues after refactoring --- .../uniwind/src/core/native/native-utils.ts | 2 +- packages/uniwind/src/core/types.ts | 2 +- .../src/metro/addMetaToStylesTemplate.ts | 24 +++++++++++---- .../uniwind/src/metro/processor/functions.ts | 29 +++++++++---------- 4 files changed, 34 insertions(+), 23 deletions(-) diff --git a/packages/uniwind/src/core/native/native-utils.ts b/packages/uniwind/src/core/native/native-utils.ts index affd49ac..c322520f 100644 --- a/packages/uniwind/src/core/native/native-utils.ts +++ b/packages/uniwind/src/core/native/native-utils.ts @@ -1,7 +1,7 @@ import { formatHex, formatHex8, interpolate, parse } from 'culori' import type { UniwindRuntime } from '../types' -export const colorMix = (color: string, mixColor: string, weight: number | string) => { +export const colorMix = (color: string, weight: number | string, mixColor: string) => { const parsedWeight = typeof weight === 'string' ? parseFloat(weight) / 100 : weight // Change alpha diff --git a/packages/uniwind/src/core/types.ts b/packages/uniwind/src/core/types.ts index f6fd21a7..343a711a 100644 --- a/packages/uniwind/src/core/types.ts +++ b/packages/uniwind/src/core/types.ts @@ -50,7 +50,7 @@ export type UniwindRuntime = { hairlineWidth: number pixelRatio: (value: number) => number fontScale: (value: number) => number - colorMix: (color: string, mixColor: string, weight: number | string) => string + colorMix: (color: string, weight: number | string, mixColor: string) => string cubicBezier: (x1: number, y1: number, x2: number, y2: number) => string lightDark: (light: string, dark: string) => string } diff --git a/packages/uniwind/src/metro/addMetaToStylesTemplate.ts b/packages/uniwind/src/metro/addMetaToStylesTemplate.ts index 09bf1a7a..93e23070 100644 --- a/packages/uniwind/src/metro/addMetaToStylesTemplate.ts +++ b/packages/uniwind/src/metro/addMetaToStylesTemplate.ts @@ -15,6 +15,18 @@ const extractVarsFromString = (value: string) => { }) } +const makeSafeForSerialization = (value: any) => { + if (value === null) { + return null + } + + if (typeof value === 'string') { + return `"${value}"` + } + + return value +} + export const addMetaToStylesTemplate = (Processor: ProcessorBuilder, currentPlatform: Platform) => { const stylesheetsEntries = Object.entries(Processor.stylesheets as StyleSheetTemplate) .map(([className, stylesPerMediaQuery]) => { @@ -91,18 +103,20 @@ export const addMetaToStylesTemplate = (Processor: ProcessorBuilder, currentPlat entries, minWidth, maxWidth, - theme, - orientation, + theme: makeSafeForSerialization(theme), + orientation: makeSafeForSerialization(orientation), rtl, - colorScheme, + colorScheme: makeSafeForSerialization(colorScheme), native: platform !== null, dependencies, index, - className: `"${className}"`, + className: makeSafeForSerialization(className), active, focus, disabled, - importantProperties: importantProperties?.map(property => property.startsWith('--') ? property : toCamelCase) ?? [], + importantProperties: importantProperties + ?.map(property => property.startsWith('--') ? property : toCamelCase) + .map(makeSafeForSerialization) ?? [], complexity: [ minWidth !== 0, theme !== null, diff --git a/packages/uniwind/src/metro/processor/functions.ts b/packages/uniwind/src/metro/processor/functions.ts index 2b7d80d2..f9d4bd41 100644 --- a/packages/uniwind/src/metro/processor/functions.ts +++ b/packages/uniwind/src/metro/processor/functions.ts @@ -102,7 +102,7 @@ export class Functions { } if (['skewX', 'skewY'].includes(fn.name)) { - return `${fn.name}(${this.Processor.CSS.processValue(fn.arguments)})` + return `"${fn.name}(${this.Processor.CSS.processValue(fn.arguments)})"` } if (fn.name === 'hairlineWidth') { @@ -141,7 +141,7 @@ export class Functions { private tryEval(value: string) { // Match units like %, deg, rad, grad, turn that are not preceded by letters or hyphens - const units = Array.from(value.match(/(? = fn.arguments.map(arg => this.Processor.CSS.processValue(arg)).reverse() - const color = tokens.at(3) - const alpha = tokens.at(2) - const mixColor = tokens.at(0) - - return [ - 'rt.colorMix(', - color, - ', ', - mixColor, - ', ', - alpha, - ')', - ].join('') + const tokens = fn.arguments + .map(arg => + pipe(arg)( + x => this.Processor.CSS.processValue(x), + String, + x => x.trim(), + ) + ) + .filter(token => !['', ',', 'in', 'srgb', 'rgb', 'hsl', 'hwb', 'lab', 'lch', 'oklab', 'oklch'].includes(token.replace(/"/g, ''))) + + return `rt.colorMix(${tokens.join(', ')})` } } From 0a991351b4b377b4878523432cea05d40ce0ad5e Mon Sep 17 00:00:00 2001 From: Hubert Bieszczad Date: Tue, 28 Oct 2025 11:28:25 +0100 Subject: [PATCH 3/3] chore: minor corrections --- packages/uniwind/src/core/native/native-utils.ts | 4 +++- packages/uniwind/src/metro/processor/functions.ts | 10 ++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/packages/uniwind/src/core/native/native-utils.ts b/packages/uniwind/src/core/native/native-utils.ts index c322520f..334406a5 100644 --- a/packages/uniwind/src/core/native/native-utils.ts +++ b/packages/uniwind/src/core/native/native-utils.ts @@ -2,7 +2,9 @@ import { formatHex, formatHex8, interpolate, parse } from 'culori' import type { UniwindRuntime } from '../types' export const colorMix = (color: string, weight: number | string, mixColor: string) => { - const parsedWeight = typeof weight === 'string' ? parseFloat(weight) / 100 : weight + const parsedWeight = typeof weight === 'string' + ? parseFloat(weight) / 100 + : weight // Change alpha if (mixColor === '#00000000') { diff --git a/packages/uniwind/src/metro/processor/functions.ts b/packages/uniwind/src/metro/processor/functions.ts index f9d4bd41..6a8f563f 100644 --- a/packages/uniwind/src/metro/processor/functions.ts +++ b/packages/uniwind/src/metro/processor/functions.ts @@ -141,7 +141,11 @@ export class Functions { private tryEval(value: string) { // Match units like %, deg, rad, grad, turn that are not preceded by letters or hyphens - const units = Array.from(value.replace(/"/g, '').match(/(?