diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8c0b935..fee4ceb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -180,17 +180,20 @@ _Do not create pull requests for these reasons:_ > [!IMPORTANT] > The Metro transformer does not fast refresh. After you make a change, you will need to recompile the project and restart the Metro server with a clean cache. -> -> ```bash -> # Build the project -> yarn build -> -> # Start the Metro server with a clean cache -> yarn example start --clean -> ``` Development on the Metro transformer is done by running the example project. +Debugging with breakpoints is supported if you run the project in VSCode's JavaScript Debug Terminal, or by setting the NodeJS debugger environment variables + +### Compiler + +> [!IMPORTANT] +> The Metro transformer does not fast refresh. After you make a change, you will need to recompile the project and restart the Metro server with a clean cache. + +The easiest way to debug the compiler is through Test Driven Development (TDD). The tests are located in the `src/__tests__/compiler` directory. + +You can use the JavaScript debugger, but you will need to use VSCode's JavaScript Debug Terminal, or set the NodeJS debugger environment variables to enable the debugger. + ### Babel Plugin > [!IMPORTANT] diff --git a/example/src/App.tsx b/example/src/App.tsx index 4cfb29e..742a593 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -6,7 +6,9 @@ export default function App() { return ( <> - Test Component + + Test Component + ); diff --git a/src/__tests__/vendor/tailwind.test.tsx b/src/__tests__/vendor/tailwind.test.tsx new file mode 100644 index 0000000..cabee1f --- /dev/null +++ b/src/__tests__/vendor/tailwind.test.tsx @@ -0,0 +1,113 @@ +import { View } from "react-native-css/components/View"; +import { registerCSS, render, screen, testID } from "react-native-css/jest"; + +/** + * Tailwind CSS utilities + * + * These tests are designed to ensure that complex Tailwind CSS utilities are compiled correctly. + * For the full Tailwind CSS test suite, see the Nativewind repository. + */ + +test("box-shadow", () => { + const compiled = registerCSS(` +.shadow-xl { + --tw-shadow: 0 20px 25px -5px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 8px 10px -6px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); +} +.shadow-red-500 { + --tw-shadow-color: oklch(63.7% 0.237 25.331); + @supports (color: color-mix(in lab, red, red)) { + --tw-shadow-color: color-mix(in oklab, var(--color-red-500) var(--tw-shadow-alpha), transparent); + } +} + `); + + expect(compiled).toStrictEqual({ + s: [ + [ + "shadow-xl", + [ + { + d: [ + [ + [ + {}, + "@boxShadow", + [ + [{}, "var", "tw-inset-shadow", 1], + [{}, "var", "tw-inset-ring-shadow", 1], + [{}, "var", "tw-ring-offset-shadow", 1], + [{}, "var", "tw-ring-shadow", 1], + [{}, "var", "tw-shadow", 1], + ], + 1, + ], + "boxShadow", + 1, + ], + ], + dv: 1, + s: [1, 1], + v: [ + [ + "tw-shadow", + [ + [ + 0, + 20, + 25, + -5, + [{}, "var", ["tw-shadow-color", "#0000001a"], 1], + ], + [ + 0, + 8, + 10, + -6, + [{}, "var", ["tw-shadow-color", "#0000001a"], 1], + ], + ], + ], + ], + }, + ], + ], + [ + "shadow-red-500", + [ + { + s: [2, 1], + v: [["tw-shadow-color", "#fb2c36"]], + }, + ], + ], + ], + }); + + render(); + const component = screen.getByTestId(testID); + + expect(component.type).toBe("View"); + expect(component.props).toStrictEqual({ + children: undefined, + style: { + boxShadow: [ + { + blurRadius: 25, + color: "#fb2c36", + offsetX: 0, + offsetY: 20, + spreadDistance: -5, + }, + { + blurRadius: 10, + color: "#fb2c36", + offsetX: 0, + offsetY: 8, + spreadDistance: -6, + }, + ], + }, + testID, + }); +}); diff --git a/src/compiler/__tests__/compiler.test.tsx b/src/compiler/__tests__/compiler.test.tsx index 5694084..b7f610e 100644 --- a/src/compiler/__tests__/compiler.test.tsx +++ b/src/compiler/__tests__/compiler.test.tsx @@ -240,6 +240,6 @@ test("breaks apart comma separated variables", () => { `); expect(compiled).toStrictEqual({ - vr: [["test", [[["blue"], ["green"]]]]], + vr: [["test", [["blue", "green"]]]], }); }); diff --git a/src/compiler/compiler.ts b/src/compiler/compiler.ts index 6f1ef08..bf70bba 100644 --- a/src/compiler/compiler.ts +++ b/src/compiler/compiler.ts @@ -68,6 +68,7 @@ export function compile( }, StyleSheetExit(sheet) { logger(`Found ${sheet.rules.length} rules to process`); + logger(JSON.stringify(sheet.rules, null, 2)); for (const rule of sheet.rules) { // Extract the style declarations and animations from the current rule diff --git a/src/compiler/compiler.types.ts b/src/compiler/compiler.types.ts index cbfd25c..d29eb77 100644 --- a/src/compiler/compiler.types.ts +++ b/src/compiler/compiler.types.ts @@ -125,12 +125,12 @@ export type StyleFunction = | [ Record, string, // string - undefined | StyleDescriptor[], // arguments + StyleDescriptor, // arguments ] | [ Record, string, // string - undefined | StyleDescriptor[], // arguments + StyleDescriptor, // arguments 1, // Should process after styles have been calculated ]; @@ -144,7 +144,7 @@ export type LightDarkVariable = export type InlineVariable = { [VAR_SYMBOL]: "inline"; - [key: string]: StyleDescriptor | undefined; + [key: string]: unknown | undefined; }; /****************************** Animations V1 ******************************/ diff --git a/src/compiler/declarations.ts b/src/compiler/declarations.ts index 79df06e..6d2060b 100644 --- a/src/compiler/declarations.ts +++ b/src/compiler/declarations.ts @@ -35,7 +35,6 @@ import type { UnresolvedColor, } from "lightningcss"; -import { isStyleDescriptorArray } from "../runtime/utils"; import type { StyleDescriptor, StyleFunction } from "./compiler.types"; import { parseEasingFunction, parseIterationCount } from "./keyframes"; import { toRNProperty } from "./selectors"; @@ -597,19 +596,19 @@ function parseTransform( value.flatMap((t): StyleDescriptor[] => { switch (t.type) { case "perspective": - return [[{}, "@perspective", [parseLength(t.value, builder)]]]; + return [[{}, "@perspective", parseLength(t.value, builder)]]; case "translate": return [ [ {}, "@translateX", - [parseLengthOrCoercePercentageToRuntime(t.value[0], builder)], + parseLengthOrCoercePercentageToRuntime(t.value[0], builder), ], [ [ {}, "@translateY", - [parseLengthOrCoercePercentageToRuntime(t.value[1], builder)], + parseLengthOrCoercePercentageToRuntime(t.value[1], builder), ], ], ]; @@ -618,7 +617,7 @@ function parseTransform( [ {}, "@translateX", - [parseLengthOrCoercePercentageToRuntime(t.value, builder)], + parseLengthOrCoercePercentageToRuntime(t.value, builder), ], ]; case "translateY": @@ -626,35 +625,35 @@ function parseTransform( [ {}, "@translateY", - [parseLengthOrCoercePercentageToRuntime(t.value, builder)], + parseLengthOrCoercePercentageToRuntime(t.value, builder), ], ]; case "rotate": - return [[{}, "@rotate", [parseAngle(t.value, builder)]]]; + return [[{}, "@rotate", parseAngle(t.value, builder)]]; case "rotateX": - return [[{}, "@rotateX", [parseAngle(t.value, builder)]]]; + return [[{}, "@rotateX", parseAngle(t.value, builder)]]; case "rotateY": - return [[{}, "@rotateY", [parseAngle(t.value, builder)]]]; + return [[{}, "@rotateY", parseAngle(t.value, builder)]]; case "rotateZ": - return [[{}, "@rotateZ", [parseAngle(t.value, builder)]]]; + return [[{}, "@rotateZ", parseAngle(t.value, builder)]]; case "scale": return [ - [{}, "@scaleX", [parseLength(t.value[0], builder)]], - [{}, "@scaleY", [parseLength(t.value[0], builder)]], + [{}, "@scaleX", parseLength(t.value[0], builder)], + [{}, "@scaleY", parseLength(t.value[1], builder)], ]; case "scaleX": - return [[{}, "scaleX", [parseLength(t.value, builder)]]]; + return [[{}, "scaleX", parseLength(t.value, builder)]]; case "scaleY": - return [[{}, "scaleY", [parseLength(t.value, builder)]]]; + return [[{}, "scaleY", parseLength(t.value, builder)]]; case "skew": return [ - [{}, "skewX", [parseAngle(t.value[0], builder)]], - [{}, "skewY", [parseAngle(t.value[0], builder)]], + [{}, "skewX", parseAngle(t.value[0], builder)], + [{}, "skewY", parseAngle(t.value[1], builder)], ]; case "skewX": - return [[{}, "skewX", [parseAngle(t.value, builder)]]]; + return [[{}, "skewX", parseAngle(t.value, builder)]]; case "skewY": - return [[{}, "skewY", [parseAngle(t.value, builder)]]]; + return [[{}, "skewY", parseAngle(t.value, builder)]]; case "translateZ": case "translate3d": case "scaleZ": @@ -676,12 +675,12 @@ function parseTranslate( builder.addDescriptor("translateX", [ {}, "translateX", - [parseTranslateProp(value, "x", builder)], + parseTranslateProp(value, "x", builder), ]); builder.addDescriptor("translateY", [ {}, - "translateX", - [parseTranslateProp(value, "y", builder)], + "translateY", + parseTranslateProp(value, "y", builder), ]); } @@ -692,17 +691,17 @@ function parseRotate( builder.addDescriptor("rotateX", [ {}, "rotateX", - [parseAngle(value.x, builder)], + parseAngle(value.x, builder), ]); builder.addDescriptor("rotateY", [ {}, "rotateY", - [parseAngle(value.y, builder)], + parseAngle(value.y, builder), ]); builder.addDescriptor("rotateZ", [ {}, "rotateZ", - [parseAngle(value.z, builder)], + parseAngle(value.z, builder), ]); } @@ -713,12 +712,12 @@ function parseScale( builder.addDescriptor("scaleX", [ {}, "scaleX", - [parseScaleValue(value, "x", builder)], + parseScaleValue(value, "x", builder), ]); builder.addDescriptor("scaleY", [ {}, "scaleY", - [parseScaleValue(value, "y", builder)], + parseScaleValue(value, "y", builder), ]); } @@ -813,10 +812,7 @@ export function parseDeclarationUnparsed( * Unparsed shorthand properties need to be parsed at runtime */ if (needsRuntimeParsing.has(property)) { - let args = parseUnparsed(declaration.value.value, builder); - if (!isStyleDescriptorArray(args)) { - args = [args]; - } + const args = parseUnparsed(declaration.value.value, builder); if (property === "animation") { builder.addDescriptor("animation", [ @@ -862,7 +858,7 @@ export function parseDeclarationCustom( export function reduceParseUnparsed( tokenOrValues: TokenOrValue[], builder: StylesheetBuilder, -): StyleDescriptor[] | undefined { +): StyleDescriptor { const result = tokenOrValues .map((tokenOrValue) => parseUnparsed(tokenOrValue, builder)) .filter((v) => v !== undefined); @@ -871,8 +867,8 @@ export function reduceParseUnparsed( return undefined; } - let currentGroup: StyleDescriptor[] = []; - const groups: StyleDescriptor[][] = [currentGroup]; + let currentGroup: StyleDescriptor = []; + let groups: StyleDescriptor[] = [currentGroup]; for (const value of result) { if ((value as unknown) === CommaSeparator) { @@ -883,6 +879,26 @@ export function reduceParseUnparsed( } } + groups = groups.flatMap((group): StyleDescriptor[] => { + if (!Array.isArray(group)) { + return []; + } + + if (group.length === 0) { + return []; + } else if (group.length === 1) { + const first = group[0]; + + if (first === undefined) { + return []; + } else { + return [first]; + } + } else { + return [group]; + } + }); + return groups.length === 1 ? groups[0] : groups; } @@ -890,8 +906,11 @@ export function unparsedFunction( token: Extract, builder: StylesheetBuilder, ): StyleFunction { - const args = reduceParseUnparsed(token.value.arguments, builder); - return [{}, token.value.name, args]; + return [ + {}, + token.value.name, + reduceParseUnparsed(token.value.arguments, builder), + ]; } /** @@ -930,7 +949,7 @@ export function parseUnparsed( if (Array.isArray(tokenOrValue)) { const args = reduceParseUnparsed(tokenOrValue, builder); if (!args) return; - if (args.length === 1) return args[0]; + if (Array.isArray(args) && args.length === 1) return args[0]; return args; } @@ -939,10 +958,10 @@ export function parseUnparsed( return parseUnresolvedColor(tokenOrValue.value, builder); } case "var": { - const args: StyleDescriptor[] = [tokenOrValue.value.name.ident.slice(2)]; + let args: StyleDescriptor = tokenOrValue.value.name.ident.slice(2); const fallback = parseUnparsed(tokenOrValue.value.fallback, builder); if (fallback !== undefined) { - args.push(fallback); + args = [args, fallback]; } return [{}, "var", args, 1]; @@ -1119,12 +1138,12 @@ export function parseLength( if (typeof inlineRem === "number") { return length.value * inlineRem; } else { - return [{}, "rem", [length.value]]; + return [{}, "rem", length.value]; } case "vw": case "vh": case "em": - return [{}, length.unit, [length.value], 1]; + return [{}, length.unit, length.value, 1]; case "in": case "cm": case "mm": @@ -2025,21 +2044,35 @@ export function parseTextAlign( } export function parseBoxShadow( - _: DeclarationType<"box-shadow">, - _builder: StylesheetBuilder, + { value }: DeclarationType<"box-shadow">, + builder: StylesheetBuilder, ) { - return undefined; - - // return value.map( - // (shadow): BoxShadowValue => ({ - // color: parseColor(shadow.color, builder), - // offsetX: parseLength(shadow.xOffset, builder) as number, - // offsetY: parseLength(shadow.yOffset, builder) as number, - // blurRadius: parseLength(shadow.blur, builder) as number, - // spreadDistance: parseLength(shadow.spread, builder) as number, - // inset: shadow.inset, - // }), - // ); + for (const [index, shadow] of value.entries()) { + builder.addDescriptor( + `boxShadow.[${index}].color`, + parseColor(shadow.color, builder), + ); + builder.addDescriptor( + `boxShadow.[${index}].offsetX`, + parseLength(shadow.xOffset, builder), + ); + builder.addDescriptor( + `boxShadow.[${index}].offsetY`, + parseLength(shadow.yOffset, builder), + ); + builder.addDescriptor( + `boxShadow.[${index}].blurRadius`, + parseLength(shadow.blur, builder), + ); + builder.addDescriptor( + `boxShadow.[${index}].spreadDistance`, + parseLength(shadow.spread, builder), + ); + builder.addDescriptor( + `boxShadow.[${index}].inset`, + shadow.inset ? true : undefined, + ); + } } export function parseDisplay( diff --git a/src/jest/index.ts b/src/jest/index.ts index 2ffae3a..de4b6ce 100644 --- a/src/jest/index.ts +++ b/src/jest/index.ts @@ -37,4 +37,6 @@ export function registerCSS( } StyleCollection.inject(compiled); + + return compiled; } diff --git a/src/runtime/native/__tests__/calc.test.tsx b/src/runtime/native/__tests__/calc.test.tsx index 09d8eb8..868aadf 100644 --- a/src/runtime/native/__tests__/calc.test.tsx +++ b/src/runtime/native/__tests__/calc.test.tsx @@ -85,8 +85,8 @@ describe("css", () => { test("calc(var(--variable) + 20px)", () => { registerCSS( `.my-class { - --variable: 100px; - width: calc(var(--variable) + 20px) + --my-var: 100px; + width: calc(var(--my-var) + 20px) }`, ); diff --git a/src/runtime/native/__tests__/units.test.tsx b/src/runtime/native/__tests__/units.test.tsx index 162eb2f..aa5d827 100644 --- a/src/runtime/native/__tests__/units.test.tsx +++ b/src/runtime/native/__tests__/units.test.tsx @@ -145,7 +145,7 @@ test("rem - dynamic", () => { expect(result.current.type).toBe(VariableContext.Provider); expect(result.current.props.value).toStrictEqual({ [VAR_SYMBOL]: true, - [emVariableName]: [{}, "rem", [10]], + [emVariableName]: [{}, "rem", 10], }); expect(result.current.props.children.type).toBe(View); @@ -161,7 +161,7 @@ test("rem - dynamic", () => { expect(result.current.type).toBe(VariableContext.Provider); expect(result.current.props.value).toStrictEqual({ [VAR_SYMBOL]: true, - [emVariableName]: [{}, "rem", [10]], + [emVariableName]: [{}, "rem", 10], }); expect(result.current.props.children.type).toBe(View); diff --git a/src/runtime/native/styles/animation.ts b/src/runtime/native/styles/animation.ts index d49e307..255a1e5 100644 --- a/src/runtime/native/styles/animation.ts +++ b/src/runtime/native/styles/animation.ts @@ -81,10 +81,14 @@ export const animation: StyleFunctionResolver = ( get, options, ) => { - const animationShortHandTuples: [unknown, string][] | undefined = - animationShorthand(resolveValue, value, get, options); + const animationShortHandTuples = animationShorthand( + resolveValue, + value, + get, + options, + ); - if (!animationShortHandTuples) { + if (!Array.isArray(animationShortHandTuples)) { return; } @@ -159,7 +163,11 @@ export const timingFunctionResolver: StyleFunctionResolver = ( return; } - const args: unknown[] = resolveValue(value[2]); + const args = resolveValue(value[2]); + + if (!Array.isArray(args)) { + return; + } const fn = resolver(); diff --git a/src/runtime/native/styles/box-shadow.ts b/src/runtime/native/styles/box-shadow.ts index e7ad971..29c5c88 100644 --- a/src/runtime/native/styles/box-shadow.ts +++ b/src/runtime/native/styles/box-shadow.ts @@ -1,4 +1,4 @@ -import { applyValue } from "../../utils"; +import { applyShorthand, isStyleDescriptorArray } from "../../utils"; import type { StyleFunctionResolver } from "./resolve"; import { shorthandHandler } from "./shorthand"; @@ -11,12 +11,12 @@ const spreadDistance = ["spreadDistance", "number"] as const; const handler = shorthandHandler( [ + [offsetX, offsetY, blurRadius, spreadDistance, color], [color, offsetX, offsetY], [color, offsetX, offsetY, blurRadius], [color, offsetX, offsetY, blurRadius, spreadDistance], [offsetX, offsetY, color], [offsetX, offsetY, blurRadius, color], - [offsetX, offsetY, blurRadius, spreadDistance, color], ], [], ); @@ -27,28 +27,29 @@ export const boxShadow: StyleFunctionResolver = ( get, options, ) => { - return func[2]?.flatMap((maybeShadow): unknown[] => { - const resolvedShadow = resolveValue(maybeShadow) as unknown; - - if (!Array.isArray(resolvedShadow)) { - return []; - } + const args = resolveValue(func[2]); - return resolvedShadow.flat().flatMap((shadow): unknown => { - const result: unknown = handler( - resolveValue, - [{}, "@boxShadowHandler", shadow], - get, - options, - ); - - if (result === undefined) { + if (!isStyleDescriptorArray(args)) { + return args; + } else { + return args.flatMap((shadows) => { + if (shadows === undefined) { return []; + } else if (isStyleDescriptorArray(shadows)) { + if (shadows.length === 0) { + return []; + } else { + return shadows + .map((shadow) => { + return applyShorthand( + handler(resolveValue, shadow, get, options), + ); + }) + .filter((v) => v !== undefined); + } + } else { + return applyShorthand(handler(resolveValue, shadows, get, options)); } - - const target = {}; - applyValue(target, "", result); - return target; }); - }); + } }; diff --git a/src/runtime/native/styles/calc.ts b/src/runtime/native/styles/calc.ts index ee4df57..530ac9a 100644 --- a/src/runtime/native/styles/calc.ts +++ b/src/runtime/native/styles/calc.ts @@ -1,4 +1,5 @@ /* eslint-disable */ +import { isStyleDescriptorArray } from "../../utils"; import type { StyleFunctionResolver } from "./resolve"; const calcPrecedence: Record = { @@ -13,71 +14,69 @@ export const calc: StyleFunctionResolver = (resolveValue, func) => { const values: number[] = []; const ops: string[] = []; - const args = func[2]; - if (!args) return; + const args = resolveValue(func[2]); + + if (!isStyleDescriptorArray(args)) return; for (let token of args) { - switch (typeof token) { - case "undefined": - // Fail on an undefined value - return; - case "number": - if (!mode) mode = "number"; - if (mode !== "number") return; - values.push(token); - continue; - 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; + if (typeof token === "number") { + if (!mode) mode = "number"; + if (mode !== "number") return; + values.push(token); + continue; + } else if (typeof token === "string") { + if (token === "(") { + ops.push(token); + } else if (token === ")") { + // Resolve all values within the brackets + while (ops.length && ops[ops.length - 1] !== "(") { + applyCalcOperator(ops.pop()!, values.pop()!, values.pop()!, values); } - } - case "string": { - if (token === "(") { - ops.push(token); - } else if (token === ")") { - // Resolve all values within the brackets - while (ops.length && ops[ops.length - 1] !== "(") { - applyCalcOperator(ops.pop()!, values.pop()!, values.pop()!, values); - } - ops.pop(); - } else if (token.endsWith("%")) { - if (!mode) mode = "percentage"; - if (mode !== "percentage") return; - values.push(Number.parseFloat(token.slice(0, -1))); - } else { - // This means we have an operator - while ( - ops.length && - ops[ops.length - 1] && - // @ts-ignore - calcPrecedence[ops[ops.length - 1]] >= calcPrecedence[token] - ) { - applyCalcOperator(ops.pop()!, values.pop()!, values.pop()!, values); - } - ops.push(token); + ops.pop(); + } else if (token.endsWith("%")) { + if (!mode) mode = "percentage"; + if (mode !== "percentage") return; + values.push(Number.parseFloat(token.slice(0, -1))); + } else { + // This means we have an operator + while ( + ops.length && + ops[ops.length - 1] && + // @ts-ignore + calcPrecedence[ops[ops.length - 1]] >= calcPrecedence[token] + ) { + applyCalcOperator(ops.pop()!, values.pop()!, values.pop()!, values); } + ops.push(token); } + } else { + // We got something unexpected + 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); } diff --git a/src/runtime/native/styles/platform-functions.ts b/src/runtime/native/styles/platform-functions.ts index 0dcdbbb..2195a9f 100644 --- a/src/runtime/native/styles/platform-functions.ts +++ b/src/runtime/native/styles/platform-functions.ts @@ -29,7 +29,7 @@ export const pixelSizeForLayoutSize: StyleFunctionResolver = ( resolveValue, value, ) => { - const size: unknown = resolveValue(value[2]?.[0]); + const size: unknown = resolveValue(value[2]); if (typeof size === "number") { return PixelRatio.getPixelSizeForLayoutSize(size); } @@ -41,7 +41,7 @@ export const roundToNearestPixel: StyleFunctionResolver = ( resolveValue, value, ) => { - const size: unknown = resolveValue(value[2]?.[0]); + const size: unknown = resolveValue(value[2]); if (typeof size === "number") { return PixelRatio.roundToNearestPixel(size); } diff --git a/src/runtime/native/styles/resolve.ts b/src/runtime/native/styles/resolve.ts index 096187e..aac223f 100644 --- a/src/runtime/native/styles/resolve.ts +++ b/src/runtime/native/styles/resolve.ts @@ -28,22 +28,30 @@ import { varResolver } from "./variables"; export type SimpleResolveValue = ( value: StyleDescriptor, castToArray?: boolean, -) => any; +) => unknown; export type StyleFunctionResolver = ( resolveValue: SimpleResolveValue, value: StyleFunction, get: Getter, options: ResolveValueOptions, -) => any; - -const shorthands: Record<`@${string}`, StyleFunctionResolver> = { - "@animation": animation, - "@textShadow": textShadow, - "@transform": transform, - "@boxShadow": boxShadow, - "@border": border, -}; +) => unknown; + +export type StyleResolver = ( + resolveValue: SimpleResolveValue, + value: StyleDescriptor, + get: Getter, + options: ResolveValueOptions, +) => unknown; + +const shorthands: Record<`@${string}`, StyleFunctionResolver | StyleResolver> = + { + "@animation": animation, + "@textShadow": textShadow, + "@transform": transform, + "@boxShadow": boxShadow, + "@border": border, + }; const functions: Record = { calc, @@ -100,10 +108,9 @@ export function resolveValue( } if (isDescriptorArray(value)) { - value = value.flatMap((d) => { - const value = resolveValue(d, get, options); - return value === undefined ? [] : value; - }) as StyleDescriptor[]; + value = value + .map((d) => resolveValue(d, get, options)) + .filter((d) => d !== undefined); if (castToArray && !Array.isArray(value)) { return [value]; @@ -127,23 +134,25 @@ export function resolveValue( throw new Error(`Unknown function: ${name}`); } - value = fn(simpleResolve, value as StyleFunction, get, options); + value = fn( + simpleResolve, + value as StyleFunction, + get, + options, + ) as StyleDescriptor; } else if (transformKeys.has(name)) { // translate, rotate, scale, etc. - return simpleResolve(value[2]?.[0], castToArray); + const args = value[2]; + return simpleResolve(args, castToArray); } else if (transformKeys.has(name.slice(1))) { // @translate, @rotate, @scale, etc. - return { [name.slice(1)]: simpleResolve(value[2], castToArray)[0] }; + return { [name.slice(1)]: simpleResolve(value[2], castToArray) }; } else { let args = simpleResolve(value[2], castToArray); if (args === undefined) { return; } else if (Array.isArray(args)) { - if (args.length === 1) { - args = args[0]; - } - let joinedArgs = args .map((arg: unknown) => { if (Array.isArray(arg)) { diff --git a/src/runtime/native/styles/shorthand.ts b/src/runtime/native/styles/shorthand.ts index 8976976..68b6410 100644 --- a/src/runtime/native/styles/shorthand.ts +++ b/src/runtime/native/styles/shorthand.ts @@ -1,7 +1,8 @@ /* eslint-disable */ import type { StyleDescriptor } from "../../../compiler"; +import { isStyleDescriptorArray } from "../../utils"; import { defaultValues } from "./defaults"; -import type { StyleFunctionResolver } from "./resolve"; +import type { StyleResolver } from "./resolve"; type ShorthandType = | "string" @@ -25,25 +26,30 @@ export const ShortHandSymbol = Symbol(); export function shorthandHandler( mappings: ShorthandRequiredValue[][], defaults: ShorthandDefaultValue[], -) { - const resolveFn: StyleFunctionResolver = ( - resolveValue, - func, - _, - { castToArray }, - ) => { - const args = func[2] || []; - - const resolved = args.flatMap((value) => { - return resolveValue(value, castToArray); - }); +): StyleResolver { + return (resolve, value, __, { castToArray }) => { + let args = isStyleDescriptorArray(value) + ? resolve(value) + : Array.isArray(value) + ? resolve(value[2]) + : value; + + if (!Array.isArray(args)) { + return; + } + + args = args.flat(); + + if (!Array.isArray(args)) { + return; + } const match = mappings.find((mapping) => { return ( - resolved.length === mapping.length && + args.length === mapping.length && mapping.every((map, index) => { const type = map[1]; - const value = resolved[index]; + const value = args[index]; if (Array.isArray(type)) { return type.includes(value) || type.includes(typeof value); @@ -77,12 +83,12 @@ export function shorthandHandler( seenDefaults.delete(map); } - let value = resolved[index]; + let value = args[index]; if (castToArray && value && !Array.isArray(value)) { value = [value]; } - return [value, map[0]]; + return [value, map[0] as StyleDescriptor]; }), ...Array.from(seenDefaults).map((map): StyleDescriptor => { let value = defaultValues[map[2]] ?? map[2]; @@ -96,6 +102,4 @@ export function shorthandHandler( { [ShortHandSymbol]: true }, ); }; - - return resolveFn; } diff --git a/src/runtime/native/styles/transform.ts b/src/runtime/native/styles/transform.ts index 691c3e2..4fc51cb 100644 --- a/src/runtime/native/styles/transform.ts +++ b/src/runtime/native/styles/transform.ts @@ -1,4 +1,3 @@ -/* eslint-disable */ import type { StyleFunctionResolver } from "./resolve"; /** @@ -9,7 +8,14 @@ export const transform: StyleFunctionResolver = ( resolveValue, transformDescriptor, ) => { - return (transformDescriptor[2] as any[]) - .map((args) => resolveValue(args)) - .filter(Boolean); + const transforms = resolveValue(transformDescriptor[2]); + + if (Array.isArray(transforms)) { + return transforms.filter((transform) => transform !== undefined) as unknown; + } else if (transforms) { + // If it's a single transform, wrap it in an array + return [transforms]; + } else { + return; + } }; diff --git a/src/runtime/native/styles/units.ts b/src/runtime/native/styles/units.ts index 6d7de49..7e806b5 100644 --- a/src/runtime/native/styles/units.ts +++ b/src/runtime/native/styles/units.ts @@ -3,18 +3,23 @@ import { rem as remObs, vh as vhObs, vw as vwObs } from "../reactivity"; import type { StyleFunctionResolver } from "./resolve"; export const em: StyleFunctionResolver = (resolve, func) => { - let value = func[2]?.[0]; + let value = func[2]; if (!value) { return; } const emValue = resolve([{}, "var", ["__rn-css-em"]]); + + if (typeof emValue !== "number") { + return undefined; + } + return round(Number(value) * emValue); }; export const vw: StyleFunctionResolver = (_, func, get) => { - const value = func[2]?.[0]; + const value = func[2]; if (typeof value !== "number") { return; @@ -24,7 +29,7 @@ export const vw: StyleFunctionResolver = (_, func, get) => { }; export const vh: StyleFunctionResolver = (_, func, get) => { - const value = func[2]?.[0]; + const value = func[2]; if (typeof value !== "number") { return; @@ -34,7 +39,7 @@ export const vh: StyleFunctionResolver = (_, func, get) => { }; export const rem: StyleFunctionResolver = (_, func, get) => { - const value = func[2]?.[0]; + const value = func[2]; if (typeof value !== "number") { return; diff --git a/src/runtime/native/styles/variables.ts b/src/runtime/native/styles/variables.ts index 31f41f3..346cfe4 100644 --- a/src/runtime/native/styles/variables.ts +++ b/src/runtime/native/styles/variables.ts @@ -1,5 +1,5 @@ -/* eslint-disable */ -import type { StyleFunction } from "../../../compiler"; +import type { StyleDescriptor, StyleFunction } from "../../../compiler"; +import { isStyleDescriptorArray } from "../../utils"; import { rootVariables, universalVariables, @@ -23,11 +23,23 @@ export function varResolver( const args = fn[2]; - if (!args) return; + let name: string | undefined; + let fallback: StyleDescriptor | undefined; - const [nameDescriptor, fallback] = args; + if (typeof args === "string") { + name = args; + } else { + const result = resolve(args); - const name = resolve(nameDescriptor); + if (isStyleDescriptorArray(result)) { + name = result[0] as string; + fallback = result[1]; + } + } + + if (typeof name !== "string") { + return; + } // If this recurses back to the same variable, we need to stop if (variableHistory.has(name)) { @@ -41,7 +53,7 @@ export function varResolver( variableHistory.add(name); - let value = resolve(inlineVariables?.[name]); + let value = resolve(inlineVariables?.[name] as StyleDescriptor); if (value !== undefined) { options.inlineVariables ??= { [VAR_SYMBOL]: "inline" }; options.inlineVariables[name] = value; diff --git a/src/runtime/utils/objects.ts b/src/runtime/utils/objects.ts index 5d32069..69e7bf7 100644 --- a/src/runtime/utils/objects.ts +++ b/src/runtime/utils/objects.ts @@ -28,6 +28,16 @@ export function getDeepPath(source: any, paths: string | string[] | false) { } } +export function applyShorthand(value: any) { + if (value === undefined) { + return; + } + + const target = {}; + applyValue(target, "", value); + return target; +} + export function applyValue( target: Record, prop: string, diff --git a/src/runtime/utils/style-value.ts b/src/runtime/utils/style-value.ts index d4409e6..2daca9e 100644 --- a/src/runtime/utils/style-value.ts +++ b/src/runtime/utils/style-value.ts @@ -1,7 +1,7 @@ import type { StyleDescriptor, StyleFunction } from "../../compiler"; export function isStyleDescriptorArray( - value: StyleDescriptor | StyleDescriptor[], + value: unknown, ): value is StyleDescriptor[] { if (Array.isArray(value)) { // If its an array and the first item is an object, the only allowed value is an array