diff --git a/.changeset/brisk-names-assign.md b/.changeset/brisk-names-assign.md new file mode 100644 index 0000000..c75d876 --- /dev/null +++ b/.changeset/brisk-names-assign.md @@ -0,0 +1,5 @@ +--- +"css-to-react-native": patch +--- + +Speed up transform() on large stylesheets. diff --git a/src/index.ts b/src/index.ts index 31c0612..ee798eb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -87,19 +87,170 @@ export const getStylesForProperty = ( const isRawValue = allowShorthand === false || !(propName in transforms) const value = inputValue.trim() - const propValues = isRawValue - ? { [propName]: transformRawValue(propName, value) } - : transformShorthandValue(propName, value) + if (!isRawValue) return transformShorthandValue(propName, value) + const propValues: Style = {} + propValues[propName] = transformRawValue(propName, value) return propValues } +// Known stylesheet names. Other names are camelized and not retained. +const knownPropertyNames: { [name: string]: string } = Object.create(null) + +for (const name of [ + 'align-content', + 'align-items', + 'align-self', + 'aspect-ratio', + 'backface-visibility', + 'background', + 'background-color', + 'border', + 'border-bottom-color', + 'border-bottom-end-radius', + 'border-bottom-left-radius', + 'border-bottom-right-radius', + 'border-bottom-start-radius', + 'border-bottom-width', + 'border-color', + 'border-curve', + 'border-end-color', + 'border-end-end-radius', + 'border-end-start-radius', + 'border-end-width', + 'border-left-color', + 'border-left-width', + 'border-radius', + 'border-right-color', + 'border-right-width', + 'border-start-color', + 'border-start-end-radius', + 'border-start-start-radius', + 'border-start-width', + 'border-style', + 'border-top-color', + 'border-top-end-radius', + 'border-top-left-radius', + 'border-top-right-radius', + 'border-top-start-radius', + 'border-top-width', + 'border-width', + 'bottom', + 'box-shadow', + 'color', + 'column-gap', + 'direction', + 'display', + 'elevation', + 'end', + 'filter', + 'flex', + 'flex-basis', + 'flex-direction', + 'flex-flow', + 'flex-grow', + 'flex-shrink', + 'flex-wrap', + 'font', + 'font-family', + 'font-size', + 'font-style', + 'font-variant', + 'font-weight', + 'gap', + 'height', + 'include-font-padding', + 'inset', + 'inset-block', + 'inset-block-end', + 'inset-block-start', + 'inset-inline', + 'inset-inline-end', + 'inset-inline-start', + 'justify-content', + 'left', + 'letter-spacing', + 'line-height', + 'margin', + 'margin-block', + 'margin-block-end', + 'margin-block-start', + 'margin-bottom', + 'margin-end', + 'margin-horizontal', + 'margin-inline', + 'margin-inline-end', + 'margin-inline-start', + 'margin-left', + 'margin-right', + 'margin-start', + 'margin-top', + 'margin-vertical', + 'max-height', + 'max-width', + 'min-height', + 'min-width', + 'mix-blend-mode', + 'object-fit', + 'opacity', + 'overflow', + 'overlay-color', + 'padding', + 'padding-block', + 'padding-block-end', + 'padding-block-start', + 'padding-bottom', + 'padding-end', + 'padding-horizontal', + 'padding-inline', + 'padding-inline-end', + 'padding-inline-start', + 'padding-left', + 'padding-right', + 'padding-start', + 'padding-top', + 'padding-vertical', + 'place-content', + 'pointer-events', + 'position', + 'resize-mode', + 'right', + 'row-gap', + 'shadow-color', + 'shadow-offset', + 'shadow-opacity', + 'shadow-radius', + 'start', + 'text-align', + 'text-align-vertical', + 'text-decoration', + 'text-decoration-color', + 'text-decoration-line', + 'text-decoration-style', + 'text-shadow', + 'text-shadow-color', + 'text-shadow-offset', + 'text-shadow-radius', + 'text-transform', + 'tint-color', + 'top', + 'transform', + 'transform-origin', + 'user-select', + 'vertical-align', + 'width', + 'writing-direction', + 'z-index', +]) { + knownPropertyNames[name] = camelizeStyleName(name) +} + export const getPropertyName = (propName: string): string => { + const cached = knownPropertyNames[propName] + if (cached !== undefined) return cached + const isCustomProp = /^--\w+/.test(propName) - if (isCustomProp) { - return propName - } - return camelizeStyleName(propName) + return isCustomProp ? propName : camelizeStyleName(propName) } const transform = ( diff --git a/src/transforms/transform.ts b/src/transforms/transform.ts index e754e65..82c8ff6 100644 --- a/src/transforms/transform.ts +++ b/src/transforms/transform.ts @@ -18,25 +18,33 @@ const singleLengthOrPercent = oneOfTypes([LENGTH, PERCENT]) const singleAngle = oneOfTypes([ANGLE]) const xyTransformFactory = (tokenTypes: TokenDescriptor[]) => - (key: string, valueIfOmitted?: string | number) => - (functionStream: TokenStream): TransformPartValue => { - const x = functionStream.expect(...tokenTypes) + (key: string, valueIfOmitted?: string | number) => { + const keyY = `${key}Y` + const keyX = `${key}X` - let y: string | number | undefined - if (functionStream.hasTokens()) { - functionStream.expect(COMMA) - y = functionStream.expect(...tokenTypes) - } else if (valueIfOmitted !== undefined) { - y = valueIfOmitted - } else { - // Assumption, if x === y, then we can omit XY - // I.e. scale(5) => [{ scale: 5 }] rather than [{ scaleX: 5 }, { scaleY: 5 }] - return x - } + return (functionStream: TokenStream): TransformPartValue => { + const x = functionStream.expect(...tokenTypes) - functionStream.expectEmpty() + let y: string | number | undefined + if (functionStream.hasTokens()) { + functionStream.expect(COMMA) + y = functionStream.expect(...tokenTypes) + } else if (valueIfOmitted !== undefined) { + y = valueIfOmitted + } else { + // Assumption, if x === y, then we can omit XY + // I.e. scale(5) => [{ scale: 5 }] rather than [{ scaleX: 5 }, { scaleY: 5 }] + return x + } - return [{ [`${key}Y`]: y }, { [`${key}X`]: x }] + functionStream.expectEmpty() + + const yPart: TransformStyleValue = {} + yPart[keyY] = y + const xPart: TransformStyleValue = {} + xPart[keyX] = x + return [yPart, xPart] + } } const xyNumber = xyTransformFactory([NUMBER]) const xyLengthOrPercent = xyTransformFactory([LENGTH, PERCENT]) @@ -78,7 +86,9 @@ export default (tokenStream: TokenStream): Style => { let transformedValues = partTransform(functionStream) if (!Array.isArray(transformedValues)) { - transformedValues = [{ [functionName]: transformedValues }] + const part: TransformStyleValue = {} + part[functionName] = transformedValues + transformedValues = [part] } transforms = transformedValues.concat(transforms) diff --git a/src/transforms/util.ts b/src/transforms/util.ts index 37e9d44..f4672f8 100644 --- a/src/transforms/util.ts +++ b/src/transforms/util.ts @@ -10,19 +10,24 @@ import { } from '../tokenTypes' import type { ParsedShadow, ShadowOffset, Style } from '../types' -export const directionFactory = - ({ - types = [LENGTH, UNSUPPORTED_LENGTH_UNIT, PERCENT], - directions = ['Top', 'Right', 'Bottom', 'Left'], - prefix = '', - suffix = '', - }: { - types?: TokenDescriptor[] - directions?: string[] - prefix?: string - suffix?: string - }) => - (tokenStream: TokenStream): Style => { +export const directionFactory = ({ + types = [LENGTH, UNSUPPORTED_LENGTH_UNIT, PERCENT], + directions = ['Top', 'Right', 'Bottom', 'Left'], + prefix = '', + suffix = '', +}: { + types?: TokenDescriptor[] + directions?: string[] + prefix?: string + suffix?: string +}) => { + // Stable keys. Building a new string per value makes the result object slow. + const key0 = `${prefix}${directions[0]}${suffix}` + const key1 = `${prefix}${directions[1]}${suffix}` + const key2 = `${prefix}${directions[2]}${suffix}` + const key3 = `${prefix}${directions[3]}${suffix}` + + return (tokenStream: TokenStream): Style => { const values: Array = [] // borderWidth doesn't currently allow a percent value, but may do in the future @@ -37,15 +42,14 @@ export const directionFactory = const [top, right = top, bottom = top, left = right] = values - const keyFor = (n: number) => `${prefix}${directions[n]}${suffix}` - - return { - [keyFor(0)]: top, - [keyFor(1)]: right, - [keyFor(2)]: bottom, - [keyFor(3)]: left, - } + const style: Style = {} + style[key0] = top + style[key1] = right + style[key2] = bottom + style[key3] = left + return style } +} export const parseShadowOffset = (tokenStream: TokenStream): ShadowOffset => { const width = tokenStream.expect(LENGTH)