diff --git a/.config/jest.config.cjs b/.config/jest.config.cjs index 66b8c1a..f4e425f 100644 --- a/.config/jest.config.cjs +++ b/.config/jest.config.cjs @@ -6,4 +6,5 @@ const jestExpo = require("jest-expo/jest-preset"); module.exports = { ...jestExpo, testPathIgnorePatterns: ["dist/"], + setupFilesAfterEnv: ["./.config/jest.setup.js"], }; diff --git a/.config/jest.setup.js b/.config/jest.setup.js index 17ef774..53b5b12 100644 --- a/.config/jest.setup.js +++ b/.config/jest.setup.js @@ -1,3 +1,3 @@ -// import { setUpTests } from "react-native-reanimated"; +import { setUpTests } from "react-native-reanimated"; -// setUpTests(); +setUpTests(); diff --git a/example/src/App.tsx b/example/src/App.tsx index 742a593..e8de0c3 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -6,7 +6,7 @@ export default function App() { return ( <> - + Test Component diff --git a/src/compiler/compiler.types.ts b/src/compiler/compiler.types.ts index d29eb77..78e89e5 100644 --- a/src/compiler/compiler.types.ts +++ b/src/compiler/compiler.types.ts @@ -1,11 +1,6 @@ /* eslint-disable */ import type { Debugger } from "debug"; -import type { - AnimationDirection, - AnimationFillMode, - AnimationPlayState, - MediaFeatureNameFor_MediaFeatureId, -} from "lightningcss"; +import type { MediaFeatureNameFor_MediaFeatureId } from "lightningcss"; import { VAR_SYMBOL } from "../runtime/native/reactivity"; @@ -147,134 +142,12 @@ export type InlineVariable = { [key: string]: unknown | undefined; }; -/****************************** Animations V1 ******************************/ - -/** - * An animation with a fallback style value - */ -export type AnimationWithDefault_V1 = - | [AnimationRule_V1] - | [AnimationRule_V1, StyleFunction]; - -/** - * A CSS Animation rule - */ -export interface AnimationRule_V1 { - /** - * The animation delay. - */ - de?: number[]; - /** - * The direction of the animation. - */ - di?: AnimationDirection[]; - /** - * The animation duration. - */ - du?: number[]; - /** - * The animation fill mode. - */ - f?: AnimationFillMode[]; - /** - * The number of times the animation will run. - */ - i?: number[]; - /** - * The animation name. - */ - n?: string[]; - /** - * The current play state of the animation. - */ - p?: AnimationPlayState[]; - /** - * The animation timeline. - */ - t?: never[]; - /** - * The easing function for the animation. - */ - e?: EasingFunction[]; -} - -export type AnimationKeyframes_V1 = - | [AnimationInterpolation_V1[]] - | [AnimationInterpolation_V1[], AnimationEasing[]]; - -export type AnimationEasing = number | [number, EasingFunction]; - -export type AnimationInterpolation_V1 = - | [string, number[], StyleDescriptor[]] - | [string, number[], StyleDescriptor[], number] - | [string, number[], StyleDescriptor[], number, AnimationInterpolationType]; - -export type AnimationInterpolationType = "color" | "%" | undefined; - -export type EasingFunction = - | "linear" - | "ease" - | "ease-in" - | "ease-out" - | "ease-in-out" - | { - type: "cubic-bezier"; - /** - * The x-position of the first point in the curve. - */ - x1: number; - /** - * The x-position of the second point in the curve. - */ - x2: number; - /** - * The y-position of the first point in the curve. - */ - y1: number; - /** - * The y-position of the second point in the curve. - */ - y2: number; - } - | { - type: "steps"; - /** - * The number of intervals in the function. - */ - c: number; - /** - * The step position. - */ - p?: "start" | "end" | "jump-none" | "jump-both"; - }; - /****************************** Animations V2 ******************************/ export type Animation_V2 = [string, AnimationKeyframes_V2[]]; export type AnimationRecord = Record; export type AnimationKeyframes_V2 = [string | number, StyleDeclaration[]]; -/****************************** Transitions *****************************/ - -export type TransitionRule = { - /** - * Delay before the transition starts in milliseconds. - */ - de?: number[]; - /** - * Duration of the transition in milliseconds. - */ - du?: number[]; - /** - * Property to transition. - */ - p?: string[]; - /** - * Easing function for the transition. - */ - e?: EasingFunction[]; -}; - /****************************** Conditions ******************************/ export type MediaCondition = @@ -368,7 +241,7 @@ export type LoggerOptions = { export interface CompilerCollection extends CompilerOptions { features: FeatureFlagRecord; rules: Map; - keyframes: Map; + keyframes: Map; darkMode?: string | null; rootVariables: VariableRecord; universalVariables: VariableRecord; diff --git a/src/compiler/keyframes.ts b/src/compiler/keyframes.ts index 1c719e2..bf9e2a6 100644 --- a/src/compiler/keyframes.ts +++ b/src/compiler/keyframes.ts @@ -4,7 +4,7 @@ import type { KeyframesRule, } from "lightningcss"; -import type { EasingFunction, StyleDescriptor } from "./compiler.types"; +import type { StyleDescriptor } from "./compiler.types"; import { parseDeclaration } from "./declarations"; import type { StylesheetBuilder } from "./stylesheet"; @@ -18,8 +18,8 @@ export function parseIterationCount( export function parseEasingFunction( value: CSSEasingFunction[], -): StyleDescriptor[] { - return value.map((value): EasingFunction => { +): StyleDescriptor { + const easingFn = value.map((value) => { switch (value.type) { case "linear": case "ease": @@ -28,15 +28,20 @@ export function parseEasingFunction( case "ease-in-out": return value.type; case "cubic-bezier": - return value; + return [ + {}, + "cubic-bezier", + [value.x1, value.y1, value.x2, value.y2], + ] as const; case "steps": - return { - type: "steps", - c: value.count, - p: value.position?.type, - }; + return [{}, "steps", [value.count, value.position?.type]] as const; } }) as StyleDescriptor[]; + + if (easingFn.length === 1) { + return easingFn[0]; + } + return easingFn; } export function extractKeyFrames( diff --git a/src/compiler/stylesheet.ts b/src/compiler/stylesheet.ts index 64280b2..d180c42 100644 --- a/src/compiler/stylesheet.ts +++ b/src/compiler/stylesheet.ts @@ -266,6 +266,8 @@ export class StylesheetBuilder { } } else if (forceTuple || Array.isArray(propPath)) { declarations.push([value, propPath]); + } else if (Array.isArray(value) && value.some(isStyleFunction)) { + declarations.push([value, propPath]); } else { if (!this.staticDeclarations) { this.staticDeclarations = {}; diff --git a/src/runtime/native/styles/animation.ts b/src/runtime/native/styles/animation.ts index 255a1e5..8da3f3d 100644 --- a/src/runtime/native/styles/animation.ts +++ b/src/runtime/native/styles/animation.ts @@ -1,6 +1,7 @@ /* eslint-disable */ import type { ComponentType } from "react"; +import { applyShorthand } from "../../utils"; import { StyleCollection } from "../injection"; import { weakFamily } from "../reactivity"; import type { StyleFunctionResolver } from "./resolve"; @@ -57,6 +58,7 @@ export const animationShorthand = shorthandHandler( playState, timingFunction, ], + "tuples", ); export const animatedComponentFamily = weakFamily( @@ -92,6 +94,8 @@ export const animation: StyleFunctionResolver = ( return; } + animationShortHandTuples.pop(); + const nameTuple = animationShortHandTuples.find( (tuple) => tuple[1] === "animationName", ); @@ -133,7 +137,7 @@ export const animation: StyleFunctionResolver = ( nameTuple[0] = animation; - return animationShortHandTuples; + return applyShorthand(animationShortHandTuples); }; const advancedTimingFunctions: Record< diff --git a/src/runtime/native/styles/box-shadow.ts b/src/runtime/native/styles/box-shadow.ts index 29c5c88..e6d0786 100644 --- a/src/runtime/native/styles/box-shadow.ts +++ b/src/runtime/native/styles/box-shadow.ts @@ -1,4 +1,4 @@ -import { applyShorthand, isStyleDescriptorArray } from "../../utils"; +import { isStyleDescriptorArray } from "../../utils"; import type { StyleFunctionResolver } from "./resolve"; import { shorthandHandler } from "./shorthand"; @@ -19,6 +19,7 @@ const handler = shorthandHandler( [offsetX, offsetY, blurRadius, color], ], [], + "object", ); export const boxShadow: StyleFunctionResolver = ( @@ -38,17 +39,15 @@ export const boxShadow: StyleFunctionResolver = ( } else if (isStyleDescriptorArray(shadows)) { if (shadows.length === 0) { return []; - } else { + } else if (Array.isArray(shadows[0])) { return shadows - .map((shadow) => { - return applyShorthand( - handler(resolveValue, shadow, get, options), - ); - }) + .map((shadow) => handler(resolveValue, shadow, get, options)) .filter((v) => v !== undefined); + } else { + return handler(resolveValue, shadows, get, options); } } else { - return applyShorthand(handler(resolveValue, shadows, get, options)); + return handler(resolveValue, shadows, get, options); } }); } diff --git a/src/runtime/native/styles/calc.ts b/src/runtime/native/styles/calc.ts index 530ac9a..911c760 100644 --- a/src/runtime/native/styles/calc.ts +++ b/src/runtime/native/styles/calc.ts @@ -30,7 +30,7 @@ export const calc: StyleFunctionResolver = (resolveValue, func) => { } else if (token === ")") { // Resolve all values within the brackets while (ops.length && ops[ops.length - 1] !== "(") { - applyCalcOperator(ops.pop()!, values.pop()!, values.pop()!, values); + applyCalcOperator(ops.pop()!, values.pop(), values.pop(), values); } ops.pop(); } else if (token.endsWith("%")) { @@ -45,7 +45,7 @@ export const calc: StyleFunctionResolver = (resolveValue, func) => { // @ts-ignore calcPrecedence[ops[ops.length - 1]] >= calcPrecedence[token] ) { - applyCalcOperator(ops.pop()!, values.pop()!, values.pop()!, values); + applyCalcOperator(ops.pop()!, values.pop(), values.pop(), values); } ops.push(token); } @@ -54,31 +54,9 @@ export const calc: StyleFunctionResolver = (resolveValue, func) => { return; } } - // case "object": { - // // All values should resolve to a numerical value - // const value = resolveValue(token); - // switch (typeof value) { - // case "number": { - // if (!mode) mode = "number"; - // if (mode !== "number") return; - // values.push(value); - // continue; - // } - // case "string": { - // if (!value.endsWith("%")) { - // return; - // } - // if (!mode) mode = "percentage"; - // if (mode !== "percentage") return; - // values.push(Number.parseFloat(value.slice(0, -1))); - // continue; - // } - // default: - // return; - // } - // } + while (ops.length) { - applyCalcOperator(ops.pop()!, values.pop()!, values.pop()!, values); + applyCalcOperator(ops.pop()!, values.pop(), values.pop(), values); } if (!mode) return; @@ -100,8 +78,8 @@ export const calc: StyleFunctionResolver = (resolveValue, func) => { function applyCalcOperator( operator: string, - b: number, // These are reversed because we pop them off the stack - a: number, + b = 0, // These are reversed because we pop them off the stack + a = 0, values: number[], ) { switch (operator) { diff --git a/src/runtime/native/styles/constants.ts b/src/runtime/native/styles/constants.ts index 6bc8a4e..8de5af9 100644 --- a/src/runtime/native/styles/constants.ts +++ b/src/runtime/native/styles/constants.ts @@ -1 +1,2 @@ export const emVariableName = "__rn-css-em"; +export const ShortHandSymbol = Symbol(); diff --git a/src/runtime/native/styles/shorthand.ts b/src/runtime/native/styles/shorthand.ts index 68b6410..ca074a5 100644 --- a/src/runtime/native/styles/shorthand.ts +++ b/src/runtime/native/styles/shorthand.ts @@ -1,6 +1,7 @@ /* eslint-disable */ -import type { StyleDescriptor } from "../../../compiler"; -import { isStyleDescriptorArray } from "../../utils"; +import { setDeepPath } from "../../utils/objects"; +import { isStyleDescriptorArray } from "../../utils/style-value"; +import { ShortHandSymbol } from "./constants"; import { defaultValues } from "./defaults"; import type { StyleResolver } from "./resolve"; @@ -21,11 +22,10 @@ type ShorthandDefaultValue = readonly [ any, ]; -export const ShortHandSymbol = Symbol(); - export function shorthandHandler( mappings: ShorthandRequiredValue[][], defaults: ShorthandDefaultValue[], + returnType: "shorthandObject" | "tuples" | "object" = "shorthandObject", ): StyleResolver { return (resolve, value, __, { castToArray }) => { let args = isStyleDescriptorArray(value) @@ -76,30 +76,46 @@ export function shorthandHandler( const seenDefaults = new Set(defaults); - return Object.assign( - [ - ...match.map((map, index): StyleDescriptor => { - if (map.length === 3) { - seenDefaults.delete(map); - } - - let value = args[index]; - if (castToArray && value && !Array.isArray(value)) { - value = [value]; - } - - return [value, map[0] as StyleDescriptor]; - }), - ...Array.from(seenDefaults).map((map): StyleDescriptor => { + const tuples = [ + ...match.map((map, index): [unknown, ShorthandRequiredValue[0]] => { + if (map.length === 3) { + seenDefaults.delete(map); + } + + let value = args[index]; + if (castToArray && value && !Array.isArray(value)) { + value = [value]; + } + + return [value, map[0]]; + }), + ...Array.from(seenDefaults).map( + (map): [unknown, ShorthandRequiredValue[0]] => { let value = defaultValues[map[2]] ?? map[2]; if (castToArray && value && !Array.isArray(value)) { value = [value]; } return [value, map[0]]; - }), - ], - { [ShortHandSymbol]: true }, - ); + }, + ), + ]; + + if (returnType === "shorthandObject" || returnType === "object") { + const target: Record = + returnType === "shorthandObject" ? { [ShortHandSymbol]: true } : {}; + + for (const [value, prop] of tuples) { + if (typeof prop === "string") { + target[prop] = value; + } else { + setDeepPath(target, prop, value); + } + } + + return target; + } else { + return tuples; + } }; } diff --git a/src/runtime/runtime.types.ts b/src/runtime/runtime.types.ts index 118e6ba..08214b4 100644 --- a/src/runtime/runtime.types.ts +++ b/src/runtime/runtime.types.ts @@ -7,12 +7,6 @@ import type { ViewStyle, } from "react-native"; -import type { makeMutable, SharedValue } from "react-native-reanimated"; - -import type { - AnimationInterpolation_V1, - AnimationKeyframes_V1, -} from "../compiler"; import type { ComponentPropsDotNotation, ReactComponent } from "./utils"; import type { DotNotation, ResolveDotPath } from "./utils/dot-notation.types"; @@ -122,25 +116,6 @@ export type InlineStyle = | (Record | undefined | null)[] | (() => unknown); -/****************************** Animations ******************************/ - -export type Mutable = ReturnType>; -export type AnimationMutable = Mutable; - -export interface KeyFramesWithStyles { - animation: AnimationKeyframes_V1; - baseStyles: Record; -} - -export type SharedValueInterpolation = [ - SharedValue, - AnimationInterpolation_V1[], -]; - -/****************************** Transitions *****************************/ - -export type Transition = [string | string[], Mutable]; - /********************************* Misc *********************************/ export type Props = Record | undefined | null; diff --git a/src/runtime/utils/objects.ts b/src/runtime/utils/objects.ts index 69e7bf7..7109e5c 100644 --- a/src/runtime/utils/objects.ts +++ b/src/runtime/utils/objects.ts @@ -1,6 +1,6 @@ /* eslint-disable */ +import { ShortHandSymbol } from "../native/styles/constants"; import { transformKeys } from "../native/styles/defaults"; -import { ShortHandSymbol } from "../native/styles/shorthand"; export function getDeepPath(source: any, paths: string | string[] | false) { if (!source) { @@ -33,8 +33,14 @@ export function applyShorthand(value: any) { return; } - const target = {}; - applyValue(target, "", value); + const target: Record = { [ShortHandSymbol]: true }; + + const values = value as [unknown, string][]; + + for (const [value, prop] of values) { + target[prop] = value; + } + return target; } @@ -61,9 +67,8 @@ export function applyValue( } return; } else if (typeof value === "object" && value && ShortHandSymbol in value) { - for (const entry of value) { - setDeepPath(target, entry[1], entry[0]); - } + delete value[ShortHandSymbol]; + Object.assign(target, value); return; } @@ -72,7 +77,7 @@ export function applyValue( export function setDeepPath( target: Record, - paths: string | string[], + paths: string | string[] | readonly string[], value: any, ) { if (typeof paths === "string") {