diff --git a/.flowconfig b/.flowconfig index 11d214c52..1cad1f85d 100644 --- a/.flowconfig +++ b/.flowconfig @@ -19,6 +19,7 @@ module.use_strict=true munge_underscores=true module.name_mapper='^@stylexjs/babel-plugin$' -> '/packages/@stylexjs/babel-plugin/src/index.js' module.name_mapper='^@stylexjs/stylex$' -> '/packages/@stylexjs/stylex/src/stylex.js' +module.name_mapper='^@stylexjs/shared$' -> '/packages/@stylexjs/shared/src/index.js' module.name_mapper='^style-value-parser$' -> '/packages/style-value-parser/src/index.js' ; type-stubs module.system.node.resolve_dirname=flow_modules diff --git a/packages/@stylexjs/babel-plugin/package.json b/packages/@stylexjs/babel-plugin/package.json index 2abbe4a52..10ad45121 100644 --- a/packages/@stylexjs/babel-plugin/package.json +++ b/packages/@stylexjs/babel-plugin/package.json @@ -21,6 +21,7 @@ "@babel/traverse": "^7.26.8", "@babel/types": "^7.26.8", "@dual-bundle/import-meta-resolve": "^4.1.0", + "@stylexjs/shared": "0.16.3", "@stylexjs/stylex": "0.16.3", "postcss-value-parser": "^4.1.0" }, diff --git a/packages/@stylexjs/babel-plugin/src/shared/index.js b/packages/@stylexjs/babel-plugin/src/shared/index.js index e4b35fdc1..6e9ea8890 100644 --- a/packages/@stylexjs/babel-plugin/src/shared/index.js +++ b/packages/@stylexjs/babel-plugin/src/shared/index.js @@ -41,7 +41,7 @@ import { PSEUDO_CLASS_PRIORITIES as _PSEUDO_CLASS_PRIORITIES, AT_RULE_PRIORITIES as _AT_RULE_PRIORITIES, PSEUDO_ELEMENT_PRIORITY as _PSEUDO_ELEMENT_PRIORITY, -} from './utils/property-priorities'; +} from '@stylexjs/shared'; export * as types from './types'; export * as when from './when/when'; diff --git a/packages/@stylexjs/babel-plugin/src/shared/utils/generate-css-rule.js b/packages/@stylexjs/babel-plugin/src/shared/utils/generate-css-rule.js index d82eace49..fb30e6cc9 100644 --- a/packages/@stylexjs/babel-plugin/src/shared/utils/generate-css-rule.js +++ b/packages/@stylexjs/babel-plugin/src/shared/utils/generate-css-rule.js @@ -14,7 +14,7 @@ import { defaultOptions } from './default-options'; import generateLtr from '../physical-rtl/generate-ltr'; import generateRtl from '../physical-rtl/generate-rtl'; -import getPriority from './property-priorities'; +import { getPriority } from '@stylexjs/shared'; const THUMB_VARIANTS = [ '::-webkit-slider-thumb', diff --git a/packages/@stylexjs/eslint-plugin/__tests__/stylex-sort-keys-test.js b/packages/@stylexjs/eslint-plugin/__tests__/stylex-sort-keys-test.js index c7a02391a..dbe8ef4f9 100644 --- a/packages/@stylexjs/eslint-plugin/__tests__/stylex-sort-keys-test.js +++ b/packages/@stylexjs/eslint-plugin/__tests__/stylex-sort-keys-test.js @@ -88,8 +88,8 @@ eslintTester.run('stylex-sort-keys', rule.default, { alignItems: 'center', display: 'flex', ...obj, - alignSelf: 'center', borderColor: 'black', + alignSelf: 'center', } }); `, @@ -126,6 +126,30 @@ eslintTester.run('stylex-sort-keys', rule.default, { }); `, }, + { + code: ` + import { create as cr } from '@stylexjs/stylex'; + const styles = cr({ + button: { + marginBlock: 6, + marginInline: 8, + } + }); + `, + }, + { + code: ` + import { create as cr } from '@stylexjs/stylex'; + const styles = cr({ + button: { + margin: 16, + marginInline: 8, + marginBlockEnd: 6, + marginLeft: 4, + } + }); + `, + }, { options: [{ allowLineSeparatedGroups: true }], code: ` @@ -135,8 +159,8 @@ eslintTester.run('stylex-sort-keys', rule.default, { alignItems: 'center', display: 'flex', - alignSelf: 'center', borderColor: 'black', + alignSelf: 'center', } }); `, @@ -333,11 +357,11 @@ eslintTester.run('stylex-sort-keys', rule.default, { import * as stylex from '@stylexjs/stylex'; const styles = stylex.create({ nav: { + paddingBlock: 0, maxWidth: { default: "1080px", "@media (min-width: 2000px)": "calc((1080 / 24) * 1rem)" }, - paddingBlock: 0, }, });`, }, @@ -348,8 +372,8 @@ eslintTester.run('stylex-sort-keys', rule.default, { import * as stylex from '@stylexjs/stylex'; const styles = stylex.create({ main: { - padding: 10, animationDuration: '100ms', + padding: 10, fontSize: 12, } }); @@ -358,8 +382,8 @@ eslintTester.run('stylex-sort-keys', rule.default, { import * as stylex from '@stylexjs/stylex'; const styles = stylex.create({ main: { - animationDuration: '100ms', padding: 10, + animationDuration: '100ms', fontSize: 12, } }); @@ -367,7 +391,7 @@ eslintTester.run('stylex-sort-keys', rule.default, { errors: [ { message: - 'StyleX property key "animationDuration" should be above "padding"', + 'StyleX property key "padding" should be above "animationDuration"', }, ], }, @@ -409,8 +433,8 @@ eslintTester.run('stylex-sort-keys', rule.default, { alignItems: 'center', display: 'flex', ...obj, - borderColor: 'red', // ok alignSelf: 'center', + borderColor: 'red', // ok } }); `, @@ -422,15 +446,15 @@ eslintTester.run('stylex-sort-keys', rule.default, { alignItems: 'center', display: 'flex', ...obj, - alignSelf: 'center', borderColor: 'red', // ok + alignSelf: 'center', } }); `, errors: [ { message: - 'StyleX property key "alignSelf" should be above "borderColor"', + 'StyleX property key "borderColor" should be above "alignSelf"', }, ], }, @@ -1070,8 +1094,8 @@ eslintTester.run('stylex-sort-keys', rule.default, { // foo // bar - borderColor: 'black', alignSelf: 'center', + borderColor: 'black', } }); `, @@ -1083,16 +1107,16 @@ eslintTester.run('stylex-sort-keys', rule.default, { display: 'flex', // foo - alignSelf: 'center', - // bar borderColor: 'black', + // bar + alignSelf: 'center', } }); `, errors: [ { message: - 'StyleX property key "alignSelf" should be above "borderColor"', + 'StyleX property key "borderColor" should be above "alignSelf"', }, ], }, @@ -1142,8 +1166,8 @@ eslintTester.run('stylex-sort-keys', rule.default, { import { css } from 'a'; const styles = css.create({ main: { - padding: 10, animationDuration: '100ms', + padding: 10, fontSize: 12, } }); @@ -1152,8 +1176,8 @@ eslintTester.run('stylex-sort-keys', rule.default, { import { css } from 'a'; const styles = css.create({ main: { - animationDuration: '100ms', padding: 10, + animationDuration: '100ms', fontSize: 12, } }); @@ -1161,7 +1185,7 @@ eslintTester.run('stylex-sort-keys', rule.default, { errors: [ { message: - 'StyleX property key "animationDuration" should be above "padding"', + 'StyleX property key "padding" should be above "animationDuration"', }, ], }, @@ -1199,14 +1223,76 @@ eslintTester.run('stylex-sort-keys', rule.default, { }, ], }, + { + code: ` + import { create, when } from '@stylexjs/stylex'; + const styles = create({ + base: { + display: 'flex', + width: { + ':hover': 10, + default: 20, + }, + }, + }); + `, + output: ` + import { create, when } from '@stylexjs/stylex'; + const styles = create({ + base: { + display: 'flex', + width: { + default: 20, + ':hover': 10, + }, + }, + }); + `, + errors: [ + { + message: 'StyleX property key "default" should be above ":hover"', + }, + ], + }, + { + code: ` + import { create, when } from '@stylexjs/stylex'; + const styles = create({ + base: { + display: 'flex', + width: { + ':focus': 10, + ':hover': 20, + }, + }, + }); + `, + output: ` + import { create, when } from '@stylexjs/stylex'; + const styles = create({ + base: { + display: 'flex', + width: { + ':hover': 20, + ':focus': 10, + }, + }, + }); + `, + errors: [ + { + message: 'StyleX property key ":hover" should be above ":focus"', + }, + ], + }, { code: ` import { create, when } from '@stylexjs/stylex'; const styles = create({ base: { width: { - [when.siblingAfter(":active")]: 30, - [when.descendant(":focus")]: 20, + [when.siblingAfter(':active')]: 30, + [when.descendant(':focus')]: 20, }, display: 'flex', }, @@ -1218,17 +1304,85 @@ eslintTester.run('stylex-sort-keys', rule.default, { base: { display: 'flex', width: { - [when.siblingAfter(":active")]: 30, - [when.descendant(":focus")]: 20, + [when.siblingAfter(':active')]: 30, + [when.descendant(':focus')]: 20, }, }, }); `, errors: [ + { + message: + 'StyleX property key ":when:descendant:focus" should be above ":when:siblingAfter:active"', + }, { message: 'StyleX property key "display" should be above "width"', }, ], }, + { + code: ` + import { create, when } from '@stylexjs/stylex'; + const styles = create({ + base: { + display: 'flex', + width: { + [stylex.when.siblingAfter(':active')]: 30, + [when.descendant(':focus')]: 20, + }, + }, + }); + `, + output: ` + import { create, when } from '@stylexjs/stylex'; + const styles = create({ + base: { + display: 'flex', + width: { + [when.descendant(':focus')]: 20, + [stylex.when.siblingAfter(':active')]: 30, + }, + }, + }); + `, + errors: [ + { + message: + 'StyleX property key ":when:descendant:focus" should be above ":when:siblingAfter:active"', + }, + ], + }, + { + code: ` + import { create, when } from '@stylexjs/stylex'; + const styles = create({ + base: { + display: 'flex', + width: { + [when[api](\`:focus\`)]: 20, + [when[api](\`:active\`)]: 30, + }, + }, + }); + `, + output: ` + import { create, when } from '@stylexjs/stylex'; + const styles = create({ + base: { + display: 'flex', + width: { + [when[api](\`:active\`)]: 30, + [when[api](\`:focus\`)]: 20, + }, + }, + }); + `, + errors: [ + { + message: + 'StyleX property key ":when:api:active" should be above ":when:api:focus"', + }, + ], + }, ], }); diff --git a/packages/@stylexjs/eslint-plugin/package.json b/packages/@stylexjs/eslint-plugin/package.json index a62e4c560..a07a70f50 100644 --- a/packages/@stylexjs/eslint-plugin/package.json +++ b/packages/@stylexjs/eslint-plugin/package.json @@ -16,6 +16,7 @@ "test": "jest --detectOpenHandles --coverage" }, "dependencies": { + "@stylexjs/shared": "0.16.3", "css-shorthand-expand": "^1.2.0", "micromatch": "^4.0.5", "postcss-value-parser": "^4.2.0" diff --git a/packages/@stylexjs/eslint-plugin/src/stylex-sort-keys.js b/packages/@stylexjs/eslint-plugin/src/stylex-sort-keys.js index 471f97285..457f4745b 100644 --- a/packages/@stylexjs/eslint-plugin/src/stylex-sort-keys.js +++ b/packages/@stylexjs/eslint-plugin/src/stylex-sort-keys.js @@ -55,6 +55,7 @@ function isValidOrder( const curr = getPropertyPriorityAndType(currName, order); if (prev.type !== 'string' || curr.type !== 'string') { + if (prev.priority === curr.priority) return prevName <= currName; return prev.priority <= curr.priority; } diff --git a/packages/@stylexjs/eslint-plugin/src/utils/getPropertyName.js b/packages/@stylexjs/eslint-plugin/src/utils/getPropertyName.js index 19cda1622..fa513b3c8 100644 --- a/packages/@stylexjs/eslint-plugin/src/utils/getPropertyName.js +++ b/packages/@stylexjs/eslint-plugin/src/utils/getPropertyName.js @@ -77,12 +77,44 @@ function getStaticPropertyName(node: Node | ChainExpression): string | null { return prop.name; } + if (prop.type === 'CallExpression') { + const callee = getCalleeName(prop.callee); + if (!callee) return null; + + if (callee.startsWith('stylex.when') || callee.startsWith('when')) { + const relation = callee.split('.').pop(); + const arg = prop.arguments[0]; + if (!arg) return null; + + return `:when:${relation ?? ''}${getStaticStringValue(arg) ?? ''}`; + } + } + return getStaticStringValue(prop); } return null; } +function getCalleeName(node: Node): string | null { + const parts: string[] = []; + let current = node; + + while (current && current.type === 'MemberExpression') { + if (current.property.type === 'Identifier') { + parts.unshift(current.property.name); + } + + current = current.object; + } + + if (current && current.type === 'Identifier') { + parts.unshift(current.name); + } + + return parts.length > 0 ? parts.join('.') : null; +} + export default function getPropertyName( node: $ReadOnly<{ ...Property, ... }>, ): string | null { diff --git a/packages/@stylexjs/eslint-plugin/src/utils/getPropertyPriorityAndType.js b/packages/@stylexjs/eslint-plugin/src/utils/getPropertyPriorityAndType.js index 9f2d3c98b..c08b9ed14 100644 --- a/packages/@stylexjs/eslint-plugin/src/utils/getPropertyPriorityAndType.js +++ b/packages/@stylexjs/eslint-plugin/src/utils/getPropertyPriorityAndType.js @@ -9,6 +9,13 @@ 'use strict'; +import { + getAtRulePriority, + getPseudoElementPriority, + getPseudoClassPriority, + getDefaultPriority, +} from '@stylexjs/shared'; + import CLEAN_ORDER_PRIORITIES from '../reference/cleanOrderPriorities'; import RECESS_ORDER_PRIORITIES from '../reference/recessOrderPriorities'; @@ -32,41 +39,94 @@ export default function getPropertyPriorityAndType( key: string, order: 'default' | 'clean' | 'recess', ): PriorityAndType { - const BASE_PRIORITY = ORDER_PRIORITIES[order] + const orderPriority = ORDER_PRIORITIES[order] ? ORDER_PRIORITIES[order].length - 1 : 0; - const AT_CONTAINER_PRIORITY = BASE_PRIORITY + 300; - const AT_MEDIA_PRIORITY = BASE_PRIORITY + 200; - const AT_SUPPORT_PRIORITY = BASE_PRIORITY + 30; - const PSEUDO_CLASS_PRIORITY = BASE_PRIORITY + 40; - const PSEUDO_ELEMENT_PRIORITY = BASE_PRIORITY + 5000; - - if (key.startsWith('@supports')) { - return { priority: AT_SUPPORT_PRIORITY, type: 'atRule' }; - } - - if (key.startsWith('::')) { - return { priority: PSEUDO_ELEMENT_PRIORITY, type: 'pseudoElement' }; + const atRulePriority = getAtRulePriority(key); + if (atRulePriority) { + return { + priority: orderPriority + atRulePriority, + type: 'atRule', + }; } - if (key.startsWith(':')) { - // TODO: Consider restoring pseudo-specific priorities + const pseudoElementPriority = getPseudoElementPriority(key); + if (pseudoElementPriority) { return { - priority: PSEUDO_CLASS_PRIORITY, - type: 'pseudoClass', + priority: orderPriority + pseudoElementPriority, + type: 'pseudoElement', }; } - if (key.startsWith('@media')) { - return { priority: AT_MEDIA_PRIORITY, type: 'atRule' }; + if (key.startsWith(':when:ancestor')) { + const ancestorPriority = getPseudoClassPriority( + key.replace(':when:ancestor', ''), + ); + if (ancestorPriority) { + return { + priority: orderPriority + ancestorPriority / 100 + 10, + type: 'pseudoClass', + }; + } + } else if (key.startsWith(':when:descendant')) { + const descendantPriority = getPseudoClassPriority( + key.replace(':when:descendant', ''), + ); + if (descendantPriority) { + return { + priority: orderPriority + descendantPriority / 100 + 15, + type: 'pseudoClass', + }; + } + } else if (key.startsWith(':when:anySibling')) { + const anySiblingPriority = getPseudoClassPriority( + key.replace(':when:anySibling', ''), + ); + if (anySiblingPriority) { + return { + priority: orderPriority + anySiblingPriority / 100 + 20, + type: 'pseudoClass', + }; + } + } else if (key.startsWith(':when:siblingBefore')) { + const siblingBeforePriority = getPseudoClassPriority( + key.replace(':when:siblingBefore', ''), + ); + if (siblingBeforePriority) { + return { + priority: orderPriority + siblingBeforePriority / 100 + 30, + type: 'pseudoClass', + }; + } + } else if (key.startsWith(':when:siblingAfter')) { + const siblingAfterPriority = getPseudoClassPriority( + key.replace(':when:siblingAfter', ''), + ); + if (siblingAfterPriority) { + return { + priority: orderPriority + siblingAfterPriority / 100 + 40, + type: 'pseudoClass', + }; + } } - if (key.startsWith('@container')) { - return { priority: AT_CONTAINER_PRIORITY, type: 'atRule' }; + const pseudoClassPriority = getPseudoClassPriority(key); + if (pseudoClassPriority) { + return { + priority: orderPriority + pseudoClassPriority, + type: 'pseudoClass', + }; } - if (ORDER_PRIORITIES[order]) { + if (order === 'default') { + const defaultPriority = getDefaultPriority( + key.replace(/[A-Z]/g, '-$&').toLowerCase(), + ); + if (defaultPriority) { + return { priority: defaultPriority, type: 'knownCssProperty' }; + } + } else if (ORDER_PRIORITIES[order]) { const index = ORDER_PRIORITIES[order].indexOf(key); if (index !== -1) { return { priority: index, type: 'knownCssProperty' }; diff --git a/packages/@stylexjs/shared/.babelrc b/packages/@stylexjs/shared/.babelrc new file mode 100644 index 000000000..f80af2244 --- /dev/null +++ b/packages/@stylexjs/shared/.babelrc @@ -0,0 +1,16 @@ +{ + "assumptions": { + "iterableIsArray": true + }, + "presets": [ + ["@babel/preset-env", { + "exclude": [ + "@babel/plugin-transform-typeof-symbol" + ], + "targets": "defaults" + }], + "@babel/preset-flow", + "@babel/preset-react" + ], + "plugins": [["babel-plugin-syntax-hermes-parser", {"flow": "detect"}]] +} diff --git a/packages/@stylexjs/shared/package.json b/packages/@stylexjs/shared/package.json new file mode 100644 index 000000000..c6fd53895 --- /dev/null +++ b/packages/@stylexjs/shared/package.json @@ -0,0 +1,22 @@ +{ + "name": "@stylexjs/shared", + "version": "0.16.3", + "main": "lib/index.js", + "repository": { + "type": "git", + "url": "git+https://github.com/facebook/stylex.git" + }, + "license": "MIT", + "scripts": { + "prebuild": "gen-types -i src/ -o lib/", + "build": "babel src/ --out-dir lib/", + "test": "jest --coverage --passWithNoTests" + }, + "devDependencies": { + "@babel/cli": "^7.23.9", + "@babel/core": "^7.23.9" + }, + "files": [ + "lib/*" + ] +} diff --git a/packages/@stylexjs/shared/src/index.js b/packages/@stylexjs/shared/src/index.js new file mode 100644 index 000000000..88313e84b --- /dev/null +++ b/packages/@stylexjs/shared/src/index.js @@ -0,0 +1,35 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict + */ + +import { + default as _getPriority, + getAtRulePriority as _getAtRulePriority, + getPseudoElementPriority as _getPseudoElementPriority, + getPseudoClassPriority as _getPseudoClassPriority, + getDefaultPriority as _getDefaultPriority, + PSEUDO_CLASS_PRIORITIES as _PSEUDO_CLASS_PRIORITIES, + AT_RULE_PRIORITIES as _AT_RULE_PRIORITIES, + PSEUDO_ELEMENT_PRIORITY as _PSEUDO_ELEMENT_PRIORITY, +} from './utils/property-priorities'; + +export const getAtRulePriority: typeof _getAtRulePriority = _getAtRulePriority; +export const getPseudoElementPriority: typeof _getPseudoElementPriority = + _getPseudoElementPriority; +export const getPseudoClassPriority: typeof _getPseudoClassPriority = + _getPseudoClassPriority; +export const getDefaultPriority: typeof _getDefaultPriority = + _getDefaultPriority; +export const getPriority: typeof _getPriority = _getPriority; + +export const PSEUDO_CLASS_PRIORITIES: typeof _PSEUDO_CLASS_PRIORITIES = + _PSEUDO_CLASS_PRIORITIES; +export const AT_RULE_PRIORITIES: typeof _AT_RULE_PRIORITIES = + _AT_RULE_PRIORITIES; +export const PSEUDO_ELEMENT_PRIORITY: typeof _PSEUDO_ELEMENT_PRIORITY = + _PSEUDO_ELEMENT_PRIORITY; diff --git a/packages/@stylexjs/babel-plugin/src/shared/utils/property-priorities.js b/packages/@stylexjs/shared/src/utils/property-priorities.js similarity index 97% rename from packages/@stylexjs/babel-plugin/src/shared/utils/property-priorities.js rename to packages/@stylexjs/shared/src/utils/property-priorities.js index 50a4ed4fa..f55058b85 100644 --- a/packages/@stylexjs/babel-plugin/src/shared/utils/property-priorities.js +++ b/packages/@stylexjs/shared/src/utils/property-priorities.js @@ -724,7 +724,7 @@ const RELATIONAL_SELECTORS = { /^:where\(\.[0-9a-zA-Z_-]+(:[a-zA-Z-]+)\s+~\s+\*,\s+:has\(~\s\.[0-9a-zA-Z_-]+(:[a-zA-Z-]+)\)\)$/, }; -export default function getPriority(key: string): number { +export function getAtRulePriority(key: string): number | void { if (key.startsWith('--')) { return 1; } @@ -740,11 +740,15 @@ export default function getPriority(key: string): number { if (key.startsWith('@container')) { return AT_RULE_PRIORITIES['@container']; } +} +export function getPseudoElementPriority(key: string): number | void { if (key.startsWith('::')) { return PSEUDO_ELEMENT_PRIORITY; } +} +export function getPseudoClassPriority(key: string): number | void { const pseudoBase = (p: string): number => (PSEUDO_CLASS_PRIORITIES[p] ?? 40) / 100; @@ -783,7 +787,9 @@ export default function getPriority(key: string): number { return PSEUDO_CLASS_PRIORITIES[prop] ?? 40; } +} +export function getDefaultPriority(key: string): number | void { if (shorthandsOfShorthands.has(key)) { return 1000; } @@ -796,5 +802,20 @@ export default function getPriority(key: string): number { if (longHandPhysical.has(key)) { return 4000; } +} + +export default function getPriority(key: string): number { + const atRulePriority = getAtRulePriority(key); + if (atRulePriority) return atRulePriority; + + const pseudoElementPriority = getPseudoElementPriority(key); + if (pseudoElementPriority) return pseudoElementPriority; + + const pseudoClassPriority = getPseudoClassPriority(key); + if (pseudoClassPriority) return pseudoClassPriority; + + const defaultPriority = getDefaultPriority(key); + if (defaultPriority) return defaultPriority; + return 3000; }