From a7c48c74e76be92dc8b817a0eb53fa9086801c68 Mon Sep 17 00:00:00 2001 From: Mathieu Acthernoene Date: Thu, 2 Jan 2025 19:05:40 +0100 Subject: [PATCH 01/22] Initial implementation commit --- package.json | 5 +- pnpm-lock.yaml | 24 ++ src/hyphenateName.ts | 12 + src/index.ts | 32 +- src/normalizeValue.ts | 100 +++++ src/preprocess.ts | 167 ++++++++ src/sheet.ts | 389 ++++++++++++++++++ src/types.ts | 133 ++++++ src/types/react-native__normalize-colors.d.ts | 4 + src/utils.ts | 16 + tsconfig.json | 4 +- 11 files changed, 881 insertions(+), 5 deletions(-) create mode 100644 src/hyphenateName.ts create mode 100644 src/normalizeValue.ts create mode 100644 src/preprocess.ts create mode 100644 src/sheet.ts create mode 100644 src/types.ts create mode 100644 src/types/react-native__normalize-colors.d.ts create mode 100644 src/utils.ts diff --git a/package.json b/package.json index 3925fdc..8832712 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,10 @@ ] }, "dependencies": { - "@emotion/hash": "^0.9.2" + "@emotion/hash": "^0.9.2", + "@react-native/normalize-colors": "^0.76.5", + "csstype": "^3.1.3", + "postcss-value-parser": "^4.2.0" }, "devDependencies": { "@vitest/browser": "^2.1.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index da335d1..7ef02e1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,15 @@ importers: '@emotion/hash': specifier: ^0.9.2 version: 0.9.2 + '@react-native/normalize-colors': + specifier: ^0.76.5 + version: 0.76.5 + csstype: + specifier: ^3.1.3 + version: 3.1.3 + postcss-value-parser: + specifier: ^4.2.0 + version: 4.2.0 devDependencies: '@vitest/browser': specifier: ^2.1.8 @@ -404,6 +413,9 @@ packages: '@polka/url@1.0.0-next.28': resolution: {integrity: sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==} + '@react-native/normalize-colors@0.76.5': + resolution: {integrity: sha512-6QRLEok1r55gLqj+94mEWUENuU5A6wsr2OoXpyq/CgQ7THWowbHtru/kRGRr6o3AQXrVnZheR60JNgFcpNYIug==} + '@rollup/rollup-android-arm-eabi@4.28.1': resolution: {integrity: sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ==} cpu: [arm] @@ -668,6 +680,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + debug@4.4.0: resolution: {integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==} engines: {node: '>=6.0'} @@ -913,6 +928,9 @@ packages: yaml: optional: true + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + postcss@8.4.49: resolution: {integrity: sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==} engines: {node: ^10 || ^12 || >=14} @@ -1488,6 +1506,8 @@ snapshots: '@polka/url@1.0.0-next.28': {} + '@react-native/normalize-colors@0.76.5': {} + '@rollup/rollup-android-arm-eabi@4.28.1': optional: true @@ -1718,6 +1738,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + csstype@3.1.3: {} + debug@4.4.0: dependencies: ms: 2.1.3 @@ -1947,6 +1969,8 @@ snapshots: optionalDependencies: postcss: 8.4.49 + postcss-value-parser@4.2.0: {} + postcss@8.4.49: dependencies: nanoid: 3.3.8 diff --git a/src/hyphenateName.ts b/src/hyphenateName.ts new file mode 100644 index 0000000..f1295a2 --- /dev/null +++ b/src/hyphenateName.ts @@ -0,0 +1,12 @@ +// https://github.com/facebook/react/blob/v19.0.0/packages/react-dom-bindings/src/shared/hyphenateStyleName.js + +const uppercasePattern = /([A-Z])/g; +const hyphenateNameCache: Record = {}; + +export const hyphenateName = (name: string): string => { + hyphenateNameCache[name] ??= name + .replace(uppercasePattern, "-$1") + .toLowerCase(); + + return hyphenateNameCache[name]; +}; diff --git a/src/index.ts b/src/index.ts index 1fd4236..abb0d21 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,31 @@ -export const helloWorld = () => { - console.log("Hello world"); +import { + preprocessAtomicStyle, + preprocessKeyframes, + preprocessResetStyle, +} from "./preprocess"; +import { createSheet } from "./sheet"; +import { Keyframes, Nestable, Style } from "./types"; +import { forEach } from "./utils"; + +const sheet = createSheet(); + +const keyframes = (keyframes: Keyframes): string | undefined => + sheet.insertKeyframes(preprocessKeyframes(keyframes)); + +const make = ( + styles: Record>, +): Record => { + const output = {} as Record; + + forEach(styles, (key, value) => { + output[key] = + key[0] === "$" + ? sheet.insertResetRule(preprocessResetStyle(value)) + : sheet.insertAtomicRules(preprocessAtomicStyle(value)); + }); + + return output; }; + +export const css = { keyframes, make }; +export const cx = sheet.cx; diff --git a/src/normalizeValue.ts b/src/normalizeValue.ts new file mode 100644 index 0000000..0ba0467 --- /dev/null +++ b/src/normalizeValue.ts @@ -0,0 +1,100 @@ +import normalizeColor from "@react-native/normalize-colors"; +import { Property } from "./types"; + +const normalizeValueCache: Record = {}; + +/** + * CSS properties which accept numbers but are not in units of "px" + * From https://github.com/facebook/react/blob/v19.0.0/packages/react-dom-bindings/src/shared/isUnitlessNumber.js + */ +const unitlessProperties = new Set([ + "animationIterationCount", + "aspectRatio", + "borderImageOutset", + "borderImageSlice", + "borderImageWidth", + "columnCount", + "flex", + "flexGrow", + "flexShrink", + "fontWeight", + "gridColumnEnd", + "gridColumnStart", + "gridRowEnd", + "gridRowStart", + "lineClamp", + "lineHeight", + "opacity", + "order", + "orphans", + "scale", + "tabSize", + "widows", + "zIndex", + "zoom", + + // SVG-related properties + "fillOpacity", + "floodOpacity", + "stopOpacity", + "strokeDasharray", + "strokeDashoffset", + "strokeMiterlimit", + "strokeOpacity", + "strokeWidth", +] satisfies Property[]); + +/** + * From https://github.com/necolas/react-native-web/blob/0.19.13/packages/react-native-web/src/exports/StyleSheet/compiler/normalizeValueWithProperty.js#L13 + */ +const colorProperties = new Set([ + "backgroundColor", + "borderBottomColor", + "borderColor", + "borderLeftColor", + "borderRightColor", + "borderTopColor", + "color", + "textDecorationColor", +] satisfies Property[]); + +const isWebColor = (color: string): boolean => + color === "currentcolor" || + color === "currentColor" || + color === "inherit" || + color.indexOf("var(") === 0; + +export const normalizeValue = ( + value: string | number | undefined, + property: string, +): string | undefined => { + if (typeof value === "number") { + return unitlessProperties.has(property) ? String(value) : `${value}px`; + } + + if (colorProperties.has(property)) { + if (value == null || isWebColor(value)) { + return value; + } + + if (normalizeValueCache[value] != null) { + return normalizeValueCache[value]; + } + + const normalizedColor = normalizeColor(value); + + if (normalizedColor != null) { + const int = ((normalizedColor << 24) | (normalizedColor >>> 8)) >>> 0; + + const r = (int >> 16) & 255; + const g = (int >> 8) & 255; + const b = int & 255; + const a = ((int >> 24) & 255) / 255; + + normalizeValueCache[value] = `rgba(${r}, ${g}, ${b}, ${a.toFixed(2)})`; + return normalizeValueCache[value]; + } + } + + return value; +}; diff --git a/src/preprocess.ts b/src/preprocess.ts new file mode 100644 index 0000000..3f05f75 --- /dev/null +++ b/src/preprocess.ts @@ -0,0 +1,167 @@ +import valueParser from "postcss-value-parser"; +import { + Keyframes, + LonghandProperty, + Nestable, + Property, + ShorthandProperty, + Style, + ValueOf, +} from "./types"; +import { forEach } from "./utils"; + +const shorthands: Partial> = { + backgroundPosition: ["backgroundPositionX", "backgroundPositionY"], + borderColor: [ + "borderTopColor", + "borderRightColor", + "borderBottomColor", + "borderLeftColor", + ], + borderRadius: [ + "borderTopLeftRadius", + "borderTopRightRadius", + "borderBottomRightRadius", + "borderBottomLeftRadius", + ], + borderStyle: [ + "borderTopStyle", + "borderRightStyle", + "borderBottomStyle", + "borderLeftStyle", + ], + borderWidth: [ + "borderTopWidth", + "borderRightWidth", + "borderBottomWidth", + "borderLeftWidth", + ], + gap: ["rowGap", "columnGap"], + inset: ["top", "right", "bottom", "left"], + insetBlock: ["insetBlockStart", "insetBlockEnd"], + insetInline: ["insetInlineStart", "insetInlineEnd"], + margin: ["marginTop", "marginRight", "marginBottom", "marginLeft"], + marginBlock: ["marginBlockStart", "marginBlockEnd"], + marginHorizontal: ["marginRight", "marginLeft"], + marginInline: ["marginInlineStart", "marginInlineEnd"], + marginVertical: ["marginTop", "marginBottom"], + overflow: ["overflowX", "overflowY"], + overscrollBehavior: ["overscrollBehaviorX", "overscrollBehaviorY"], + padding: ["paddingTop", "paddingRight", "paddingBottom", "paddingLeft"], + paddingBlock: ["paddingBlockStart", "paddingBlockEnd"], + paddingHorizontal: ["paddingRight", "paddingLeft"], + paddingInline: ["paddingInlineStart", "paddingInlineEnd"], + paddingVertical: ["paddingTop", "paddingBottom"], + scrollMarginBlock: ["scrollMarginBlockStart", "scrollMarginBlockEnd"], + scrollMarginInline: ["scrollMarginInlineStart", "scrollMarginInlineEnd"], + scrollPaddingBlock: ["scrollPaddingBlockStart", "scrollPaddingBlockEnd"], + scrollPaddingInline: ["scrollPaddingInlineStart", "scrollPaddingInlineEnd"], +} satisfies Record< + Exclude, // flex is handled differently + LonghandProperty[] +>; + +const preprocessRule = ( + acc: Style, + key: Property, + value: ValueOf