From 63e0616e95501c42ad5519660c82f0efb85099b0 Mon Sep 17 00:00:00 2001 From: Melissa Liu Date: Sun, 21 Dec 2025 16:44:29 -0500 Subject: [PATCH 1/8] [inline-css] create new inline styles package --- .../__tests__/transform-stylex-props-test.js | 81 ++++++++++++ .../babel-plugin/src/utils/state-manager.js | 4 + .../babel-plugin/src/visitors/imports.js | 51 ++++++++ .../babel-plugin/src/visitors/stylex-props.js | 122 ++++++++++++++++++ packages/@stylexjs/inline-css/README.md | 29 +++++ packages/@stylexjs/inline-css/package.json | 23 ++++ packages/@stylexjs/inline-css/src/index.d.ts | 16 +++ packages/@stylexjs/inline-css/src/index.js | 35 +++++ 8 files changed, 361 insertions(+) create mode 100644 packages/@stylexjs/inline-css/README.md create mode 100644 packages/@stylexjs/inline-css/package.json create mode 100644 packages/@stylexjs/inline-css/src/index.d.ts create mode 100644 packages/@stylexjs/inline-css/src/index.js diff --git a/packages/@stylexjs/babel-plugin/__tests__/transform-stylex-props-test.js b/packages/@stylexjs/babel-plugin/__tests__/transform-stylex-props-test.js index cffa3edd3..4d11dad7f 100644 --- a/packages/@stylexjs/babel-plugin/__tests__/transform-stylex-props-test.js +++ b/packages/@stylexjs/babel-plugin/__tests__/transform-stylex-props-test.js @@ -55,6 +55,87 @@ describe('@stylexjs/babel-plugin', () => { `); }); + test('inline-css uses same classnames as stylex.create', () => { + const inline = transform(` + import stylex from 'stylex'; + import * as css from '@stylexjs/inline-css'; + stylex.props(css.display.flex); + `); + expect(inline).toMatchInlineSnapshot(` + "import _inject from "@stylexjs/stylex/lib/stylex-inject"; + var _inject2 = _inject; + import stylex from 'stylex'; + import * as css from '@stylexjs/inline-css'; + _inject2({ + ltr: ".x78zum5{display:flex}", + priority: 3000 + }); + ({ + className: "x78zum5" + });" + `); + const local = transform(` + import stylex from 'stylex'; + const styles = stylex.create({ + flex: { display: 'flex' }, + }); + stylex.props(styles.flex); + `); + expect(local).toMatchInlineSnapshot(` + "import _inject from "@stylexjs/stylex/lib/stylex-inject"; + var _inject2 = _inject; + import stylex from 'stylex'; + _inject2({ + ltr: ".x78zum5{display:flex}", + priority: 3000 + }); + ({ + className: "x78zum5" + });" + `); + expect(inline).toBe(local); + }); + + test('inline-css supports leading underscore value', () => { + const inline = transform(` + import stylex from 'stylex'; + import * as css from '@stylexjs/inline-css'; + stylex.props(css.padding._16px); + `); + expect(inline).toMatchInlineSnapshot(` + "import _inject from \\"@stylexjs/stylex/lib/stylex-inject\\"; + var _inject2 = _inject; + import stylex from 'stylex'; + _inject2({ + ltr: \\".x1tamke2{padding:16px}\\", + priority: 1000 + }); + ({ + className: \\"x1tamke2\\" + });" + `); + const local = transform(` + import stylex from 'stylex'; + const styles = stylex.create({ + pad: { padding: '16px' }, + }); + stylex.props(styles.pad); + `); + expect(local).toMatchInlineSnapshot(` + "import _inject from \\"@stylexjs/stylex/lib/stylex-inject\\"; + var _inject2 = _inject; + import stylex from 'stylex'; + _inject2({ + ltr: \\".x1tamke2{padding:16px}\\", + priority: 1000 + }); + ({ + className: \\"x1tamke2\\" + });" + `); + expect(inline).toBe(local); + }); + test('basic stylex call', () => { expect( transform(` diff --git a/packages/@stylexjs/babel-plugin/src/utils/state-manager.js b/packages/@stylexjs/babel-plugin/src/utils/state-manager.js index f9d7ccbcb..4246a9962 100644 --- a/packages/@stylexjs/babel-plugin/src/utils/state-manager.js +++ b/packages/@stylexjs/babel-plugin/src/utils/state-manager.js @@ -165,12 +165,16 @@ export default class StateManager { +stylexViewTransitionClassImport: Set = new Set(); +stylexDefaultMarkerImport: Set = new Set(); +stylexWhenImport: Set = new Set(); + +inlineCSSNamespaceImports: Set = new Set(); + +inlineCSSNamedImports: Map = new Map(); injectImportInserted: ?t.Identifier = null; // `stylex.create` calls +styleMap: Map = new Map(); +styleVars: Map> = new Map(); + +inlineStylesCache: Map = + new Map(); // results of `stylex.create` calls that should be kept +styleVarsToKeep: Set<[string, true | string, true | Array]> = diff --git a/packages/@stylexjs/babel-plugin/src/visitors/imports.js b/packages/@stylexjs/babel-plugin/src/visitors/imports.js index 784e98f24..9a5a20271 100644 --- a/packages/@stylexjs/babel-plugin/src/visitors/imports.js +++ b/packages/@stylexjs/babel-plugin/src/visitors/imports.js @@ -12,6 +12,8 @@ import type { NodePath } from '@babel/traverse'; import * as t from '@babel/types'; import StateManager from '../utils/state-manager'; +const INLINE_CSS_SOURCES = new Set(['@stylexjs/inline-css']); + // Read imports of react and remember the name of the local variables for later export function readImportDeclarations( path: NodePath, @@ -22,6 +24,28 @@ export function readImportDeclarations( return; } const sourcePath = node.source.value; + + if (INLINE_CSS_SOURCES.has(sourcePath)) { + for (const specifier of node.specifiers) { + if (specifier.type === 'ImportNamespaceSpecifier') { + state.inlineCSSNamespaceImports.add(specifier.local.name); + } else if (specifier.type === 'ImportDefaultSpecifier') { + state.inlineCSSNamespaceImports.add(specifier.local.name); + } else if ( + specifier.type === 'ImportSpecifier' && + (specifier.imported.type === 'Identifier' || + specifier.imported.type === 'StringLiteral') + ) { + const importedName = + specifier.imported.type === 'Identifier' + ? specifier.imported.name + : specifier.imported.value; + state.inlineCSSNamedImports.set(specifier.local.name, importedName); + } + } + return; + } + if (state.importSources.includes(sourcePath)) { for (const specifier of node.specifiers) { if ( @@ -183,4 +207,31 @@ export function readRequires( } } } + + if ( + init != null && + init.type === 'CallExpression' && + init.callee?.type === 'Identifier' && + init.callee?.name === 'require' && + init.arguments?.length === 1 && + init.arguments?.[0].type === 'StringLiteral' && + INLINE_CSS_SOURCES.has(init.arguments[0].value) + ) { + if (node.id.type === 'Identifier') { + state.inlineCSSNamespaceImports.add(node.id.name); + } + if (node.id.type === 'ObjectPattern') { + for (const prop of node.id.properties) { + if ( + prop.type === 'ObjectProperty' && + prop.key.type === 'Identifier' && + prop.value.type === 'Identifier' + ) { + const importedName = prop.key.name; + const localName = prop.value.name; + state.inlineCSSNamedImports.set(localName, importedName); + } + } + } + } } diff --git a/packages/@stylexjs/babel-plugin/src/visitors/stylex-props.js b/packages/@stylexjs/babel-plugin/src/visitors/stylex-props.js index d32378994..0c891b9aa 100644 --- a/packages/@stylexjs/babel-plugin/src/visitors/stylex-props.js +++ b/packages/@stylexjs/babel-plugin/src/visitors/stylex-props.js @@ -16,6 +16,11 @@ import { props } from '@stylexjs/stylex'; import { convertObjectToAST } from '../utils/js-to-ast'; import { evaluate } from '../utils/evaluate-path'; import stylexDefaultMarker from '../shared/stylex-defaultMarker'; +import styleXCreateSet from '../shared/stylex-create'; +import { + injectDevClassNames, + convertToTestStyles, +} from '../utils/dev-classname'; type ClassNameValue = string | null | boolean | NonStringClassNameValue; type NonStringClassNameValue = [t.Expression, ClassNameValue, ClassNameValue]; @@ -42,6 +47,11 @@ class ConditionalStyle { type ResolvedArg = ?StyleObject | ConditionalStyle; type ResolvedArgs = Array; +type InlineStyle = $ReadOnly<{ + property: string, + value: string | number, +}>; + export function skipStylexPropsChildren( path: NodePath, state: StateManager, @@ -331,6 +341,11 @@ function parseNullableStyle( return null; } + const inlineStyle = getInlineStyle(path, state); + if (inlineStyle != null) { + return compileInlineStyle(inlineStyle, state, path); + } + if (t.isMemberExpression(node)) { const { object, property, computed: computed } = node; let objName = null; @@ -380,6 +395,113 @@ function parseNullableStyle( return 'other'; } +function getInlineStyle( + path: NodePath, + state: StateManager, +): null | InlineStyle { + const node = path.node; + if (!t.isMemberExpression(node)) { + return null; + } + + const valueKey = getPropKey(node.property, node.computed); + if (valueKey == null) { + return null; + } + + const parent = node.object; + + if (t.isIdentifier(parent) && state.inlineCSSNamedImports.has(parent.name)) { + const propName = state.inlineCSSNamedImports.get(parent.name); + if (propName != null) { + return { property: propName, value: normalizeInlineValue(valueKey) }; + } + } + + if (t.isMemberExpression(parent)) { + const propName = getPropKey(parent.property, parent.computed); + const base = parent.object; + if ( + propName != null && + t.isIdentifier(base) && + state.inlineCSSNamespaceImports.has(base.name) + ) { + return { property: propName, value: normalizeInlineValue(valueKey) }; + } + } + + return null; +} + +function normalizeInlineValue(value: string | number): string | number { + if (typeof value === 'string' && value.startsWith('_')) { + return value.slice(1); + } + return value; +} + +function getPropKey( + prop: t.Expression | t.PrivateName | t.Identifier, + computed: boolean, +): null | string | number { + if (!computed && t.isIdentifier(prop)) { + return prop.name; + } + if (computed && t.isStringLiteral(prop)) { + return prop.value; + } + if (computed && t.isNumericLiteral(prop)) { + return prop.value; + } + return null; +} + +function compileInlineStyle( + inlineStyle: InlineStyle, + state: StateManager, + path: NodePath, +): StyleObject { + const { property, value } = inlineStyle; + const cacheKey = `${property}|${typeof value}:${String(value)}`; + const cached = state.inlineStylesCache.get(cacheKey); + if (cached != null) { + return cached; + } + + const [compiledNamespaces, injectedStyles] = styleXCreateSet( + { + __inline__: { + [property]: value, + }, + }, + state.options, + ); + + let compiled = compiledNamespaces.__inline__; + if (state.isDev && state.options.enableDevClassNames) { + compiled = injectDevClassNames( + { __inline__: compiled }, + null, + state, + ).__inline__; + } + if (state.isTest) { + compiled = convertToTestStyles( + { __inline__: compiled }, + null, + state, + ).__inline__; + } + + const listOfStyles = Object.entries(injectedStyles).map( + ([key, { priority, ...rest }]) => [key, rest, priority], + ); + state.registerStyles(listOfStyles, path); + + state.inlineStylesCache.set(cacheKey, compiled); + return compiled; +} + function makeStringExpression(values: ResolvedArgs): t.Expression { const conditions = values .filter( diff --git a/packages/@stylexjs/inline-css/README.md b/packages/@stylexjs/inline-css/README.md new file mode 100644 index 000000000..5de582428 --- /dev/null +++ b/packages/@stylexjs/inline-css/README.md @@ -0,0 +1,29 @@ +# @stylexjs/inline-css + +Compile-time helpers that let you author StyleX styles with raw CSS properties: + +```js +import * as stylex from '@stylexjs/stylex'; +import * as css from '@stylexjs/inline-css'; + +function Example({ color }) { + return ( +
+ ); +} +``` + +The bindings exported here are compile-time only. The StyleX Babel plugin +detects property/value member expressions like `css.display.flex` and compiles +them to class names with the corresponding CSS injected. At runtime, accessing +these bindings directly will throw – make sure your code is built with the +StyleX compiler enabled. + diff --git a/packages/@stylexjs/inline-css/package.json b/packages/@stylexjs/inline-css/package.json new file mode 100644 index 000000000..1bb219287 --- /dev/null +++ b/packages/@stylexjs/inline-css/package.json @@ -0,0 +1,23 @@ +{ + "name": "@stylexjs/inline-css", + "version": "0.17.4", + "description": "Inline CSS property helpers for StyleX.", + "license": "MIT", + "main": "src/index.js", + "types": "src/index.d.ts", + "repository": { + "type": "git", + "url": "git+https://github.com/facebook/stylex.git" + }, + "sideEffects": false, + "peerDependencies": { + "@stylexjs/stylex": "^0.17.4" + }, + "dependencies": { + "csstype": "^3.1.3" + }, + "files": [ + "src" + ] +} + diff --git a/packages/@stylexjs/inline-css/src/index.d.ts b/packages/@stylexjs/inline-css/src/index.d.ts new file mode 100644 index 000000000..affa8c087 --- /dev/null +++ b/packages/@stylexjs/inline-css/src/index.d.ts @@ -0,0 +1,16 @@ +import type { StyleXClassNameFor } from '@stylexjs/stylex'; +import type { Properties } from 'csstype'; + +type InlineValue = { + [Key in string | number]: StyleXClassNameFor; +}; + +type InlineCSS = { + [Key in keyof Properties]: InlineValue< + Properties[Key] + >; +}; + +declare const inlineCSS: InlineCSS; + +export = inlineCSS; diff --git a/packages/@stylexjs/inline-css/src/index.js b/packages/@stylexjs/inline-css/src/index.js new file mode 100644 index 000000000..5fa83e800 --- /dev/null +++ b/packages/@stylexjs/inline-css/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. + */ + +'use strict'; + +const valueProxy = (propName) => + new Proxy( + {}, + { + get() { + throw new Error( + `@stylexjs/inline-css is a compile-time helper. Attempted to read the value '${propName}', but the StyleX compiler did not run.`, + ); + }, + }, + ); + +const inlineCSS = new Proxy( + {}, + { + get(_target, prop) { + if (typeof prop === 'string') { + return valueProxy(prop); + } + return undefined; + }, + }, +); + +module.exports = inlineCSS; +module.exports.default = inlineCSS; From bc271faa04bb977ac62c1aedb434a9150008bc78 Mon Sep 17 00:00:00 2001 From: Melissa Liu Date: Mon, 22 Dec 2025 04:18:01 -0500 Subject: [PATCH 2/8] add dynamic inline styles support --- .../__tests__/transform-stylex-props-test.js | 467 +++++++++++++++--- .../babel-plugin/src/utils/state-manager.js | 2 + .../babel-plugin/src/visitors/stylex-props.js | 284 +++++++++-- packages/@stylexjs/inline-css/README.md | 57 ++- packages/@stylexjs/inline-css/src/index.d.ts | 4 +- packages/@stylexjs/inline-css/src/index.js | 38 +- 6 files changed, 713 insertions(+), 139 deletions(-) diff --git a/packages/@stylexjs/babel-plugin/__tests__/transform-stylex-props-test.js b/packages/@stylexjs/babel-plugin/__tests__/transform-stylex-props-test.js index 4d11dad7f..56c55a4bc 100644 --- a/packages/@stylexjs/babel-plugin/__tests__/transform-stylex-props-test.js +++ b/packages/@stylexjs/babel-plugin/__tests__/transform-stylex-props-test.js @@ -55,87 +55,6 @@ describe('@stylexjs/babel-plugin', () => { `); }); - test('inline-css uses same classnames as stylex.create', () => { - const inline = transform(` - import stylex from 'stylex'; - import * as css from '@stylexjs/inline-css'; - stylex.props(css.display.flex); - `); - expect(inline).toMatchInlineSnapshot(` - "import _inject from "@stylexjs/stylex/lib/stylex-inject"; - var _inject2 = _inject; - import stylex from 'stylex'; - import * as css from '@stylexjs/inline-css'; - _inject2({ - ltr: ".x78zum5{display:flex}", - priority: 3000 - }); - ({ - className: "x78zum5" - });" - `); - const local = transform(` - import stylex from 'stylex'; - const styles = stylex.create({ - flex: { display: 'flex' }, - }); - stylex.props(styles.flex); - `); - expect(local).toMatchInlineSnapshot(` - "import _inject from "@stylexjs/stylex/lib/stylex-inject"; - var _inject2 = _inject; - import stylex from 'stylex'; - _inject2({ - ltr: ".x78zum5{display:flex}", - priority: 3000 - }); - ({ - className: "x78zum5" - });" - `); - expect(inline).toBe(local); - }); - - test('inline-css supports leading underscore value', () => { - const inline = transform(` - import stylex from 'stylex'; - import * as css from '@stylexjs/inline-css'; - stylex.props(css.padding._16px); - `); - expect(inline).toMatchInlineSnapshot(` - "import _inject from \\"@stylexjs/stylex/lib/stylex-inject\\"; - var _inject2 = _inject; - import stylex from 'stylex'; - _inject2({ - ltr: \\".x1tamke2{padding:16px}\\", - priority: 1000 - }); - ({ - className: \\"x1tamke2\\" - });" - `); - const local = transform(` - import stylex from 'stylex'; - const styles = stylex.create({ - pad: { padding: '16px' }, - }); - stylex.props(styles.pad); - `); - expect(local).toMatchInlineSnapshot(` - "import _inject from \\"@stylexjs/stylex/lib/stylex-inject\\"; - var _inject2 = _inject; - import stylex from 'stylex'; - _inject2({ - ltr: \\".x1tamke2{padding:16px}\\", - priority: 1000 - }); - ({ - className: \\"x1tamke2\\" - });" - `); - expect(inline).toBe(local); - }); - test('basic stylex call', () => { expect( transform(` @@ -312,6 +231,392 @@ describe('@stylexjs/babel-plugin', () => { }); }); + describe('props calls with inline-css', () => { + test('uses same classnames as stylex.create', () => { + const inline = transform(` + import stylex from 'stylex'; + import * as css from '@stylexjs/inline-css'; + stylex.props(css.display.flex); + `); + expect(inline).toMatchInlineSnapshot(` + "import _inject from "@stylexjs/stylex/lib/stylex-inject"; + var _inject2 = _inject; + import stylex from 'stylex'; + import * as css from '@stylexjs/inline-css'; + _inject2({ + ltr: ".x78zum5{display:flex}", + priority: 3000 + }); + ({ + className: "x78zum5" + });" + `); + const local = transform(` + import stylex from 'stylex'; + const styles = stylex.create({ + flex: { display: 'flex' }, + }); + stylex.props(styles.flex); + `); + expect(local).toMatchInlineSnapshot(` + "import _inject from "@stylexjs/stylex/lib/stylex-inject"; + var _inject2 = _inject; + import stylex from 'stylex'; + _inject2({ + ltr: ".x78zum5{display:flex}", + priority: 3000 + }); + ({ + className: "x78zum5" + });" + `); + }); + + test('supports leading underscore value', () => { + const inline = transform(` + import stylex from 'stylex'; + import * as css from '@stylexjs/inline-css'; + stylex.props(css.padding._16px); + `); + expect(inline).toMatchInlineSnapshot(` + "import _inject from "@stylexjs/stylex/lib/stylex-inject"; + var _inject2 = _inject; + import stylex from 'stylex'; + import * as css from '@stylexjs/inline-css'; + _inject2({ + ltr: ".x1tamke2{padding:16px}", + priority: 1000 + }); + ({ + className: "x1tamke2" + });" + `); + const local = transform(` + import stylex from 'stylex'; + const styles = stylex.create({ + pad: { padding: '16px' }, + }); + stylex.props(styles.pad); + `); + expect(local).toMatchInlineSnapshot(` + "import _inject from "@stylexjs/stylex/lib/stylex-inject"; + var _inject2 = _inject; + import stylex from 'stylex'; + _inject2({ + ltr: ".x1tamke2{padding:16px}", + priority: 1000 + }); + ({ + className: "x1tamke2" + });" + `); + }); + + test('supports key syntax', () => { + const inline = transform(` + import stylex from 'stylex'; + import * as css from '@stylexjs/inline-css'; + stylex.props(css.width['calc(100% - 20px)']); + `); + expect(inline).toMatchInlineSnapshot(` + "import _inject from "@stylexjs/stylex/lib/stylex-inject"; + var _inject2 = _inject; + import stylex from 'stylex'; + import * as css from '@stylexjs/inline-css'; + _inject2({ + ltr: ".xnlsq7q{width:calc(100% - 20px)}", + priority: 4000 + }); + ({ + className: "xnlsq7q" + });" + `); + const local = transform(` + import stylex from 'stylex'; + const styles = stylex.create({ + w: { width: 'calc(100% - 20px)' }, + }); + stylex.props(styles.w); + `); + expect(inline).toMatchInlineSnapshot(` + "import _inject from "@stylexjs/stylex/lib/stylex-inject"; + var _inject2 = _inject; + import stylex from 'stylex'; + import * as css from '@stylexjs/inline-css'; + _inject2({ + ltr: ".xnlsq7q{width:calc(100% - 20px)}", + priority: 4000 + }); + ({ + className: "xnlsq7q" + });" + `); + expect(local).toMatchInlineSnapshot(` + "import _inject from "@stylexjs/stylex/lib/stylex-inject"; + var _inject2 = _inject; + import stylex from 'stylex'; + _inject2({ + ltr: ".xnlsq7q{width:calc(100% - 20px)}", + priority: 4000 + }); + ({ + className: "xnlsq7q" + });" + `); + }); + + test('dynamic style', () => { + const inline = transform(` + import stylex from 'stylex'; + import * as css from '@stylexjs/inline-css'; + stylex.props(css.color(color)); + `); + const local = transform(` + import stylex from 'stylex'; + const styles = stylex.create({ + color: (c) => ({ color: c }), + }); + stylex.props(styles.color(color)); + `); + expect(local).toMatchInlineSnapshot(` + "import _inject from "@stylexjs/stylex/lib/stylex-inject"; + var _inject2 = _inject; + import stylex from 'stylex'; + _inject2({ + ltr: ".x14rh7hd{color:var(--x-color)}", + priority: 3000 + }); + _inject2({ + ltr: "@property --x-color { syntax: \\"*\\"; inherits: false;}", + priority: 0 + }); + const styles = { + color: c => [{ + kMwMTN: c != null ? "x14rh7hd" : c, + $$css: true + }, { + "--x-color": c != null ? c : undefined + }] + }; + stylex.props(styles.color(color));" + `); + expect(inline).toMatchInlineSnapshot(` + "import _inject from "@stylexjs/stylex/lib/stylex-inject"; + var _inject2 = _inject; + import stylex from 'stylex'; + import * as css from '@stylexjs/inline-css'; + _inject2({ + ltr: ".x14rh7hd{color:var(--x-color)}", + priority: 3000 + }); + _inject2({ + ltr: "@property --x-color { syntax: \\"*\\"; inherits: false;}", + priority: 0 + }); + stylex.props([{ + "color": color != null ? "x14rh7hd" : color, + "$$css": true + }, { + "--x-color": color != null ? color : undefined + }]);" + `); + }); + + test('inline static with inline dynamic', () => { + const output = transform(` + import stylex from 'stylex'; + import * as css from '@stylexjs/inline-css'; + stylex.props(css.display.flex, css.color(color)); + `); + expect(output).toContain('.x78zum5{display:flex}'); + expect(output).toContain('.x14rh7hd{color:var(--x-color)}'); + expect(output).toContain('--x-color'); + expect(output).toContain('color != null ? "x14rh7hd" : color'); + }); + + test('inline static with create dynamic', () => { + const output = transform(` + import stylex from 'stylex'; + import * as css from '@stylexjs/inline-css'; + const styles = stylex.create({ + opacity: (o) => ({ opacity: o }), + }); + stylex.props(css.display.flex, styles.opacity(0.5)); + `); + expect(output).toContain('.x78zum5{display:flex}'); + expect(output).toContain('.xb4nw82{opacity:var(--x-opacity)}'); + expect(output).toContain('--x-opacity'); + }); + + test('inline dynamic with create dynamic', () => { + const output = transform(` + import stylex from 'stylex'; + import * as css from '@stylexjs/inline-css'; + const styles = stylex.create({ + opacity: (o) => ({ opacity: o }), + }); + stylex.props(css.color(color), styles.opacity(0.5)); + `); + expect(output).toContain('.x14rh7hd{color:var(--x-color)}'); + expect(output).toContain('.xb4nw82{opacity:var(--x-opacity)}'); + expect(output).toContain('--x-color'); + expect(output).toContain('--x-opacity'); + }); + + describe('with options', () => { + test('dev/debug classnames for inline-css', () => { + const inline = transform( + ` + import stylex from 'stylex'; + import * as css from '@stylexjs/inline-css'; + stylex.props(css.display.flex); + `, + { + dev: true, + debug: true, + enableDevClassNames: true, + enableDebugClassNames: true, + filename: '/tmp/Foo.js', + }, + ); + expect(inline).toMatchInlineSnapshot(` + "import _inject from "@stylexjs/stylex/lib/stylex-inject"; + var _inject2 = _inject; + import stylex from 'stylex'; + import * as css from '@stylexjs/inline-css'; + _inject2({ + ltr: ".display-x78zum5{display:flex}", + priority: 3000 + }); + ({ + className: "Foo____inline__ display-x78zum5" + });" + `); + }); + }); + }); + + test('inline static + inline dynamic coexist', () => { + const inline = transform(` + import stylex from 'stylex'; + import * as css from '@stylexjs/inline-css'; + stylex.props(css.display.flex, css.color(color)); + `); + expect(inline).toMatchInlineSnapshot(` + "import _inject from "@stylexjs/stylex/lib/stylex-inject"; + var _inject2 = _inject; + import stylex from 'stylex'; + import * as css from '@stylexjs/inline-css'; + _inject2({ + ltr: ".x78zum5{display:flex}", + priority: 3000 + }); + _inject2({ + ltr: ".x14rh7hd{color:var(--x-color)}", + priority: 3000 + }); + _inject2({ + ltr: "@property --x-color { syntax: \\"*\\"; inherits: false;}", + priority: 0 + }); + stylex.props([{ + display: "x78zum5", + color: color != null ? "x14rh7hd" : color, + $$css: true + }, { + "--x-color": color != null ? color : undefined + }]);" + `); + }); + + test('inline static + create dynamic', () => { + const output = transform(` + import stylex from 'stylex'; + import * as css from '@stylexjs/inline-css'; + const styles = stylex.create({ + opacity: (o) => ({ opacity: o }), + }); + stylex.props(css.display.flex, styles.opacity(0.5)); + `); + expect(output).toMatchInlineSnapshot(` + "import _inject from "@stylexjs/stylex/lib/stylex-inject"; + var _inject2 = _inject; + import stylex from 'stylex'; + import * as css from '@stylexjs/inline-css'; + _inject2({ + ltr: ".x78zum5{display:flex}", + priority: 3000 + }); + _inject2({ + ltr: ".xb4nw82{opacity:var(--x-opacity)}", + priority: 3000 + }); + _inject2({ + ltr: "@property --x-opacity { syntax: \\"*\\"; inherits: false;}", + priority: 0 + }); + const styles = { + opacity: o => [{ + kSiTet: o != null ? "xb4nw82" : o, + $$css: true + }, { + "--x-opacity": o != null ? o : undefined + }] + }; + stylex.props([{ + display: "x78zum5", + $$css: true + }, styles.opacity(0.5)]);" + `); + }); + + test('inline dynamic + create dynamic', () => { + const output = transform(` + import stylex from 'stylex'; + import * as css from '@stylexjs/inline-css'; + const styles = stylex.create({ + opacity: (o) => ({ opacity: o }), + }); + stylex.props(css.color(color), styles.opacity(0.5)); + `); + expect(output).toMatchInlineSnapshot(` + "import _inject from "@stylexjs/stylex/lib/stylex-inject"; + var _inject2 = _inject; + import stylex from 'stylex'; + import * as css from '@stylexjs/inline-css'; + _inject2({ + ltr: ".x14rh7hd{color:var(--x-color)}", + priority: 3000 + }); + _inject2({ + ltr: ".xb4nw82{opacity:var(--x-opacity)}", + priority: 3000 + }); + _inject2({ + ltr: "@property --x-color { syntax: \\"*\\"; inherits: false;}", + priority: 0 + }); + _inject2({ + ltr: "@property --x-opacity { syntax: \\"*\\"; inherits: false;}", + priority: 0 + }); + const styles = { + opacity: o => [{ + kSiTet: o != null ? "xb4nw82" : o, + $$css: true + }, { + "--x-opacity": o != null ? o : undefined + }] + }; + stylex.props([{ + color: color != null ? "x14rh7hd" : color, + $$css: true + }, { + "--x-color": color != null ? color : undefined + }, styles.opacity(0.5)]);" + `); + }); + test('stylex call with number', () => { expect( transform(` diff --git a/packages/@stylexjs/babel-plugin/src/utils/state-manager.js b/packages/@stylexjs/babel-plugin/src/utils/state-manager.js index 4246a9962..226ea0165 100644 --- a/packages/@stylexjs/babel-plugin/src/utils/state-manager.js +++ b/packages/@stylexjs/babel-plugin/src/utils/state-manager.js @@ -175,6 +175,8 @@ export default class StateManager { +styleVars: Map> = new Map(); +inlineStylesCache: Map = new Map(); + +inlineDynamicCache: Map = + new Map(); // results of `stylex.create` calls that should be kept +styleVarsToKeep: Set<[string, true | string, true | Array]> = diff --git a/packages/@stylexjs/babel-plugin/src/visitors/stylex-props.js b/packages/@stylexjs/babel-plugin/src/visitors/stylex-props.js index 0c891b9aa..6b5000656 100644 --- a/packages/@stylexjs/babel-plugin/src/visitors/stylex-props.js +++ b/packages/@stylexjs/babel-plugin/src/visitors/stylex-props.js @@ -17,11 +17,14 @@ import { convertObjectToAST } from '../utils/js-to-ast'; import { evaluate } from '../utils/evaluate-path'; import stylexDefaultMarker from '../shared/stylex-defaultMarker'; import styleXCreateSet from '../shared/stylex-create'; +import { convertStyleToClassName } from '../shared/utils/convert-to-className'; import { injectDevClassNames, convertToTestStyles, } from '../utils/dev-classname'; +const INLINE_CSS_SOURCE = '@stylexjs/inline-css'; + type ClassNameValue = string | null | boolean | NonStringClassNameValue; type NonStringClassNameValue = [t.Expression, ClassNameValue, ClassNameValue]; @@ -29,6 +32,13 @@ type StyleObject = { [key: string]: string | null | boolean, }; +type InlineCSSTuple = [ + StyleObject, + $ReadOnly<{ + [string]: mixed, + }>, +]; + class ConditionalStyle { test: t.Expression; primary: ?StyleObject; @@ -44,14 +54,23 @@ class ConditionalStyle { } } -type ResolvedArg = ?StyleObject | ConditionalStyle; +type ResolvedArg = + | ?(StyleObject | InlineCSSTuple | InlineCSSDynamic) + | ConditionalStyle; type ResolvedArgs = Array; -type InlineStyle = $ReadOnly<{ +type InlineCSS = $ReadOnly<{ property: string, value: string | number, }>; +type InlineCSSDynamic = $ReadOnly<{ + className: string, + classKey: string, + varName: string, + value: t.Expression, +}>; + export function skipStylexPropsChildren( path: NodePath, state: StateManager, @@ -119,7 +138,8 @@ export default function transformStylexProps( if ( argPath.isObjectExpression() || argPath.isIdentifier() || - argPath.isMemberExpression() + argPath.isMemberExpression() || + argPath.isCallExpression() ) { const resolved = parseNullableStyle(argPath, state, evaluatePathFnConfig); if (resolved === 'other') { @@ -188,7 +208,7 @@ export default function transformStylexProps( bailOut = true; } if (bailOut) { - break; + continue; } } if (!state.options.enableInlinedConditionalMerge && conditional) { @@ -332,7 +352,7 @@ function parseNullableStyle( path: NodePath, state: StateManager, evaluatePathFnConfig: FunctionConfig, -): null | StyleObject | 'other' { +): null | StyleObject | InlineCSSTuple | InlineCSSDynamic | 'other' { const node = path.node; if ( t.isNullLiteral(node) || @@ -341,9 +361,58 @@ function parseNullableStyle( return null; } - const inlineStyle = getInlineStyle(path, state); - if (inlineStyle != null) { - return compileInlineStyle(inlineStyle, state, path); + const inlineCSS = resolveInlineCSS(path, state); + if (inlineCSS != null) { + if (!Array.isArray(inlineCSS) && path.isCallExpression()) { + const { className, classKey, varName, value } = inlineCSS; + const compiledObj = t.objectExpression([ + t.objectProperty( + t.stringLiteral(classKey), + t.conditionalExpression( + t.binaryExpression('!=', value, t.nullLiteral()), + t.stringLiteral(className), + value, + ), + ), + t.objectProperty(t.stringLiteral('$$css'), t.booleanLiteral(true)), + ]); + const inlineObj = t.objectExpression([ + t.objectProperty( + t.stringLiteral(varName), + t.conditionalExpression( + t.binaryExpression('!=', value, t.nullLiteral()), + value, + t.identifier('undefined'), + ), + ), + ]); + path.replaceWith(t.arrayExpression([compiledObj, inlineObj])); + return 'other'; + } + if (!Array.isArray(inlineCSS) && !path.isCallExpression()) { + // Inline static CSS should be inlined immediately so it survives later bailouts. + path.replaceWith(convertObjectToAST(inlineCSS)); + return inlineCSS; + } + if (Array.isArray(inlineCSS) && path.isCallExpression()) { + // Dynamic inline-css cannot be reduced to plain objects for the build-time + // props optimization. Replace the argument with the compiled tuple so + // runtime stylex.props receives the correct value, then bail out. + const [compiled, inlineVars] = inlineCSS; + path.replaceWith( + t.arrayExpression([ + convertObjectToAST(compiled), + convertObjectToAST(inlineVars), + ]), + ); + return 'other'; + } + // Static inline-css: inline it immediately so it survives any later bailouts. + if (!Array.isArray(inlineCSS)) { + path.replaceWith(convertObjectToAST(inlineCSS)); + return inlineCSS; + } + return inlineCSS; } if (t.isMemberExpression(node)) { @@ -395,10 +464,29 @@ function parseNullableStyle( return 'other'; } -function getInlineStyle( +function resolveInlineCSS( path: NodePath, state: StateManager, -): null | InlineStyle { +): null | StyleObject | InlineCSSTuple | InlineCSSDynamic { + if (path.isCallExpression()) { + const dynamic = getInlineDynamicStyle(path, state); + if (dynamic != null) { + return compileInlineDynamicStyle(dynamic, state, path); + } + } + + const inlineStyle = getInlineStaticCSS(path, state); + if (inlineStyle != null) { + return compileInlineStaticCSS(inlineStyle, state, path); + } + + return null; +} + +function getInlineStaticCSS( + path: NodePath, + state: StateManager, +): null | InlineCSS { const node = path.node; if (!t.isMemberExpression(node)) { return null; @@ -411,22 +499,63 @@ function getInlineStyle( const parent = node.object; - if (t.isIdentifier(parent) && state.inlineCSSNamedImports.has(parent.name)) { - const propName = state.inlineCSSNamedImports.get(parent.name); - if (propName != null) { + if (t.isIdentifier(parent) && isInlineCSSIdentifier(parent, state, path)) { + return { property: valueKey, value: normalizeInlineValue(valueKey) }; + } + + if (t.isMemberExpression(parent)) { + const propName = getPropKey(parent.property, parent.computed); + const base = parent.object; + if ( + propName != null && + t.isIdentifier(base) && + isInlineCSSIdentifier(base, state, path) + ) { return { property: propName, value: normalizeInlineValue(valueKey) }; } } + return null; +} + +function getInlineDynamicStyle( + path: NodePath, + state: StateManager, +): null | { property: string, value: t.Expression } { + const callee = path.get('callee'); + if (!callee.isMemberExpression()) { + return null; + } + const valueKey = getPropKey(callee.node.property, callee.node.computed); + if (valueKey == null) { + return null; + } + const parent = callee.node.object; + + if ( + t.isIdentifier(parent) && + path.node.arguments.length === 1 && + isInlineCSSIdentifier(parent, state, path) + ) { + return { + property: valueKey, + value: path.get('arguments')[0].node, + }; + } + if (t.isMemberExpression(parent)) { const propName = getPropKey(parent.property, parent.computed); const base = parent.object; if ( propName != null && t.isIdentifier(base) && - state.inlineCSSNamespaceImports.has(base.name) + isInlineCSSIdentifier(base, state, path) && + path.node.arguments.length === 1 ) { - return { property: propName, value: normalizeInlineValue(valueKey) }; + return { + property: propName, + value: path.get('arguments')[0].node, + }; } } @@ -456,8 +585,47 @@ function getPropKey( return null; } -function compileInlineStyle( - inlineStyle: InlineStyle, +function isInlineCSSIdentifier( + ident: t.Identifier, + state: StateManager, + path: NodePath<>, +): boolean { + if ( + state.inlineCSSNamedImports.has(ident.name) || + state.inlineCSSNamespaceImports.has(ident.name) + ) { + return true; + } + const binding = path.scope?.getBinding(ident.name); + if ( + binding && + binding.path.isImportSpecifier() && + binding.path.parent.type === 'ImportDeclaration' && + binding.path.parent.source.value === INLINE_CSS_SOURCE + ) { + return true; + } + if ( + binding && + binding.path.isImportNamespaceSpecifier() && + binding.path.parent.type === 'ImportDeclaration' && + binding.path.parent.source.value === INLINE_CSS_SOURCE + ) { + return true; + } + if ( + binding && + binding.path.isImportDefaultSpecifier() && + binding.path.parent.type === 'ImportDeclaration' && + binding.path.parent.source.value === INLINE_CSS_SOURCE + ) { + return true; + } + return false; +} + +function compileInlineStaticCSS( + inlineStyle: InlineCSS, state: StateManager, path: NodePath, ): StyleObject { @@ -477,29 +645,85 @@ function compileInlineStyle( state.options, ); - let compiled = compiledNamespaces.__inline__; + const compiled = applyDevTest(compiledNamespaces.__inline__, state); + + const listOfStyles = Object.entries(injectedStyles).map( + ([key, { priority, ...rest }]) => [key, rest, priority], + ); + state.registerStyles(listOfStyles, path); + + state.inlineStylesCache.set(cacheKey, compiled); + return compiled; +} + +function compileInlineDynamicStyle( + inlineStyle: { property: string, value: t.Expression }, + state: StateManager, + path: NodePath, +): InlineCSSDynamic { + const { property, value } = inlineStyle; + const cacheKey = property; + let cached = state.inlineDynamicCache.get(cacheKey); + + if (cached == null) { + const varName = `--x-${property}`; + const [classKey, className, styleObj] = convertStyleToClassName( + [property, `var(${varName})`], + [], + [], + [], + state.options, + ); + const { priority, ...rest } = styleObj; + + const propertyInjection = { + priority: 0, + ltr: `@property ${varName} { syntax: "*"; inherits: false;}`, + rtl: null, + }; + + state.registerStyles( + [ + [classKey, rest, priority], + [`${classKey}-var`, propertyInjection, 0], + ], + path, + ); + + cached = { className, varName, classKey }; + state.inlineDynamicCache.set(cacheKey, cached); + } + + const { className, varName, classKey } = cached; + + return { + className, + classKey, + varName, + value, + }; +} + +function applyDevTest( + compiled: { [string]: string | null, $$css: true }, + state: StateManager, +): { [string]: string | null, $$css: true } { + let result = compiled; if (state.isDev && state.options.enableDevClassNames) { - compiled = injectDevClassNames( - { __inline__: compiled }, + result = injectDevClassNames( + { __inline__: result }, null, state, ).__inline__; } if (state.isTest) { - compiled = convertToTestStyles( - { __inline__: compiled }, + result = convertToTestStyles( + { __inline__: result }, null, state, ).__inline__; } - - const listOfStyles = Object.entries(injectedStyles).map( - ([key, { priority, ...rest }]) => [key, rest, priority], - ); - state.registerStyles(listOfStyles, path); - - state.inlineStylesCache.set(cacheKey, compiled); - return compiled; + return result; } function makeStringExpression(values: ResolvedArgs): t.Expression { diff --git a/packages/@stylexjs/inline-css/README.md b/packages/@stylexjs/inline-css/README.md index 5de582428..aa6c925e9 100644 --- a/packages/@stylexjs/inline-css/README.md +++ b/packages/@stylexjs/inline-css/README.md @@ -1,6 +1,16 @@ # @stylexjs/inline-css -Compile-time helpers that let you author StyleX styles with raw CSS properties: +Compile-time helpers for authoring StyleX styles inline inside stylex.props(). + +This package exposes CSS properties as a namespaced object and lets you express +static and dynamic styles using normal JavaScript syntax. There is no new +runtime styling system and no design tokens — everything compiles to the same +output as stylex.create. + +The compiler treats inline styles from this package as if they were authored +locally, enabling the same optimizations as normal StyleX styles. + +## Usage ```js import * as stylex from '@stylexjs/stylex'; @@ -14,16 +24,49 @@ function Example({ color }) { css.flexDirection.column, css.padding._16px, css.width['calc(100% - 20cqi)'], - css.color[color], + css.color(color), )} /> ); } ``` -The bindings exported here are compile-time only. The StyleX Babel plugin -detects property/value member expressions like `css.display.flex` and compiles -them to class names with the corresponding CSS injected. At runtime, accessing -these bindings directly will throw – make sure your code is built with the -StyleX compiler enabled. +### Static values +Static styles are expressed via property access and are fully resolved at +compile time. + +```js +css.display.flex; +css.flexDirection.column; +``` + +#### Values starting with numbers + +Use a leading underscore for values that begin with a number. The underscore is +ignored by the compiler and has no semantic meaning. + +```js +css.padding._16px; +css.fontSize._1rem; +``` + +### Complex literal values + +For values that are not valid JavaScript identifiers (for example, values that +contain spaces or symbols), use computed property syntax. + +```js +css.fontSize['1.25rem']; +css.width['calc(100% - 20cqi)']; +css.gridTemplateColumns['1fr minmax(0, 3fr)']; +``` + +### Dynamic values + +Dynamic styles use call syntax and should be used sparingly. + +```js +css.color(color); +css.marginLeft(offset); +``` diff --git a/packages/@stylexjs/inline-css/src/index.d.ts b/packages/@stylexjs/inline-css/src/index.d.ts index affa8c087..67bd4adad 100644 --- a/packages/@stylexjs/inline-css/src/index.d.ts +++ b/packages/@stylexjs/inline-css/src/index.d.ts @@ -1,9 +1,9 @@ -import type { StyleXClassNameFor } from '@stylexjs/stylex'; +import type { StyleXClassNameFor, StyleXStyles } from '@stylexjs/stylex'; import type { Properties } from 'csstype'; type InlineValue = { [Key in string | number]: StyleXClassNameFor; -}; +} & ((value: V) => StyleXStyles); type InlineCSS = { [Key in keyof Properties]: InlineValue< diff --git a/packages/@stylexjs/inline-css/src/index.js b/packages/@stylexjs/inline-css/src/index.js index 5fa83e800..89598f6e1 100644 --- a/packages/@stylexjs/inline-css/src/index.js +++ b/packages/@stylexjs/inline-css/src/index.js @@ -7,29 +7,29 @@ 'use strict'; -const valueProxy = (propName) => - new Proxy( - {}, - { - get() { - throw new Error( - `@stylexjs/inline-css is a compile-time helper. Attempted to read the value '${propName}', but the StyleX compiler did not run.`, - ); - }, +const valueProxy = (_propName) => + new Proxy(function () {}, { + get() { + return valueProxy(''); }, - ); - -const inlineCSS = new Proxy( - {}, - { - get(_target, prop) { - if (typeof prop === 'string') { - return valueProxy(prop); - } + apply() { return undefined; }, + }); + +const inlineCSS = new Proxy(function () {}, { + get(_target, prop) { + if (typeof prop === 'string') { + return valueProxy(prop); + } + return undefined; + }, + apply() { + throw new Error( + '@stylexjs/inline-css is a compile-time helper. Attempted to call it as a function, but the StyleX compiler did not run.', + ); }, -); +}); module.exports = inlineCSS; module.exports.default = inlineCSS; From a61d115a917b41a61b9cc00439cd4f5f0e8dc075 Mon Sep 17 00:00:00 2001 From: Melissa Liu Date: Mon, 22 Dec 2025 05:19:10 -0500 Subject: [PATCH 3/8] add dedupe tests and flow fix --- .../__tests__/transform-stylex-props-test.js | 280 ++++++++++-------- .../babel-plugin/src/utils/state-manager.js | 10 +- .../babel-plugin/src/visitors/stylex-props.js | 92 +++--- 3 files changed, 201 insertions(+), 181 deletions(-) diff --git a/packages/@stylexjs/babel-plugin/__tests__/transform-stylex-props-test.js b/packages/@stylexjs/babel-plugin/__tests__/transform-stylex-props-test.js index 56c55a4bc..b67bfc770 100644 --- a/packages/@stylexjs/babel-plugin/__tests__/transform-stylex-props-test.js +++ b/packages/@stylexjs/babel-plugin/__tests__/transform-stylex-props-test.js @@ -232,7 +232,7 @@ describe('@stylexjs/babel-plugin', () => { }); describe('props calls with inline-css', () => { - test('uses same classnames as stylex.create', () => { + test('inline static styles match stylex.create', () => { const inline = transform(` import stylex from 'stylex'; import * as css from '@stylexjs/inline-css'; @@ -272,7 +272,7 @@ describe('@stylexjs/babel-plugin', () => { `); }); - test('supports leading underscore value', () => { + test('inline static supports leading underscore value', () => { const inline = transform(` import stylex from 'stylex'; import * as css from '@stylexjs/inline-css'; @@ -312,7 +312,7 @@ describe('@stylexjs/babel-plugin', () => { `); }); - test('supports key syntax', () => { + test('inline static supports computed key syntax', () => { const inline = transform(` import stylex from 'stylex'; import * as css from '@stylexjs/inline-css'; @@ -365,6 +365,42 @@ describe('@stylexjs/babel-plugin', () => { `); }); + test('dedupes duplicate properties across create and inline-css', () => { + const output = transform(` + import stylex from 'stylex'; + import * as css from '@stylexjs/inline-css'; + const styles = stylex.create({ + base: { color: 'red', backgroundColor: 'white' }, + }); + stylex.props(styles.base, css.color.blue, css.backgroundColor.white); + `); + expect(output).toMatchInlineSnapshot(` + "import _inject from "@stylexjs/stylex/lib/stylex-inject"; + var _inject2 = _inject; + import stylex from 'stylex'; + import * as css from '@stylexjs/inline-css'; + _inject2({ + ltr: ".x1e2nbdu{color:red}", + priority: 3000 + }); + _inject2({ + ltr: ".x12peec7{background-color:white}", + priority: 3000 + }); + _inject2({ + ltr: ".xju2f9n{color:blue}", + priority: 3000 + }); + _inject2({ + ltr: ".x12peec7{background-color:white}", + priority: 3000 + }); + ({ + className: "xju2f9n x12peec7" + });" + `); + }); + test('dynamic style', () => { const inline = transform(` import stylex from 'stylex'; @@ -463,6 +499,123 @@ describe('@stylexjs/babel-plugin', () => { expect(output).toContain('--x-opacity'); }); + test('inline static + inline dynamic coexist', () => { + const inline = transform(` + import stylex from 'stylex'; + import * as css from '@stylexjs/inline-css'; + stylex.props(css.display.flex, css.color(color)); + `); + expect(inline).toMatchInlineSnapshot(` + "import _inject from "@stylexjs/stylex/lib/stylex-inject"; + var _inject2 = _inject; + import stylex from 'stylex'; + import * as css from '@stylexjs/inline-css'; + _inject2({ + ltr: ".x78zum5{display:flex}", + priority: 3000 + }); + _inject2({ + ltr: ".x14rh7hd{color:var(--x-color)}", + priority: 3000 + }); + _inject2({ + ltr: "@property --x-color { syntax: \\"*\\"; inherits: false;}", + priority: 0 + }); + stylex.props({ + k1xSpc: "x78zum5", + $$css: true + }, [{ + "color": color != null ? "x14rh7hd" : color, + "$$css": true + }, { + "--x-color": color != null ? color : undefined + }]);" + `); + }); + + test('inline static + create dynamic', () => { + const output = transform(` + import stylex from 'stylex'; + import * as css from '@stylexjs/inline-css'; + const styles = stylex.create({ + opacity: (o) => ({ opacity: o }), + }); + stylex.props(css.display.flex, styles.opacity(0.5)); + `); + expect(output).toMatchInlineSnapshot(` + "import _inject from "@stylexjs/stylex/lib/stylex-inject"; + var _inject2 = _inject; + import stylex from 'stylex'; + import * as css from '@stylexjs/inline-css'; + _inject2({ + ltr: ".xb4nw82{opacity:var(--x-opacity)}", + priority: 3000 + }); + _inject2({ + ltr: "@property --x-opacity { syntax: \\"*\\"; inherits: false;}", + priority: 0 + }); + _inject2({ + ltr: ".x78zum5{display:flex}", + priority: 3000 + }); + ({ + className: "x78zum5 xb4nw82", + style: { + "--x-opacity": 0.5 + } + });" + `); + }); + + test('inline dynamic + create dynamic', () => { + const output = transform(` + import stylex from 'stylex'; + import * as css from '@stylexjs/inline-css'; + const styles = stylex.create({ + opacity: (o) => ({ opacity: o }), + }); + stylex.props(css.color(color), styles.opacity(0.5)); + `); + expect(output).toMatchInlineSnapshot(` + "import _inject from "@stylexjs/stylex/lib/stylex-inject"; + var _inject2 = _inject; + import stylex from 'stylex'; + import * as css from '@stylexjs/inline-css'; + _inject2({ + ltr: ".xb4nw82{opacity:var(--x-opacity)}", + priority: 3000 + }); + _inject2({ + ltr: "@property --x-opacity { syntax: \\"*\\"; inherits: false;}", + priority: 0 + }); + const styles = { + opacity: o => [{ + kSiTet: o != null ? "xb4nw82" : o, + $$css: true + }, { + "--x-opacity": o != null ? o : undefined + }] + }; + _inject2({ + ltr: ".x14rh7hd{color:var(--x-color)}", + priority: 3000 + }); + _inject2({ + ltr: "@property --x-color { syntax: \\"*\\"; inherits: false;}", + priority: 0 + }); + stylex.props([{ + "color": color != null ? "x14rh7hd" : color, + "$$css": true + }, { + "--x-color": color != null ? color : undefined + }], styles.opacity(0.5));" + `); + }); + describe('with options', () => { test('dev/debug classnames for inline-css', () => { const inline = transform( @@ -496,127 +649,6 @@ describe('@stylexjs/babel-plugin', () => { }); }); - test('inline static + inline dynamic coexist', () => { - const inline = transform(` - import stylex from 'stylex'; - import * as css from '@stylexjs/inline-css'; - stylex.props(css.display.flex, css.color(color)); - `); - expect(inline).toMatchInlineSnapshot(` - "import _inject from "@stylexjs/stylex/lib/stylex-inject"; - var _inject2 = _inject; - import stylex from 'stylex'; - import * as css from '@stylexjs/inline-css'; - _inject2({ - ltr: ".x78zum5{display:flex}", - priority: 3000 - }); - _inject2({ - ltr: ".x14rh7hd{color:var(--x-color)}", - priority: 3000 - }); - _inject2({ - ltr: "@property --x-color { syntax: \\"*\\"; inherits: false;}", - priority: 0 - }); - stylex.props([{ - display: "x78zum5", - color: color != null ? "x14rh7hd" : color, - $$css: true - }, { - "--x-color": color != null ? color : undefined - }]);" - `); - }); - - test('inline static + create dynamic', () => { - const output = transform(` - import stylex from 'stylex'; - import * as css from '@stylexjs/inline-css'; - const styles = stylex.create({ - opacity: (o) => ({ opacity: o }), - }); - stylex.props(css.display.flex, styles.opacity(0.5)); - `); - expect(output).toMatchInlineSnapshot(` - "import _inject from "@stylexjs/stylex/lib/stylex-inject"; - var _inject2 = _inject; - import stylex from 'stylex'; - import * as css from '@stylexjs/inline-css'; - _inject2({ - ltr: ".x78zum5{display:flex}", - priority: 3000 - }); - _inject2({ - ltr: ".xb4nw82{opacity:var(--x-opacity)}", - priority: 3000 - }); - _inject2({ - ltr: "@property --x-opacity { syntax: \\"*\\"; inherits: false;}", - priority: 0 - }); - const styles = { - opacity: o => [{ - kSiTet: o != null ? "xb4nw82" : o, - $$css: true - }, { - "--x-opacity": o != null ? o : undefined - }] - }; - stylex.props([{ - display: "x78zum5", - $$css: true - }, styles.opacity(0.5)]);" - `); - }); - - test('inline dynamic + create dynamic', () => { - const output = transform(` - import stylex from 'stylex'; - import * as css from '@stylexjs/inline-css'; - const styles = stylex.create({ - opacity: (o) => ({ opacity: o }), - }); - stylex.props(css.color(color), styles.opacity(0.5)); - `); - expect(output).toMatchInlineSnapshot(` - "import _inject from "@stylexjs/stylex/lib/stylex-inject"; - var _inject2 = _inject; - import stylex from 'stylex'; - import * as css from '@stylexjs/inline-css'; - _inject2({ - ltr: ".x14rh7hd{color:var(--x-color)}", - priority: 3000 - }); - _inject2({ - ltr: ".xb4nw82{opacity:var(--x-opacity)}", - priority: 3000 - }); - _inject2({ - ltr: "@property --x-color { syntax: \\"*\\"; inherits: false;}", - priority: 0 - }); - _inject2({ - ltr: "@property --x-opacity { syntax: \\"*\\"; inherits: false;}", - priority: 0 - }); - const styles = { - opacity: o => [{ - kSiTet: o != null ? "xb4nw82" : o, - $$css: true - }, { - "--x-opacity": o != null ? o : undefined - }] - }; - stylex.props([{ - color: color != null ? "x14rh7hd" : color, - $$css: true - }, { - "--x-color": color != null ? color : undefined - }, styles.opacity(0.5)]);" - `); - }); - test('stylex call with number', () => { expect( transform(` diff --git a/packages/@stylexjs/babel-plugin/src/utils/state-manager.js b/packages/@stylexjs/babel-plugin/src/utils/state-manager.js index 226ea0165..bb9294362 100644 --- a/packages/@stylexjs/babel-plugin/src/utils/state-manager.js +++ b/packages/@stylexjs/babel-plugin/src/utils/state-manager.js @@ -13,6 +13,7 @@ import type { CompiledNamespaces, StyleXOptions as RuntimeOptions, } from '../shared'; +import type { FlatCompiledStyles } from '../shared/common-types'; import type { Check } from './validate'; import * as t from '@babel/types'; @@ -173,10 +174,11 @@ export default class StateManager { // `stylex.create` calls +styleMap: Map = new Map(); +styleVars: Map> = new Map(); - +inlineStylesCache: Map = - new Map(); - +inlineDynamicCache: Map = - new Map(); + +inlineStylesCache: Map = new Map(); + +inlineDynamicCache: Map< + string, + { className: string, varName: string, classKey: string }, + > = new Map(); // results of `stylex.create` calls that should be kept +styleVarsToKeep: Set<[string, true | string, true | Array]> = diff --git a/packages/@stylexjs/babel-plugin/src/visitors/stylex-props.js b/packages/@stylexjs/babel-plugin/src/visitors/stylex-props.js index 6b5000656..17636895d 100644 --- a/packages/@stylexjs/babel-plugin/src/visitors/stylex-props.js +++ b/packages/@stylexjs/babel-plugin/src/visitors/stylex-props.js @@ -9,6 +9,7 @@ import type { NodePath } from '@babel/traverse'; import type { FunctionConfig } from '../utils/evaluate-path'; +import type { FlatCompiledStyles } from '../shared/common-types'; import * as t from '@babel/types'; import StateManager from '../utils/state-manager'; @@ -28,16 +29,10 @@ const INLINE_CSS_SOURCE = '@stylexjs/inline-css'; type ClassNameValue = string | null | boolean | NonStringClassNameValue; type NonStringClassNameValue = [t.Expression, ClassNameValue, ClassNameValue]; -type StyleObject = { - [key: string]: string | null | boolean, -}; - -type InlineCSSTuple = [ - StyleObject, - $ReadOnly<{ - [string]: mixed, - }>, -]; +type StyleObject = $ReadOnly<{ + [key: string]: string | null, + $$css?: true | string, +}>; class ConditionalStyle { test: t.Expression; @@ -54,9 +49,7 @@ class ConditionalStyle { } } -type ResolvedArg = - | ?(StyleObject | InlineCSSTuple | InlineCSSDynamic) - | ConditionalStyle; +type ResolvedArg = ?StyleObject | ConditionalStyle; type ResolvedArgs = Array; type InlineCSS = $ReadOnly<{ @@ -352,7 +345,7 @@ function parseNullableStyle( path: NodePath, state: StateManager, evaluatePathFnConfig: FunctionConfig, -): null | StyleObject | InlineCSSTuple | InlineCSSDynamic | 'other' { +): null | StyleObject | 'other' { const node = path.node; if ( t.isNullLiteral(node) || @@ -363,8 +356,9 @@ function parseNullableStyle( const inlineCSS = resolveInlineCSS(path, state); if (inlineCSS != null) { - if (!Array.isArray(inlineCSS) && path.isCallExpression()) { - const { className, classKey, varName, value } = inlineCSS; + if (path.isCallExpression()) { + const dynamicCSS: InlineCSSDynamic = inlineCSS as any; + const { className, classKey, varName, value } = dynamicCSS; const compiledObj = t.objectExpression([ t.objectProperty( t.stringLiteral(classKey), @@ -389,30 +383,10 @@ function parseNullableStyle( path.replaceWith(t.arrayExpression([compiledObj, inlineObj])); return 'other'; } - if (!Array.isArray(inlineCSS) && !path.isCallExpression()) { - // Inline static CSS should be inlined immediately so it survives later bailouts. - path.replaceWith(convertObjectToAST(inlineCSS)); - return inlineCSS; - } - if (Array.isArray(inlineCSS) && path.isCallExpression()) { - // Dynamic inline-css cannot be reduced to plain objects for the build-time - // props optimization. Replace the argument with the compiled tuple so - // runtime stylex.props receives the correct value, then bail out. - const [compiled, inlineVars] = inlineCSS; - path.replaceWith( - t.arrayExpression([ - convertObjectToAST(compiled), - convertObjectToAST(inlineVars), - ]), - ); - return 'other'; - } - // Static inline-css: inline it immediately so it survives any later bailouts. - if (!Array.isArray(inlineCSS)) { - path.replaceWith(convertObjectToAST(inlineCSS)); - return inlineCSS; - } - return inlineCSS; + // Inline static CSS should be inlined immediately so it survives later bailouts. + const staticCSS: StyleObject = inlineCSS as any; + path.replaceWith(convertObjectToAST(staticCSS as any)); + return staticCSS; } if (t.isMemberExpression(node)) { @@ -467,7 +441,7 @@ function parseNullableStyle( function resolveInlineCSS( path: NodePath, state: StateManager, -): null | StyleObject | InlineCSSTuple | InlineCSSDynamic { +): null | StyleObject | InlineCSSDynamic { if (path.isCallExpression()) { const dynamic = getInlineDynamicStyle(path, state); if (dynamic != null) { @@ -537,9 +511,14 @@ function getInlineDynamicStyle( path.node.arguments.length === 1 && isInlineCSSIdentifier(parent, state, path) ) { + const argPath = path.get('arguments')[0]; + if (!argPath || !argPath.node || !argPath.isExpression()) { + return null; + } + const exprArg: t.Expression = argPath.node as any; return { property: valueKey, - value: path.get('arguments')[0].node, + value: exprArg, }; } @@ -552,9 +531,14 @@ function getInlineDynamicStyle( isInlineCSSIdentifier(base, state, path) && path.node.arguments.length === 1 ) { + const argPath = path.get('arguments')[0]; + if (!argPath || !argPath.node || !argPath.isExpression()) { + return null; + } + const exprArg: t.Expression = argPath.node as any; return { property: propName, - value: path.get('arguments')[0].node, + value: exprArg, }; } } @@ -572,7 +556,7 @@ function normalizeInlineValue(value: string | number): string | number { function getPropKey( prop: t.Expression | t.PrivateName | t.Identifier, computed: boolean, -): null | string | number { +): null | string { if (!computed && t.isIdentifier(prop)) { return prop.name; } @@ -580,7 +564,7 @@ function getPropKey( return prop.value; } if (computed && t.isNumericLiteral(prop)) { - return prop.value; + return String(prop.value); } return null; } @@ -653,7 +637,8 @@ function compileInlineStaticCSS( state.registerStyles(listOfStyles, path); state.inlineStylesCache.set(cacheKey, compiled); - return compiled; + // Flow sees FlatCompiledStyles as read-only; treat it as StyleObject for callers. + return compiled as any; } function compileInlineDynamicStyle( @@ -677,7 +662,6 @@ function compileInlineDynamicStyle( const { priority, ...rest } = styleObj; const propertyInjection = { - priority: 0, ltr: `@property ${varName} { syntax: "*"; inherits: false;}`, rtl: null, }; @@ -705,23 +689,25 @@ function compileInlineDynamicStyle( } function applyDevTest( - compiled: { [string]: string | null, $$css: true }, + compiled: FlatCompiledStyles, state: StateManager, -): { [string]: string | null, $$css: true } { +): FlatCompiledStyles { let result = compiled; if (state.isDev && state.options.enableDevClassNames) { - result = injectDevClassNames( + const devStyles: any = injectDevClassNames( { __inline__: result }, null, state, - ).__inline__; + ); + result = devStyles.__inline__; } if (state.isTest) { - result = convertToTestStyles( + const testStyles: any = convertToTestStyles( { __inline__: result }, null, state, - ).__inline__; + ); + result = testStyles.__inline__; } return result; } From 590316244a9bc81ae6beec2572007c1eeb8f75fd Mon Sep 17 00:00:00 2001 From: Melissa Liu Date: Mon, 22 Dec 2025 05:48:58 -0500 Subject: [PATCH 4/8] fix named imports --- .../__tests__/transform-stylex-props-test.js | 39 +++++++++++++--- .../babel-plugin/src/utils/state-manager.js | 5 +- .../babel-plugin/src/visitors/imports.js | 30 +++++++++--- .../babel-plugin/src/visitors/stylex-props.js | 46 +++++++++++++++---- 4 files changed, 95 insertions(+), 25 deletions(-) diff --git a/packages/@stylexjs/babel-plugin/__tests__/transform-stylex-props-test.js b/packages/@stylexjs/babel-plugin/__tests__/transform-stylex-props-test.js index b67bfc770..ca52ea7bc 100644 --- a/packages/@stylexjs/babel-plugin/__tests__/transform-stylex-props-test.js +++ b/packages/@stylexjs/babel-plugin/__tests__/transform-stylex-props-test.js @@ -365,6 +365,27 @@ describe('@stylexjs/babel-plugin', () => { `); }); + test('inline css supports named imports', () => { + const inline = transform(` + import stylex from 'stylex'; + import { color } from '@stylexjs/inline-css'; + stylex.props(color.blue); + `); + expect(inline).toMatchInlineSnapshot(` + "import _inject from "@stylexjs/stylex/lib/stylex-inject"; + var _inject2 = _inject; + import stylex from 'stylex'; + import { color } from '@stylexjs/inline-css'; + _inject2({ + ltr: ".xju2f9n{color:blue}", + priority: 3000 + }); + ({ + className: "xju2f9n" + });" + `); + }); + test('dedupes duplicate properties across create and inline-css', () => { const output = transform(` import stylex from 'stylex'; @@ -556,16 +577,22 @@ describe('@stylexjs/babel-plugin', () => { ltr: "@property --x-opacity { syntax: \\"*\\"; inherits: false;}", priority: 0 }); + const styles = { + opacity: o => [{ + kSiTet: o != null ? "xb4nw82" : o, + $$css: true + }, { + "--x-opacity": o != null ? o : undefined + }] + }; _inject2({ ltr: ".x78zum5{display:flex}", priority: 3000 }); - ({ - className: "x78zum5 xb4nw82", - style: { - "--x-opacity": 0.5 - } - });" + stylex.props({ + k1xSpc: "x78zum5", + $$css: true + }, styles.opacity(0.5));" `); }); diff --git a/packages/@stylexjs/babel-plugin/src/utils/state-manager.js b/packages/@stylexjs/babel-plugin/src/utils/state-manager.js index bb9294362..1ee398610 100644 --- a/packages/@stylexjs/babel-plugin/src/utils/state-manager.js +++ b/packages/@stylexjs/babel-plugin/src/utils/state-manager.js @@ -166,8 +166,9 @@ export default class StateManager { +stylexViewTransitionClassImport: Set = new Set(); +stylexDefaultMarkerImport: Set = new Set(); +stylexWhenImport: Set = new Set(); - +inlineCSSNamespaceImports: Set = new Set(); - +inlineCSSNamedImports: Map = new Map(); + // Map of local identifier -> imported name. + // For namespace/default imports we store '*'. + +inlineCSSImports: Map = new Map(); injectImportInserted: ?t.Identifier = null; diff --git a/packages/@stylexjs/babel-plugin/src/visitors/imports.js b/packages/@stylexjs/babel-plugin/src/visitors/imports.js index 9a5a20271..7f04d3635 100644 --- a/packages/@stylexjs/babel-plugin/src/visitors/imports.js +++ b/packages/@stylexjs/babel-plugin/src/visitors/imports.js @@ -28,9 +28,9 @@ export function readImportDeclarations( if (INLINE_CSS_SOURCES.has(sourcePath)) { for (const specifier of node.specifiers) { if (specifier.type === 'ImportNamespaceSpecifier') { - state.inlineCSSNamespaceImports.add(specifier.local.name); + state.inlineCSSImports.set(specifier.local.name, '*'); } else if (specifier.type === 'ImportDefaultSpecifier') { - state.inlineCSSNamespaceImports.add(specifier.local.name); + state.inlineCSSImports.set(specifier.local.name, '*'); } else if ( specifier.type === 'ImportSpecifier' && (specifier.imported.type === 'Identifier' || @@ -40,7 +40,7 @@ export function readImportDeclarations( specifier.imported.type === 'Identifier' ? specifier.imported.name : specifier.imported.value; - state.inlineCSSNamedImports.set(specifier.local.name, importedName); + state.inlineCSSImports.set(specifier.local.name, importedName); } } return; @@ -150,6 +150,24 @@ export function readRequires( return; } state.importPaths.add(importPath); + if (INLINE_CSS_SOURCES.has(importPath)) { + if (node.id.type === 'Identifier') { + state.inlineCSSImports.set(node.id.name, '*'); + return; + } + if (node.id.type === 'ObjectPattern') { + for (const prop of node.id.properties) { + if ( + prop.type === 'ObjectProperty' && + prop.key.type === 'Identifier' && + prop.value.type === 'Identifier' + ) { + state.inlineCSSImports.set(prop.value.name, prop.key.name); + } + } + return; + } + } if (node.id.type === 'Identifier') { state.stylexImport.add(node.id.name); } @@ -218,7 +236,7 @@ export function readRequires( INLINE_CSS_SOURCES.has(init.arguments[0].value) ) { if (node.id.type === 'Identifier') { - state.inlineCSSNamespaceImports.add(node.id.name); + state.inlineCSSImports.set(node.id.name, '*'); } if (node.id.type === 'ObjectPattern') { for (const prop of node.id.properties) { @@ -227,9 +245,7 @@ export function readRequires( prop.key.type === 'Identifier' && prop.value.type === 'Identifier' ) { - const importedName = prop.key.name; - const localName = prop.value.name; - state.inlineCSSNamedImports.set(localName, importedName); + state.inlineCSSImports.set(prop.value.name, prop.key.name); } } } diff --git a/packages/@stylexjs/babel-plugin/src/visitors/stylex-props.js b/packages/@stylexjs/babel-plugin/src/visitors/stylex-props.js index 17636895d..df04edf5a 100644 --- a/packages/@stylexjs/babel-plugin/src/visitors/stylex-props.js +++ b/packages/@stylexjs/babel-plugin/src/visitors/stylex-props.js @@ -136,7 +136,9 @@ export default function transformStylexProps( ) { const resolved = parseNullableStyle(argPath, state, evaluatePathFnConfig); if (resolved === 'other') { - bailOutIndex = currentIndex; + if (bailOutIndex == null) { + bailOutIndex = currentIndex; + } bailOut = true; } else { resolvedArgs.push(resolved); @@ -158,7 +160,9 @@ export default function transformStylexProps( evaluatePathFnConfig, ); if (primary === 'other' || fallback === 'other') { - bailOutIndex = currentIndex; + if (bailOutIndex == null) { + bailOutIndex = currentIndex; + } bailOut = true; } else { resolvedArgs.push(new ConditionalStyle(test, primary, fallback)); @@ -185,7 +189,9 @@ export default function transformStylexProps( evaluatePathFnConfig, ); if (leftResolved !== 'other' || rightResolved === 'other') { - bailOutIndex = currentIndex; + if (bailOutIndex == null) { + bailOutIndex = currentIndex; + } bailOut = true; } else { resolvedArgs.push( @@ -194,7 +200,9 @@ export default function transformStylexProps( conditional++; } } else { - bailOutIndex = currentIndex; + if (bailOutIndex == null) { + bailOutIndex = currentIndex; + } bailOut = true; } if (conditional > 4) { @@ -389,6 +397,19 @@ function parseNullableStyle( return staticCSS; } + // Local dynamic style functions (e.g., styles.opacity(1)) should bail out + // so runtime props merging keeps the conditional class/inline var semantics. + if (path.isCallExpression()) { + const callee = path.get('callee'); + if (callee.isMemberExpression()) { + const obj = callee.get('object'); + if (obj.isIdentifier() && state.styleMap.has(obj.node.name)) { + return 'other'; + } + } + return 'other'; + } + if (t.isMemberExpression(node)) { const { object, property, computed: computed } = node; let objName = null; @@ -416,8 +437,14 @@ function parseNullableStyle( if (objName != null && propName != null) { const style = state.styleMap.get(objName); if (style != null && style[String(propName)] != null) { + const memberVal = style[String(propName)]; + // Dynamic style functions (arrow/function expressions) should bail out + // so runtime props handling remains intact. + if (typeof memberVal === 'function') { + return 'other'; + } // $FlowFixMe[incompatible-type] - return style[String(propName)]; + return memberVal; } } } @@ -474,7 +501,9 @@ function getInlineStaticCSS( const parent = node.object; if (t.isIdentifier(parent) && isInlineCSSIdentifier(parent, state, path)) { - return { property: valueKey, value: normalizeInlineValue(valueKey) }; + const importedName = state.inlineCSSImports.get(parent.name) ?? 'color'; + const property = importedName === '*' ? valueKey : importedName; + return { property, value: normalizeInlineValue(valueKey) }; } if (t.isMemberExpression(parent)) { @@ -574,10 +603,7 @@ function isInlineCSSIdentifier( state: StateManager, path: NodePath<>, ): boolean { - if ( - state.inlineCSSNamedImports.has(ident.name) || - state.inlineCSSNamespaceImports.has(ident.name) - ) { + if (state.inlineCSSImports.has(ident.name)) { return true; } const binding = path.scope?.getBinding(ident.name); From 74d21bd3482940ea8fe7c65623cf207e1758774f Mon Sep 17 00:00:00 2001 From: Melissa Liu Date: Thu, 15 Jan 2026 02:07:49 -0500 Subject: [PATCH 5/8] new imports --- packages/@stylexjs/babel-plugin/src/visitors/imports.js | 6 ++++++ packages/@stylexjs/stylex/package.json | 1 + packages/@stylexjs/stylex/src/stylex.js | 3 +++ 3 files changed, 10 insertions(+) diff --git a/packages/@stylexjs/babel-plugin/src/visitors/imports.js b/packages/@stylexjs/babel-plugin/src/visitors/imports.js index 7f04d3635..e344315e8 100644 --- a/packages/@stylexjs/babel-plugin/src/visitors/imports.js +++ b/packages/@stylexjs/babel-plugin/src/visitors/imports.js @@ -121,6 +121,9 @@ export function readImportDeclarations( if (importedName === 'defaultMarker') { state.stylexDefaultMarkerImport.add(localName); } + if (importedName === 'css') { + state.inlineCSSImports.set(localName, '*'); + } } } } @@ -221,6 +224,9 @@ export function readRequires( if (prop.key.name === 'defaultMarker') { state.stylexDefaultMarkerImport.add(value.name); } + if (prop.key.name === 'css') { + state.inlineCSSImports.set(value.name, '*'); + } } } } diff --git a/packages/@stylexjs/stylex/package.json b/packages/@stylexjs/stylex/package.json index e8fa7897d..0e8313681 100644 --- a/packages/@stylexjs/stylex/package.json +++ b/packages/@stylexjs/stylex/package.json @@ -37,6 +37,7 @@ "test": "cross-env BABEL_ENV=test jest --coverage" }, "dependencies": { + "@stylexjs/inline-css": "0.17.4", "css-mediaquery": "^0.1.2", "invariant": "^2.2.4", "styleq": "0.2.1" diff --git a/packages/@stylexjs/stylex/src/stylex.js b/packages/@stylexjs/stylex/src/stylex.js index ff5185734..b855c4730 100644 --- a/packages/@stylexjs/stylex/src/stylex.js +++ b/packages/@stylexjs/stylex/src/stylex.js @@ -284,3 +284,6 @@ _legacyMerge.when = when; _legacyMerge.viewTransitionClass = viewTransitionClass; export const legacyMerge: IStyleX = _legacyMerge; + +// Re-export inline-css for cleaner imports +export { default as css } from '@stylexjs/inline-css'; From 67dec13d3e1102182e4b9e6355db6a948b6c2bb2 Mon Sep 17 00:00:00 2001 From: Melissa Liu Date: Sun, 18 Jan 2026 07:49:10 -0500 Subject: [PATCH 6/8] Rename @stylexjs/inline-css to @stylexjs/utility-styles - Rename package from inline-css to utility-styles - Update package.json, README, types, and source files - Update stylex package dependency and re-export --- packages/@stylexjs/stylex/package.json | 2 +- packages/@stylexjs/stylex/src/stylex.js | 4 +-- .../{inline-css => utility-styles}/README.md | 36 +++++++++---------- .../package.json | 9 +++-- .../src/index.d.ts | 10 +++--- .../src/index.js | 8 ++--- 6 files changed, 34 insertions(+), 35 deletions(-) rename packages/@stylexjs/{inline-css => utility-styles}/README.md (63%) rename packages/@stylexjs/{inline-css => utility-styles}/package.json (66%) rename packages/@stylexjs/{inline-css => utility-styles}/src/index.d.ts (61%) rename packages/@stylexjs/{inline-css => utility-styles}/src/index.js (68%) diff --git a/packages/@stylexjs/stylex/package.json b/packages/@stylexjs/stylex/package.json index 0e8313681..9697db767 100644 --- a/packages/@stylexjs/stylex/package.json +++ b/packages/@stylexjs/stylex/package.json @@ -37,7 +37,7 @@ "test": "cross-env BABEL_ENV=test jest --coverage" }, "dependencies": { - "@stylexjs/inline-css": "0.17.4", + "@stylexjs/utility-styles": "0.17.5", "css-mediaquery": "^0.1.2", "invariant": "^2.2.4", "styleq": "0.2.1" diff --git a/packages/@stylexjs/stylex/src/stylex.js b/packages/@stylexjs/stylex/src/stylex.js index b855c4730..6939a3dec 100644 --- a/packages/@stylexjs/stylex/src/stylex.js +++ b/packages/@stylexjs/stylex/src/stylex.js @@ -285,5 +285,5 @@ _legacyMerge.viewTransitionClass = viewTransitionClass; export const legacyMerge: IStyleX = _legacyMerge; -// Re-export inline-css for cleaner imports -export { default as css } from '@stylexjs/inline-css'; +// Re-export utility-styles for cleaner imports +export { default as x } from '@stylexjs/utility-styles'; diff --git a/packages/@stylexjs/inline-css/README.md b/packages/@stylexjs/utility-styles/README.md similarity index 63% rename from packages/@stylexjs/inline-css/README.md rename to packages/@stylexjs/utility-styles/README.md index aa6c925e9..002b3dc23 100644 --- a/packages/@stylexjs/inline-css/README.md +++ b/packages/@stylexjs/utility-styles/README.md @@ -1,30 +1,30 @@ -# @stylexjs/inline-css +# @stylexjs/utility-styles -Compile-time helpers for authoring StyleX styles inline inside stylex.props(). +Compile-time helpers for authoring StyleX utility styles. This package exposes CSS properties as a namespaced object and lets you express static and dynamic styles using normal JavaScript syntax. There is no new runtime styling system and no design tokens — everything compiles to the same output as stylex.create. -The compiler treats inline styles from this package as if they were authored +The compiler treats utility styles from this package as if they were authored locally, enabling the same optimizations as normal StyleX styles. ## Usage ```js import * as stylex from '@stylexjs/stylex'; -import * as css from '@stylexjs/inline-css'; +import x from '@stylexjs/utility-styles'; function Example({ color }) { return (
); @@ -37,8 +37,8 @@ Static styles are expressed via property access and are fully resolved at compile time. ```js -css.display.flex; -css.flexDirection.column; +x.display.flex; +x.flexDirection.column; ``` #### Values starting with numbers @@ -47,8 +47,8 @@ Use a leading underscore for values that begin with a number. The underscore is ignored by the compiler and has no semantic meaning. ```js -css.padding._16px; -css.fontSize._1rem; +x.padding._16px; +x.fontSize._1rem; ``` ### Complex literal values @@ -57,9 +57,9 @@ For values that are not valid JavaScript identifiers (for example, values that contain spaces or symbols), use computed property syntax. ```js -css.fontSize['1.25rem']; -css.width['calc(100% - 20cqi)']; -css.gridTemplateColumns['1fr minmax(0, 3fr)']; +x.fontSize['1.25rem']; +x.width['calc(100% - 20cqi)']; +x.gridTemplateColumns['1fr minmax(0, 3fr)']; ``` ### Dynamic values @@ -67,6 +67,6 @@ css.gridTemplateColumns['1fr minmax(0, 3fr)']; Dynamic styles use call syntax and should be used sparingly. ```js -css.color(color); -css.marginLeft(offset); +x.color(color); +x.marginLeft(offset); ``` diff --git a/packages/@stylexjs/inline-css/package.json b/packages/@stylexjs/utility-styles/package.json similarity index 66% rename from packages/@stylexjs/inline-css/package.json rename to packages/@stylexjs/utility-styles/package.json index 1bb219287..7ec1e7c20 100644 --- a/packages/@stylexjs/inline-css/package.json +++ b/packages/@stylexjs/utility-styles/package.json @@ -1,7 +1,7 @@ { - "name": "@stylexjs/inline-css", - "version": "0.17.4", - "description": "Inline CSS property helpers for StyleX.", + "name": "@stylexjs/utility-styles", + "version": "0.17.5", + "description": "Utility style helpers for StyleX.", "license": "MIT", "main": "src/index.js", "types": "src/index.d.ts", @@ -11,7 +11,7 @@ }, "sideEffects": false, "peerDependencies": { - "@stylexjs/stylex": "^0.17.4" + "@stylexjs/stylex": "^0.17.5" }, "dependencies": { "csstype": "^3.1.3" @@ -20,4 +20,3 @@ "src" ] } - diff --git a/packages/@stylexjs/inline-css/src/index.d.ts b/packages/@stylexjs/utility-styles/src/index.d.ts similarity index 61% rename from packages/@stylexjs/inline-css/src/index.d.ts rename to packages/@stylexjs/utility-styles/src/index.d.ts index 67bd4adad..81fa0c043 100644 --- a/packages/@stylexjs/inline-css/src/index.d.ts +++ b/packages/@stylexjs/utility-styles/src/index.d.ts @@ -1,16 +1,16 @@ import type { StyleXClassNameFor, StyleXStyles } from '@stylexjs/stylex'; import type { Properties } from 'csstype'; -type InlineValue = { +type UtilValue = { [Key in string | number]: StyleXClassNameFor; } & ((value: V) => StyleXStyles); -type InlineCSS = { - [Key in keyof Properties]: InlineValue< +type UtilStyles = { + [Key in keyof Properties]: UtilValue< Properties[Key] >; }; -declare const inlineCSS: InlineCSS; +declare const utilityStyles: UtilStyles; -export = inlineCSS; +export = utilityStyles; diff --git a/packages/@stylexjs/inline-css/src/index.js b/packages/@stylexjs/utility-styles/src/index.js similarity index 68% rename from packages/@stylexjs/inline-css/src/index.js rename to packages/@stylexjs/utility-styles/src/index.js index 89598f6e1..7fe13204b 100644 --- a/packages/@stylexjs/inline-css/src/index.js +++ b/packages/@stylexjs/utility-styles/src/index.js @@ -17,7 +17,7 @@ const valueProxy = (_propName) => }, }); -const inlineCSS = new Proxy(function () {}, { +const utilityStyles = new Proxy(function () {}, { get(_target, prop) { if (typeof prop === 'string') { return valueProxy(prop); @@ -26,10 +26,10 @@ const inlineCSS = new Proxy(function () {}, { }, apply() { throw new Error( - '@stylexjs/inline-css is a compile-time helper. Attempted to call it as a function, but the StyleX compiler did not run.', + '@stylexjs/utility-styles is a compile-time helper. Attempted to call it as a function, but the StyleX compiler did not run.', ); }, }); -module.exports = inlineCSS; -module.exports.default = inlineCSS; +module.exports = utilityStyles; +module.exports.default = utilityStyles; From 0c76f9d0e683a642de5fb48a01026cbb86d5d5da Mon Sep 17 00:00:00 2001 From: Melissa Liu Date: Wed, 21 Jan 2026 04:50:33 -0500 Subject: [PATCH 7/8] Move compilation to babel-plugin, utility-styles outputs raw objects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - utility-styles/babel-transform.js: transforms css.display.flex → { display: 'flex' } - babel-plugin/stylex-props.js: compiles raw objects using styleXCreateSet - No dependency on @stylexjs/shared refactor (uses ../shared) - All 797 tests pass --- .../__tests__/transform-stylex-props-test.js | 148 +++-- packages/@stylexjs/babel-plugin/src/index.js | 10 + .../babel-plugin/src/visitors/stylex-props.js | 577 ++++++++---------- .../@stylexjs/utility-styles/package.json | 4 + .../utility-styles/src/babel-transform.js | 232 +++++++ 5 files changed, 594 insertions(+), 377 deletions(-) create mode 100644 packages/@stylexjs/utility-styles/src/babel-transform.js diff --git a/packages/@stylexjs/babel-plugin/__tests__/transform-stylex-props-test.js b/packages/@stylexjs/babel-plugin/__tests__/transform-stylex-props-test.js index ca52ea7bc..1fbbcb649 100644 --- a/packages/@stylexjs/babel-plugin/__tests__/transform-stylex-props-test.js +++ b/packages/@stylexjs/babel-plugin/__tests__/transform-stylex-props-test.js @@ -231,18 +231,18 @@ describe('@stylexjs/babel-plugin', () => { }); }); - describe('props calls with inline-css', () => { + describe('props calls with utility-styles', () => { test('inline static styles match stylex.create', () => { const inline = transform(` import stylex from 'stylex'; - import * as css from '@stylexjs/inline-css'; + import * as css from '@stylexjs/utility-styles'; stylex.props(css.display.flex); `); expect(inline).toMatchInlineSnapshot(` "import _inject from "@stylexjs/stylex/lib/stylex-inject"; var _inject2 = _inject; import stylex from 'stylex'; - import * as css from '@stylexjs/inline-css'; + import * as css from '@stylexjs/utility-styles'; _inject2({ ltr: ".x78zum5{display:flex}", priority: 3000 @@ -275,14 +275,14 @@ describe('@stylexjs/babel-plugin', () => { test('inline static supports leading underscore value', () => { const inline = transform(` import stylex from 'stylex'; - import * as css from '@stylexjs/inline-css'; + import * as css from '@stylexjs/utility-styles'; stylex.props(css.padding._16px); `); expect(inline).toMatchInlineSnapshot(` "import _inject from "@stylexjs/stylex/lib/stylex-inject"; var _inject2 = _inject; import stylex from 'stylex'; - import * as css from '@stylexjs/inline-css'; + import * as css from '@stylexjs/utility-styles'; _inject2({ ltr: ".x1tamke2{padding:16px}", priority: 1000 @@ -315,14 +315,14 @@ describe('@stylexjs/babel-plugin', () => { test('inline static supports computed key syntax', () => { const inline = transform(` import stylex from 'stylex'; - import * as css from '@stylexjs/inline-css'; + import * as css from '@stylexjs/utility-styles'; stylex.props(css.width['calc(100% - 20px)']); `); expect(inline).toMatchInlineSnapshot(` "import _inject from "@stylexjs/stylex/lib/stylex-inject"; var _inject2 = _inject; import stylex from 'stylex'; - import * as css from '@stylexjs/inline-css'; + import * as css from '@stylexjs/utility-styles'; _inject2({ ltr: ".xnlsq7q{width:calc(100% - 20px)}", priority: 4000 @@ -342,7 +342,7 @@ describe('@stylexjs/babel-plugin', () => { "import _inject from "@stylexjs/stylex/lib/stylex-inject"; var _inject2 = _inject; import stylex from 'stylex'; - import * as css from '@stylexjs/inline-css'; + import * as css from '@stylexjs/utility-styles'; _inject2({ ltr: ".xnlsq7q{width:calc(100% - 20px)}", priority: 4000 @@ -368,14 +368,14 @@ describe('@stylexjs/babel-plugin', () => { test('inline css supports named imports', () => { const inline = transform(` import stylex from 'stylex'; - import { color } from '@stylexjs/inline-css'; + import { color } from '@stylexjs/utility-styles'; stylex.props(color.blue); `); expect(inline).toMatchInlineSnapshot(` "import _inject from "@stylexjs/stylex/lib/stylex-inject"; var _inject2 = _inject; import stylex from 'stylex'; - import { color } from '@stylexjs/inline-css'; + import { color } from '@stylexjs/utility-styles'; _inject2({ ltr: ".xju2f9n{color:blue}", priority: 3000 @@ -386,10 +386,10 @@ describe('@stylexjs/babel-plugin', () => { `); }); - test('dedupes duplicate properties across create and inline-css', () => { + test('dedupes duplicate properties across create and utility-styles', () => { const output = transform(` import stylex from 'stylex'; - import * as css from '@stylexjs/inline-css'; + import * as css from '@stylexjs/utility-styles'; const styles = stylex.create({ base: { color: 'red', backgroundColor: 'white' }, }); @@ -399,7 +399,7 @@ describe('@stylexjs/babel-plugin', () => { "import _inject from "@stylexjs/stylex/lib/stylex-inject"; var _inject2 = _inject; import stylex from 'stylex'; - import * as css from '@stylexjs/inline-css'; + import * as css from '@stylexjs/utility-styles'; _inject2({ ltr: ".x1e2nbdu{color:red}", priority: 3000 @@ -425,7 +425,7 @@ describe('@stylexjs/babel-plugin', () => { test('dynamic style', () => { const inline = transform(` import stylex from 'stylex'; - import * as css from '@stylexjs/inline-css'; + import * as css from '@stylexjs/utility-styles'; stylex.props(css.color(color)); `); const local = transform(` @@ -461,7 +461,7 @@ describe('@stylexjs/babel-plugin', () => { "import _inject from "@stylexjs/stylex/lib/stylex-inject"; var _inject2 = _inject; import stylex from 'stylex'; - import * as css from '@stylexjs/inline-css'; + import * as css from '@stylexjs/utility-styles'; _inject2({ ltr: ".x14rh7hd{color:var(--x-color)}", priority: 3000 @@ -470,31 +470,34 @@ describe('@stylexjs/babel-plugin', () => { ltr: "@property --x-color { syntax: \\"*\\"; inherits: false;}", priority: 0 }); - stylex.props([{ - "color": color != null ? "x14rh7hd" : color, - "$$css": true - }, { - "--x-color": color != null ? color : undefined - }]);" + const _temp = { + color: _v => [{ + "kMwMTN": _v != null ? "x14rh7hd" : _v, + "$$css": true + }, { + "--x-color": _v != null ? _v : undefined + }] + }; + stylex.props(_temp.color(color));" `); }); test('inline static with inline dynamic', () => { const output = transform(` import stylex from 'stylex'; - import * as css from '@stylexjs/inline-css'; + import * as css from '@stylexjs/utility-styles'; stylex.props(css.display.flex, css.color(color)); `); expect(output).toContain('.x78zum5{display:flex}'); expect(output).toContain('.x14rh7hd{color:var(--x-color)}'); expect(output).toContain('--x-color'); - expect(output).toContain('color != null ? "x14rh7hd" : color'); + expect(output).toContain('_v != null ? "x14rh7hd" : _v'); }); test('inline static with create dynamic', () => { const output = transform(` import stylex from 'stylex'; - import * as css from '@stylexjs/inline-css'; + import * as css from '@stylexjs/utility-styles'; const styles = stylex.create({ opacity: (o) => ({ opacity: o }), }); @@ -508,12 +511,51 @@ describe('@stylexjs/babel-plugin', () => { test('inline dynamic with create dynamic', () => { const output = transform(` import stylex from 'stylex'; - import * as css from '@stylexjs/inline-css'; + import * as css from '@stylexjs/utility-styles'; const styles = stylex.create({ opacity: (o) => ({ opacity: o }), }); stylex.props(css.color(color), styles.opacity(0.5)); `); + expect(output).toMatchInlineSnapshot(` + "import _inject from "@stylexjs/stylex/lib/stylex-inject"; + var _inject2 = _inject; + import stylex from 'stylex'; + import * as css from '@stylexjs/utility-styles'; + _inject2({ + ltr: ".xb4nw82{opacity:var(--x-opacity)}", + priority: 3000 + }); + _inject2({ + ltr: "@property --x-opacity { syntax: \\"*\\"; inherits: false;}", + priority: 0 + }); + const styles = { + opacity: o => [{ + kSiTet: o != null ? "xb4nw82" : o, + $$css: true + }, { + "--x-opacity": o != null ? o : undefined + }] + }; + _inject2({ + ltr: ".x14rh7hd{color:var(--x-color)}", + priority: 3000 + }); + _inject2({ + ltr: "@property --x-color { syntax: \\"*\\"; inherits: false;}", + priority: 0 + }); + const _temp = { + color: _v => [{ + "kMwMTN": _v != null ? "x14rh7hd" : _v, + "$$css": true + }, { + "--x-color": _v != null ? _v : undefined + }] + }; + stylex.props(_temp.color(color), styles.opacity(0.5));" + `); expect(output).toContain('.x14rh7hd{color:var(--x-color)}'); expect(output).toContain('.xb4nw82{opacity:var(--x-opacity)}'); expect(output).toContain('--x-color'); @@ -523,14 +565,14 @@ describe('@stylexjs/babel-plugin', () => { test('inline static + inline dynamic coexist', () => { const inline = transform(` import stylex from 'stylex'; - import * as css from '@stylexjs/inline-css'; + import * as css from '@stylexjs/utility-styles'; stylex.props(css.display.flex, css.color(color)); `); expect(inline).toMatchInlineSnapshot(` "import _inject from "@stylexjs/stylex/lib/stylex-inject"; var _inject2 = _inject; import stylex from 'stylex'; - import * as css from '@stylexjs/inline-css'; + import * as css from '@stylexjs/utility-styles'; _inject2({ ltr: ".x78zum5{display:flex}", priority: 3000 @@ -543,22 +585,26 @@ describe('@stylexjs/babel-plugin', () => { ltr: "@property --x-color { syntax: \\"*\\"; inherits: false;}", priority: 0 }); - stylex.props({ + const _temp = { + color: _v => [{ + "kMwMTN": _v != null ? "x14rh7hd" : _v, + "$$css": true + }, { + "--x-color": _v != null ? _v : undefined + }] + }; + const _temp2 = { k1xSpc: "x78zum5", $$css: true - }, [{ - "color": color != null ? "x14rh7hd" : color, - "$$css": true - }, { - "--x-color": color != null ? color : undefined - }]);" + }; + stylex.props(_temp2, _temp.color(color));" `); }); test('inline static + create dynamic', () => { const output = transform(` import stylex from 'stylex'; - import * as css from '@stylexjs/inline-css'; + import * as css from '@stylexjs/utility-styles'; const styles = stylex.create({ opacity: (o) => ({ opacity: o }), }); @@ -568,7 +614,7 @@ describe('@stylexjs/babel-plugin', () => { "import _inject from "@stylexjs/stylex/lib/stylex-inject"; var _inject2 = _inject; import stylex from 'stylex'; - import * as css from '@stylexjs/inline-css'; + import * as css from '@stylexjs/utility-styles'; _inject2({ ltr: ".xb4nw82{opacity:var(--x-opacity)}", priority: 3000 @@ -589,17 +635,18 @@ describe('@stylexjs/babel-plugin', () => { ltr: ".x78zum5{display:flex}", priority: 3000 }); - stylex.props({ + const _temp = { k1xSpc: "x78zum5", $$css: true - }, styles.opacity(0.5));" + }; + stylex.props(_temp, styles.opacity(0.5));" `); }); test('inline dynamic + create dynamic', () => { const output = transform(` import stylex from 'stylex'; - import * as css from '@stylexjs/inline-css'; + import * as css from '@stylexjs/utility-styles'; const styles = stylex.create({ opacity: (o) => ({ opacity: o }), }); @@ -609,7 +656,7 @@ describe('@stylexjs/babel-plugin', () => { "import _inject from "@stylexjs/stylex/lib/stylex-inject"; var _inject2 = _inject; import stylex from 'stylex'; - import * as css from '@stylexjs/inline-css'; + import * as css from '@stylexjs/utility-styles'; _inject2({ ltr: ".xb4nw82{opacity:var(--x-opacity)}", priority: 3000 @@ -634,21 +681,24 @@ describe('@stylexjs/babel-plugin', () => { ltr: "@property --x-color { syntax: \\"*\\"; inherits: false;}", priority: 0 }); - stylex.props([{ - "color": color != null ? "x14rh7hd" : color, - "$$css": true - }, { - "--x-color": color != null ? color : undefined - }], styles.opacity(0.5));" + const _temp = { + color: _v => [{ + "kMwMTN": _v != null ? "x14rh7hd" : _v, + "$$css": true + }, { + "--x-color": _v != null ? _v : undefined + }] + }; + stylex.props(_temp.color(color), styles.opacity(0.5));" `); }); describe('with options', () => { - test('dev/debug classnames for inline-css', () => { + test('dev/debug classnames for utility-styles', () => { const inline = transform( ` import stylex from 'stylex'; - import * as css from '@stylexjs/inline-css'; + import * as css from '@stylexjs/utility-styles'; stylex.props(css.display.flex); `, { @@ -663,7 +713,7 @@ describe('@stylexjs/babel-plugin', () => { "import _inject from "@stylexjs/stylex/lib/stylex-inject"; var _inject2 = _inject; import stylex from 'stylex'; - import * as css from '@stylexjs/inline-css'; + import * as css from '@stylexjs/utility-styles'; _inject2({ ltr: ".display-x78zum5{display:flex}", priority: 3000 diff --git a/packages/@stylexjs/babel-plugin/src/index.js b/packages/@stylexjs/babel-plugin/src/index.js index 7b4648a01..84fc9c8ce 100644 --- a/packages/@stylexjs/babel-plugin/src/index.js +++ b/packages/@stylexjs/babel-plugin/src/index.js @@ -38,6 +38,8 @@ import { LOGICAL_FLOAT_END_VAR, } from './shared/preprocess-rules/legacy-expand-shorthands'; import transformStyleXDefineMarker from './visitors/stylex-define-marker'; +import { createUtilityStylesVisitor } from '@stylexjs/utility-styles/babel-transform'; +import { convertObjectToAST } from './utils/js-to-ast'; const NAME = 'stylex'; @@ -160,6 +162,14 @@ function styleXTransform(): PluginObj<> { skipStylexPropsChildren(path, state); }, }); + // Run utility-styles visitor first to transform x.prop.value patterns + // This runs BEFORE stylex.props so that utility styles are already + // compiled when stylex.props processes them + const utilityStylesVisitor = createUtilityStylesVisitor(state, { + convertObjectToAST, + }); + path.traverse(utilityStylesVisitor); + path.traverse({ CallExpression(path: NodePath) { transformStylexCall(path, state); diff --git a/packages/@stylexjs/babel-plugin/src/visitors/stylex-props.js b/packages/@stylexjs/babel-plugin/src/visitors/stylex-props.js index df04edf5a..baea183f3 100644 --- a/packages/@stylexjs/babel-plugin/src/visitors/stylex-props.js +++ b/packages/@stylexjs/babel-plugin/src/visitors/stylex-props.js @@ -9,7 +9,6 @@ import type { NodePath } from '@babel/traverse'; import type { FunctionConfig } from '../utils/evaluate-path'; -import type { FlatCompiledStyles } from '../shared/common-types'; import * as t from '@babel/types'; import StateManager from '../utils/state-manager'; @@ -18,13 +17,8 @@ import { convertObjectToAST } from '../utils/js-to-ast'; import { evaluate } from '../utils/evaluate-path'; import stylexDefaultMarker from '../shared/stylex-defaultMarker'; import styleXCreateSet from '../shared/stylex-create'; -import { convertStyleToClassName } from '../shared/utils/convert-to-className'; -import { - injectDevClassNames, - convertToTestStyles, -} from '../utils/dev-classname'; - -const INLINE_CSS_SOURCE = '@stylexjs/inline-css'; +import { hoistExpression } from '../utils/ast-helpers'; +import { injectDevClassNames } from '../utils/dev-classname'; type ClassNameValue = string | null | boolean | NonStringClassNameValue; type NonStringClassNameValue = [t.Expression, ClassNameValue, ClassNameValue]; @@ -52,18 +46,6 @@ class ConditionalStyle { type ResolvedArg = ?StyleObject | ConditionalStyle; type ResolvedArgs = Array; -type InlineCSS = $ReadOnly<{ - property: string, - value: string | number, -}>; - -type InlineCSSDynamic = $ReadOnly<{ - className: string, - classKey: string, - varName: string, - value: t.Expression, -}>; - export function skipStylexPropsChildren( path: NodePath, state: StateManager, @@ -124,6 +106,12 @@ export default function transformStylexProps( disableImports: true, }; + // Pre-process: compile any raw style objects (from utility-styles) before the main loop + // This transforms { property: 'value' } into { propKey: 'className', $$css: true } + for (const argPath of argsPath) { + compileRawStyleObjects(argPath, state, evaluatePathFnConfig); + } + const resolvedArgs: ResolvedArgs = []; for (const argPath of argsPath) { currentIndex++; @@ -294,6 +282,32 @@ export default function transformStylexProps( MemberExpression, }); } + + // Hoist inline CSS objects (from utility-styles) to module level + // Raw objects are pre-compiled in compileRawStyleObjects, so this only hoists compiled objects + // eslint-disable-next-line no-inner-declarations + function ObjectExpression(objPath: NodePath) { + // Check if this object has $$css: true (compiled utility style object) + const hasCssMarker = objPath.node.properties.some( + (prop) => + t.isObjectProperty(prop) && + ((t.isIdentifier(prop.key) && prop.key.name === '$$css') || + (t.isStringLiteral(prop.key) && prop.key.value === '$$css')) && + t.isBooleanLiteral(prop.value, { value: true }), + ); + if (hasCssMarker) { + const hoisted = hoistExpression(objPath, objPath.node); + objPath.replaceWith(hoisted); + } + } + + if (argPath.isObjectExpression()) { + ObjectExpression(argPath); + } else { + argPath.traverse({ + ObjectExpression, + }); + } } } else { path.skip(); @@ -362,41 +376,6 @@ function parseNullableStyle( return null; } - const inlineCSS = resolveInlineCSS(path, state); - if (inlineCSS != null) { - if (path.isCallExpression()) { - const dynamicCSS: InlineCSSDynamic = inlineCSS as any; - const { className, classKey, varName, value } = dynamicCSS; - const compiledObj = t.objectExpression([ - t.objectProperty( - t.stringLiteral(classKey), - t.conditionalExpression( - t.binaryExpression('!=', value, t.nullLiteral()), - t.stringLiteral(className), - value, - ), - ), - t.objectProperty(t.stringLiteral('$$css'), t.booleanLiteral(true)), - ]); - const inlineObj = t.objectExpression([ - t.objectProperty( - t.stringLiteral(varName), - t.conditionalExpression( - t.binaryExpression('!=', value, t.nullLiteral()), - value, - t.identifier('undefined'), - ), - ), - ]); - path.replaceWith(t.arrayExpression([compiledObj, inlineObj])); - return 'other'; - } - // Inline static CSS should be inlined immediately so it survives later bailouts. - const staticCSS: StyleObject = inlineCSS as any; - path.replaceWith(convertObjectToAST(staticCSS as any)); - return staticCSS; - } - // Local dynamic style functions (e.g., styles.opacity(1)) should bail out // so runtime props merging keeps the conditional class/inline var semantics. if (path.isCallExpression()) { @@ -465,279 +444,6 @@ function parseNullableStyle( return 'other'; } -function resolveInlineCSS( - path: NodePath, - state: StateManager, -): null | StyleObject | InlineCSSDynamic { - if (path.isCallExpression()) { - const dynamic = getInlineDynamicStyle(path, state); - if (dynamic != null) { - return compileInlineDynamicStyle(dynamic, state, path); - } - } - - const inlineStyle = getInlineStaticCSS(path, state); - if (inlineStyle != null) { - return compileInlineStaticCSS(inlineStyle, state, path); - } - - return null; -} - -function getInlineStaticCSS( - path: NodePath, - state: StateManager, -): null | InlineCSS { - const node = path.node; - if (!t.isMemberExpression(node)) { - return null; - } - - const valueKey = getPropKey(node.property, node.computed); - if (valueKey == null) { - return null; - } - - const parent = node.object; - - if (t.isIdentifier(parent) && isInlineCSSIdentifier(parent, state, path)) { - const importedName = state.inlineCSSImports.get(parent.name) ?? 'color'; - const property = importedName === '*' ? valueKey : importedName; - return { property, value: normalizeInlineValue(valueKey) }; - } - - if (t.isMemberExpression(parent)) { - const propName = getPropKey(parent.property, parent.computed); - const base = parent.object; - if ( - propName != null && - t.isIdentifier(base) && - isInlineCSSIdentifier(base, state, path) - ) { - return { property: propName, value: normalizeInlineValue(valueKey) }; - } - } - - return null; -} - -function getInlineDynamicStyle( - path: NodePath, - state: StateManager, -): null | { property: string, value: t.Expression } { - const callee = path.get('callee'); - if (!callee.isMemberExpression()) { - return null; - } - const valueKey = getPropKey(callee.node.property, callee.node.computed); - if (valueKey == null) { - return null; - } - const parent = callee.node.object; - - if ( - t.isIdentifier(parent) && - path.node.arguments.length === 1 && - isInlineCSSIdentifier(parent, state, path) - ) { - const argPath = path.get('arguments')[0]; - if (!argPath || !argPath.node || !argPath.isExpression()) { - return null; - } - const exprArg: t.Expression = argPath.node as any; - return { - property: valueKey, - value: exprArg, - }; - } - - if (t.isMemberExpression(parent)) { - const propName = getPropKey(parent.property, parent.computed); - const base = parent.object; - if ( - propName != null && - t.isIdentifier(base) && - isInlineCSSIdentifier(base, state, path) && - path.node.arguments.length === 1 - ) { - const argPath = path.get('arguments')[0]; - if (!argPath || !argPath.node || !argPath.isExpression()) { - return null; - } - const exprArg: t.Expression = argPath.node as any; - return { - property: propName, - value: exprArg, - }; - } - } - - return null; -} - -function normalizeInlineValue(value: string | number): string | number { - if (typeof value === 'string' && value.startsWith('_')) { - return value.slice(1); - } - return value; -} - -function getPropKey( - prop: t.Expression | t.PrivateName | t.Identifier, - computed: boolean, -): null | string { - if (!computed && t.isIdentifier(prop)) { - return prop.name; - } - if (computed && t.isStringLiteral(prop)) { - return prop.value; - } - if (computed && t.isNumericLiteral(prop)) { - return String(prop.value); - } - return null; -} - -function isInlineCSSIdentifier( - ident: t.Identifier, - state: StateManager, - path: NodePath<>, -): boolean { - if (state.inlineCSSImports.has(ident.name)) { - return true; - } - const binding = path.scope?.getBinding(ident.name); - if ( - binding && - binding.path.isImportSpecifier() && - binding.path.parent.type === 'ImportDeclaration' && - binding.path.parent.source.value === INLINE_CSS_SOURCE - ) { - return true; - } - if ( - binding && - binding.path.isImportNamespaceSpecifier() && - binding.path.parent.type === 'ImportDeclaration' && - binding.path.parent.source.value === INLINE_CSS_SOURCE - ) { - return true; - } - if ( - binding && - binding.path.isImportDefaultSpecifier() && - binding.path.parent.type === 'ImportDeclaration' && - binding.path.parent.source.value === INLINE_CSS_SOURCE - ) { - return true; - } - return false; -} - -function compileInlineStaticCSS( - inlineStyle: InlineCSS, - state: StateManager, - path: NodePath, -): StyleObject { - const { property, value } = inlineStyle; - const cacheKey = `${property}|${typeof value}:${String(value)}`; - const cached = state.inlineStylesCache.get(cacheKey); - if (cached != null) { - return cached; - } - - const [compiledNamespaces, injectedStyles] = styleXCreateSet( - { - __inline__: { - [property]: value, - }, - }, - state.options, - ); - - const compiled = applyDevTest(compiledNamespaces.__inline__, state); - - const listOfStyles = Object.entries(injectedStyles).map( - ([key, { priority, ...rest }]) => [key, rest, priority], - ); - state.registerStyles(listOfStyles, path); - - state.inlineStylesCache.set(cacheKey, compiled); - // Flow sees FlatCompiledStyles as read-only; treat it as StyleObject for callers. - return compiled as any; -} - -function compileInlineDynamicStyle( - inlineStyle: { property: string, value: t.Expression }, - state: StateManager, - path: NodePath, -): InlineCSSDynamic { - const { property, value } = inlineStyle; - const cacheKey = property; - let cached = state.inlineDynamicCache.get(cacheKey); - - if (cached == null) { - const varName = `--x-${property}`; - const [classKey, className, styleObj] = convertStyleToClassName( - [property, `var(${varName})`], - [], - [], - [], - state.options, - ); - const { priority, ...rest } = styleObj; - - const propertyInjection = { - ltr: `@property ${varName} { syntax: "*"; inherits: false;}`, - rtl: null, - }; - - state.registerStyles( - [ - [classKey, rest, priority], - [`${classKey}-var`, propertyInjection, 0], - ], - path, - ); - - cached = { className, varName, classKey }; - state.inlineDynamicCache.set(cacheKey, cached); - } - - const { className, varName, classKey } = cached; - - return { - className, - classKey, - varName, - value, - }; -} - -function applyDevTest( - compiled: FlatCompiledStyles, - state: StateManager, -): FlatCompiledStyles { - let result = compiled; - if (state.isDev && state.options.enableDevClassNames) { - const devStyles: any = injectDevClassNames( - { __inline__: result }, - null, - state, - ); - result = devStyles.__inline__; - } - if (state.isTest) { - const testStyles: any = convertToTestStyles( - { __inline__: result }, - null, - state, - ); - result = testStyles.__inline__; - } - return result; -} - function makeStringExpression(values: ResolvedArgs): t.Expression { const conditions = values .filter( @@ -837,3 +543,218 @@ function isCalleeMemberExpression( state.stylexImport.has(node.callee.object.name) ); } + +/** + * Pre-compile raw style objects from utility-styles. + * This transforms { property: 'value' } into { propKey: 'className', $$css: true } + * and registers the CSS injection. + */ +function compileRawStyleObjects( + argPath: NodePath<>, + state: StateManager, + evaluatePathFnConfig: FunctionConfig, +): void { + function processObjectExpression(objPath: NodePath) { + // Check if this object already has $$css marker (already compiled) + const hasCssMarker = objPath.node.properties.some( + (prop) => + t.isObjectProperty(prop) && + ((t.isIdentifier(prop.key) && prop.key.name === '$$css') || + (t.isStringLiteral(prop.key) && prop.key.value === '$$css')), + ); + if (hasCssMarker) { + return; + } + + // Empty objects don't need processing + const properties = objPath.node.properties; + if (properties.length === 0) { + return; + } + + // Try to evaluate the object + const evalResult = evaluate(objPath, state, evaluatePathFnConfig); + + // Check if the object has dynamic values (non-evaluatable) + if (!evalResult.confident) { + // Check if this is a simple {property: dynamicValue} pattern + if (properties.length === 1 && t.isObjectProperty(properties[0])) { + const prop = properties[0]; + const key = t.isIdentifier(prop.key) && !prop.computed + ? prop.key.name + : t.isStringLiteral(prop.key) + ? prop.key.value + : null; + + if (key != null && t.isExpression(prop.value) && !t.isLiteral(prop.value)) { + // This is a dynamic style: { property: dynamicValue } + compileDynamicStyle(objPath, key, prop.value, state); + return; + } + } + return; + } + + if (evalResult.value == null) { + return; + } + + // Skip proxies + if (evalResult.value.__IS_PROXY === true) { + return; + } + + // This is a raw style object - compile it + const rawStyle = evalResult.value; + + // Create a namespace wrapper for styleXCreateSet: { __inline__: rawStyle } + const styleInput = { __inline__: rawStyle }; + + // eslint-disable-next-line prefer-const + let [compiledStyles, injectedStyles] = styleXCreateSet( + styleInput, + state.options, + ); + + // Inject dev class names if enabled + if (state.isDev && state.opts.enableDevClassNames) { + compiledStyles = { + ...injectDevClassNames(compiledStyles, null, state), + }; + } + + // Register the CSS + const listOfStyles = Object.entries(injectedStyles).map( + ([key, { priority, ...rest }]) => [key, rest, priority], + ); + state.registerStyles(listOfStyles, objPath); + + // Get the compiled style for __inline__ namespace + const compiledStyle = compiledStyles.__inline__; + if (compiledStyle == null) { + return; + } + + // Convert to AST and replace + const compiledAst = convertObjectToAST(compiledStyle); + objPath.replaceWith(compiledAst); + } + + if (argPath.isObjectExpression()) { + processObjectExpression(argPath); + } else if (argPath.isConditionalExpression()) { + const consequentPath = argPath.get('consequent'); + const alternatePath = argPath.get('alternate'); + if (consequentPath.isObjectExpression()) { + processObjectExpression(consequentPath); + } + if (alternatePath.isObjectExpression()) { + processObjectExpression(alternatePath); + } + } else if (argPath.isLogicalExpression()) { + const rightPath = argPath.get('right'); + if (rightPath.isObjectExpression()) { + processObjectExpression(rightPath); + } + } +} + +/** + * Compile a dynamic style { property: dynamicValue } into a hoisted function pattern. + * This creates: const _styles = { property: _v => [compiledStyle, inlineVars] } + * And replaces the original with: _styles.property(dynamicValue) + */ +function compileDynamicStyle( + objPath: NodePath, + property: string, + valueExpr: t.Expression, + state: StateManager, +): void { + // Use a CSS variable for the dynamic value + const varName = `--x-${property}`; + const rawStyle = { [property]: `var(${varName})` }; + + // Create a namespace wrapper for styleXCreateSet + const styleInput = { __inline__: rawStyle }; + + // eslint-disable-next-line prefer-const + let [compiledStyles, injectedStyles] = styleXCreateSet( + styleInput, + state.options, + ); + + // Inject dev class names if enabled + if (state.isDev && state.opts.enableDevClassNames) { + compiledStyles = { + ...injectDevClassNames(compiledStyles, null, state), + }; + } + + // Add @property rule for the CSS variable + injectedStyles[varName] = { + priority: 0, + ltr: `@property ${varName} { syntax: "*"; inherits: false;}`, + rtl: null, + }; + + // Register the CSS + const listOfStyles = Object.entries(injectedStyles).map( + ([key, { priority, ...rest }]) => [key, rest, priority], + ); + state.registerStyles(listOfStyles, objPath); + + // Get the compiled style + const compiledStyle = compiledStyles.__inline__; + if (compiledStyle == null) { + return; + } + + // Get the class name for the property + const propKey = Object.keys(compiledStyle).find((k) => k !== '$$css'); + const className = propKey != null ? compiledStyle[propKey] : null; + if (className == null || propKey == null) { + return; + } + + // Create the function pattern: + // _v => [{ propKey: _v != null ? className : _v, $$css: true }, { varName: _v != null ? _v : undefined }] + const param = t.identifier('_v'); + const nullCheck = t.binaryExpression('!=', param, t.nullLiteral()); + + const compiledObj = t.objectExpression([ + t.objectProperty( + t.stringLiteral(propKey), + t.conditionalExpression(nullCheck, t.stringLiteral(className), param), + ), + t.objectProperty(t.stringLiteral('$$css'), t.booleanLiteral(true)), + ]); + + const inlineVarsObj = t.objectExpression([ + t.objectProperty( + t.stringLiteral(varName), + t.conditionalExpression( + t.binaryExpression('!=', param, t.nullLiteral()), + param, + t.identifier('undefined'), + ), + ), + ]); + + const fnBody = t.arrayExpression([compiledObj, inlineVarsObj]); + const arrowFn = t.arrowFunctionExpression([param], fnBody); + + // Create the hoisted styles object: const _styles = { property: arrowFn } + const stylesObj = t.objectExpression([ + t.objectProperty(t.identifier(property), arrowFn), + ]); + + // Hoist the function + const hoistedId = hoistExpression(objPath, stylesObj); + + // Replace the original with: _hoistedId.property(valueExpr) + const callExpr = t.callExpression( + t.memberExpression(hoistedId, t.identifier(property)), + [valueExpr], + ); + objPath.replaceWith(callExpr); +} diff --git a/packages/@stylexjs/utility-styles/package.json b/packages/@stylexjs/utility-styles/package.json index 7ec1e7c20..94668b5a7 100644 --- a/packages/@stylexjs/utility-styles/package.json +++ b/packages/@stylexjs/utility-styles/package.json @@ -5,6 +5,10 @@ "license": "MIT", "main": "src/index.js", "types": "src/index.d.ts", + "exports": { + ".": "./src/index.js", + "./babel-transform": "./src/babel-transform.js" + }, "repository": { "type": "git", "url": "git+https://github.com/facebook/stylex.git" diff --git a/packages/@stylexjs/utility-styles/src/babel-transform.js b/packages/@stylexjs/utility-styles/src/babel-transform.js new file mode 100644 index 000000000..24cc996de --- /dev/null +++ b/packages/@stylexjs/utility-styles/src/babel-transform.js @@ -0,0 +1,232 @@ +/** + * 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. + */ + +'use strict'; + +const t = require('@babel/types'); + +const UTILITY_STYLES_SOURCE = '@stylexjs/utility-styles'; + +/** + * Creates a visitor that transforms utility style expressions into raw style objects. + * The babel-plugin will handle compilation via stylexCreate. + * + * - css.display.flex -> { display: 'flex' } + * - css.color(myVar) -> { color: myVar } + * + * @param {object} state - StateManager from babel-plugin + * @returns {object} Babel visitor + */ +function createUtilityStylesVisitor(state) { + return { + MemberExpression(path) { + if (path.node._utilityStyleProcessed) { + return; + } + + const staticStyle = getStaticStyleFromPath(path, state); + if (staticStyle != null) { + path.node._utilityStyleProcessed = true; + transformStaticStyle(path, staticStyle); + } + }, + + CallExpression(path) { + if (path.node._utilityStyleProcessed) { + return; + } + + const dynamicStyle = getDynamicStyleFromPath(path, state); + if (dynamicStyle != null) { + path.node._utilityStyleProcessed = true; + transformDynamicStyle(path, dynamicStyle); + } + }, + }; +} + +function isUtilityStylesIdentifier(ident, state, path) { + if (state.inlineCSSImports && state.inlineCSSImports.has(ident.name)) { + return true; + } + + const binding = path.scope?.getBinding(ident.name); + if (!binding) { + return false; + } + + if ( + binding.path.isImportSpecifier() && + binding.path.parent.type === 'ImportDeclaration' && + binding.path.parent.source.value === UTILITY_STYLES_SOURCE + ) { + return true; + } + + if ( + binding.path.isImportNamespaceSpecifier() && + binding.path.parent.type === 'ImportDeclaration' && + binding.path.parent.source.value === UTILITY_STYLES_SOURCE + ) { + return true; + } + + if ( + binding.path.isImportDefaultSpecifier() && + binding.path.parent.type === 'ImportDeclaration' && + binding.path.parent.source.value === UTILITY_STYLES_SOURCE + ) { + return true; + } + + return false; +} + +function getPropKey(prop, computed) { + if (!computed && t.isIdentifier(prop)) { + return prop.name; + } + if (computed && t.isStringLiteral(prop)) { + return prop.value; + } + if (computed && t.isNumericLiteral(prop)) { + return String(prop.value); + } + return null; +} + +function normalizeValue(value) { + if (typeof value === 'string' && value.startsWith('_')) { + return value.slice(1); + } + return value; +} + +function getStaticStyleFromPath(path, state) { + const node = path.node; + if (!t.isMemberExpression(node)) { + return null; + } + + if (path.parentPath?.isCallExpression() && path.parentPath.node.callee === node) { + return null; + } + + const valueKey = getPropKey(node.property, node.computed); + if (valueKey == null) { + return null; + } + + const parent = node.object; + + if (t.isMemberExpression(parent)) { + const propName = getPropKey(parent.property, parent.computed); + const base = parent.object; + if ( + propName != null && + t.isIdentifier(base) && + isUtilityStylesIdentifier(base, state, path) + ) { + return { property: propName, value: normalizeValue(valueKey) }; + } + } + + if (t.isIdentifier(parent) && isUtilityStylesIdentifier(parent, state, path)) { + const importedName = state.inlineCSSImports?.get(parent.name) ?? 'color'; + const property = importedName === '*' ? valueKey : importedName; + return { property, value: normalizeValue(valueKey) }; + } + + return null; +} + +function getDynamicStyleFromPath(path, state) { + const callee = path.get('callee'); + if (!callee.isMemberExpression()) { + return null; + } + + const valueKey = getPropKey(callee.node.property, callee.node.computed); + if (valueKey == null) { + return null; + } + + if (path.node.arguments.length !== 1) { + return null; + } + + const argPath = path.get('arguments')[0]; + if (!argPath || !argPath.node || !argPath.isExpression()) { + return null; + } + + const parent = callee.node.object; + + if (t.isIdentifier(parent) && isUtilityStylesIdentifier(parent, state, path)) { + return { + property: valueKey, + value: argPath.node, + }; + } + + if (t.isMemberExpression(parent)) { + const propName = getPropKey(parent.property, parent.computed); + const base = parent.object; + if ( + propName != null && + t.isIdentifier(base) && + isUtilityStylesIdentifier(base, state, path) + ) { + return { + property: propName, + value: argPath.node, + }; + } + } + + return null; +} + +/** + * Transform static utility style to raw style object + * css.display.flex -> { display: 'flex' } + */ +function transformStaticStyle(path, styleInfo) { + const { property, value } = styleInfo; + + const styleObj = t.objectExpression([ + t.objectProperty( + t.stringLiteral(property), + typeof value === 'number' + ? t.numericLiteral(value) + : t.stringLiteral(String(value)), + ), + ]); + + path.replaceWith(styleObj); +} + +/** + * Transform dynamic utility style to raw style object + * css.color(myVar) -> { color: myVar } + */ +function transformDynamicStyle(path, styleInfo) { + const { property, value } = styleInfo; + + const styleObj = t.objectExpression([ + t.objectProperty(t.stringLiteral(property), value), + ]); + + path.replaceWith(styleObj); +} + +module.exports = { + createUtilityStylesVisitor, + isUtilityStylesIdentifier, + getStaticStyleFromPath, + getDynamicStyleFromPath, +}; From fb5a7ae363df0e30c636517b1fa71af95382f162 Mon Sep 17 00:00:00 2001 From: Melissa Liu Date: Wed, 21 Jan 2026 05:13:45 -0500 Subject: [PATCH 8/8] rename utility-styles to atoms (@stylexjs/atoms) - Rename package from @stylexjs/utility-styles to @stylexjs/atoms - Update all imports and references in babel-plugin - Update stylex package dependency and re-export - Fix type definitions to be compatible with stylex.props() - Update test descriptions and imports --- .../{utility-styles => atoms}/README.md | 8 +-- .../{utility-styles => atoms}/package.json | 4 +- .../src/babel-transform.js | 8 +-- packages/@stylexjs/atoms/src/index.d.ts | 44 +++++++++++++++ .../{utility-styles => atoms}/src/index.js | 2 +- .../__tests__/transform-stylex-props-test.js | 56 +++++++++---------- packages/@stylexjs/babel-plugin/src/index.js | 10 ++-- .../babel-plugin/src/visitors/stylex-props.js | 6 +- packages/@stylexjs/stylex/package.json | 2 +- packages/@stylexjs/stylex/src/stylex.js | 4 +- .../@stylexjs/utility-styles/src/index.d.ts | 16 ------ 11 files changed, 94 insertions(+), 66 deletions(-) rename packages/@stylexjs/{utility-styles => atoms}/README.md (87%) rename packages/@stylexjs/{utility-styles => atoms}/package.json (83%) rename packages/@stylexjs/{utility-styles => atoms}/src/babel-transform.js (95%) create mode 100644 packages/@stylexjs/atoms/src/index.d.ts rename packages/@stylexjs/{utility-styles => atoms}/src/index.js (83%) delete mode 100644 packages/@stylexjs/utility-styles/src/index.d.ts diff --git a/packages/@stylexjs/utility-styles/README.md b/packages/@stylexjs/atoms/README.md similarity index 87% rename from packages/@stylexjs/utility-styles/README.md rename to packages/@stylexjs/atoms/README.md index 002b3dc23..f95651266 100644 --- a/packages/@stylexjs/utility-styles/README.md +++ b/packages/@stylexjs/atoms/README.md @@ -1,20 +1,20 @@ -# @stylexjs/utility-styles +# @stylexjs/atoms -Compile-time helpers for authoring StyleX utility styles. +Compile-time helpers for authoring StyleX atomic styles. This package exposes CSS properties as a namespaced object and lets you express static and dynamic styles using normal JavaScript syntax. There is no new runtime styling system and no design tokens — everything compiles to the same output as stylex.create. -The compiler treats utility styles from this package as if they were authored +The compiler treats atomic styles from this package as if they were authored locally, enabling the same optimizations as normal StyleX styles. ## Usage ```js import * as stylex from '@stylexjs/stylex'; -import x from '@stylexjs/utility-styles'; +import x from '@stylexjs/atoms'; function Example({ color }) { return ( diff --git a/packages/@stylexjs/utility-styles/package.json b/packages/@stylexjs/atoms/package.json similarity index 83% rename from packages/@stylexjs/utility-styles/package.json rename to packages/@stylexjs/atoms/package.json index 94668b5a7..708d71b27 100644 --- a/packages/@stylexjs/utility-styles/package.json +++ b/packages/@stylexjs/atoms/package.json @@ -1,7 +1,7 @@ { - "name": "@stylexjs/utility-styles", + "name": "@stylexjs/atoms", "version": "0.17.5", - "description": "Utility style helpers for StyleX.", + "description": "Atomic style helpers for StyleX.", "license": "MIT", "main": "src/index.js", "types": "src/index.d.ts", diff --git a/packages/@stylexjs/utility-styles/src/babel-transform.js b/packages/@stylexjs/atoms/src/babel-transform.js similarity index 95% rename from packages/@stylexjs/utility-styles/src/babel-transform.js rename to packages/@stylexjs/atoms/src/babel-transform.js index 24cc996de..1b67584f0 100644 --- a/packages/@stylexjs/utility-styles/src/babel-transform.js +++ b/packages/@stylexjs/atoms/src/babel-transform.js @@ -9,7 +9,7 @@ const t = require('@babel/types'); -const UTILITY_STYLES_SOURCE = '@stylexjs/utility-styles'; +const ATOMS_SOURCE = '@stylexjs/atoms'; /** * Creates a visitor that transforms utility style expressions into raw style objects. @@ -62,7 +62,7 @@ function isUtilityStylesIdentifier(ident, state, path) { if ( binding.path.isImportSpecifier() && binding.path.parent.type === 'ImportDeclaration' && - binding.path.parent.source.value === UTILITY_STYLES_SOURCE + binding.path.parent.source.value === ATOMS_SOURCE ) { return true; } @@ -70,7 +70,7 @@ function isUtilityStylesIdentifier(ident, state, path) { if ( binding.path.isImportNamespaceSpecifier() && binding.path.parent.type === 'ImportDeclaration' && - binding.path.parent.source.value === UTILITY_STYLES_SOURCE + binding.path.parent.source.value === ATOMS_SOURCE ) { return true; } @@ -78,7 +78,7 @@ function isUtilityStylesIdentifier(ident, state, path) { if ( binding.path.isImportDefaultSpecifier() && binding.path.parent.type === 'ImportDeclaration' && - binding.path.parent.source.value === UTILITY_STYLES_SOURCE + binding.path.parent.source.value === ATOMS_SOURCE ) { return true; } diff --git a/packages/@stylexjs/atoms/src/index.d.ts b/packages/@stylexjs/atoms/src/index.d.ts new file mode 100644 index 000000000..9b0bfe2b2 --- /dev/null +++ b/packages/@stylexjs/atoms/src/index.d.ts @@ -0,0 +1,44 @@ +/** + * 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. + */ + +import type { StyleXStyles } from '@stylexjs/stylex'; +import type { Properties } from 'csstype'; + +/** + * Static atom access returns CompiledStyles compatible with stylex.props + * e.g., css.display.flex -> { $$css: true, display: 'x78zum5' } + */ +type StaticAtom = StyleXStyles<{ [key: string]: V }>; + +/** + * Dynamic atom call returns StyleXStyles with inline styles + * e.g., css.color(myVar) -> [{ $$css: true, color: 'x14rh7hd' }, { '--x-color': myVar }] + */ +type DynamicAtom = (value: V) => StyleXStyles<{ [key: string]: V }>; + +/** + * Each CSS property provides both static access and dynamic function call + */ +type AtomProperty = { + [Key in string | number]: StaticAtom; +} & DynamicAtom; + +type CSSValue = string | number; + +/** + * The atoms namespace provides access to all CSS properties. + * All properties are defined (non-optional) to avoid undefined checks. + */ +type Atoms = { + [Key in keyof Properties]-?: AtomProperty< + NonNullable[Key]> + >; +}; + +declare const atoms: Atoms; + +export = atoms; diff --git a/packages/@stylexjs/utility-styles/src/index.js b/packages/@stylexjs/atoms/src/index.js similarity index 83% rename from packages/@stylexjs/utility-styles/src/index.js rename to packages/@stylexjs/atoms/src/index.js index 7fe13204b..5f69533b3 100644 --- a/packages/@stylexjs/utility-styles/src/index.js +++ b/packages/@stylexjs/atoms/src/index.js @@ -26,7 +26,7 @@ const utilityStyles = new Proxy(function () {}, { }, apply() { throw new Error( - '@stylexjs/utility-styles is a compile-time helper. Attempted to call it as a function, but the StyleX compiler did not run.', + '@stylexjs/atoms is a compile-time helper. Attempted to call it as a function, but the StyleX compiler did not run.', ); }, }); diff --git a/packages/@stylexjs/babel-plugin/__tests__/transform-stylex-props-test.js b/packages/@stylexjs/babel-plugin/__tests__/transform-stylex-props-test.js index 1fbbcb649..c92228794 100644 --- a/packages/@stylexjs/babel-plugin/__tests__/transform-stylex-props-test.js +++ b/packages/@stylexjs/babel-plugin/__tests__/transform-stylex-props-test.js @@ -231,18 +231,18 @@ describe('@stylexjs/babel-plugin', () => { }); }); - describe('props calls with utility-styles', () => { + describe('props calls with atoms', () => { test('inline static styles match stylex.create', () => { const inline = transform(` import stylex from 'stylex'; - import * as css from '@stylexjs/utility-styles'; + import * as css from '@stylexjs/atoms'; stylex.props(css.display.flex); `); expect(inline).toMatchInlineSnapshot(` "import _inject from "@stylexjs/stylex/lib/stylex-inject"; var _inject2 = _inject; import stylex from 'stylex'; - import * as css from '@stylexjs/utility-styles'; + import * as css from '@stylexjs/atoms'; _inject2({ ltr: ".x78zum5{display:flex}", priority: 3000 @@ -275,14 +275,14 @@ describe('@stylexjs/babel-plugin', () => { test('inline static supports leading underscore value', () => { const inline = transform(` import stylex from 'stylex'; - import * as css from '@stylexjs/utility-styles'; + import * as css from '@stylexjs/atoms'; stylex.props(css.padding._16px); `); expect(inline).toMatchInlineSnapshot(` "import _inject from "@stylexjs/stylex/lib/stylex-inject"; var _inject2 = _inject; import stylex from 'stylex'; - import * as css from '@stylexjs/utility-styles'; + import * as css from '@stylexjs/atoms'; _inject2({ ltr: ".x1tamke2{padding:16px}", priority: 1000 @@ -315,14 +315,14 @@ describe('@stylexjs/babel-plugin', () => { test('inline static supports computed key syntax', () => { const inline = transform(` import stylex from 'stylex'; - import * as css from '@stylexjs/utility-styles'; + import * as css from '@stylexjs/atoms'; stylex.props(css.width['calc(100% - 20px)']); `); expect(inline).toMatchInlineSnapshot(` "import _inject from "@stylexjs/stylex/lib/stylex-inject"; var _inject2 = _inject; import stylex from 'stylex'; - import * as css from '@stylexjs/utility-styles'; + import * as css from '@stylexjs/atoms'; _inject2({ ltr: ".xnlsq7q{width:calc(100% - 20px)}", priority: 4000 @@ -342,7 +342,7 @@ describe('@stylexjs/babel-plugin', () => { "import _inject from "@stylexjs/stylex/lib/stylex-inject"; var _inject2 = _inject; import stylex from 'stylex'; - import * as css from '@stylexjs/utility-styles'; + import * as css from '@stylexjs/atoms'; _inject2({ ltr: ".xnlsq7q{width:calc(100% - 20px)}", priority: 4000 @@ -368,14 +368,14 @@ describe('@stylexjs/babel-plugin', () => { test('inline css supports named imports', () => { const inline = transform(` import stylex from 'stylex'; - import { color } from '@stylexjs/utility-styles'; + import { color } from '@stylexjs/atoms'; stylex.props(color.blue); `); expect(inline).toMatchInlineSnapshot(` "import _inject from "@stylexjs/stylex/lib/stylex-inject"; var _inject2 = _inject; import stylex from 'stylex'; - import { color } from '@stylexjs/utility-styles'; + import { color } from '@stylexjs/atoms'; _inject2({ ltr: ".xju2f9n{color:blue}", priority: 3000 @@ -386,10 +386,10 @@ describe('@stylexjs/babel-plugin', () => { `); }); - test('dedupes duplicate properties across create and utility-styles', () => { + test('dedupes duplicate properties across create and atoms', () => { const output = transform(` import stylex from 'stylex'; - import * as css from '@stylexjs/utility-styles'; + import * as css from '@stylexjs/atoms'; const styles = stylex.create({ base: { color: 'red', backgroundColor: 'white' }, }); @@ -399,7 +399,7 @@ describe('@stylexjs/babel-plugin', () => { "import _inject from "@stylexjs/stylex/lib/stylex-inject"; var _inject2 = _inject; import stylex from 'stylex'; - import * as css from '@stylexjs/utility-styles'; + import * as css from '@stylexjs/atoms'; _inject2({ ltr: ".x1e2nbdu{color:red}", priority: 3000 @@ -425,7 +425,7 @@ describe('@stylexjs/babel-plugin', () => { test('dynamic style', () => { const inline = transform(` import stylex from 'stylex'; - import * as css from '@stylexjs/utility-styles'; + import * as css from '@stylexjs/atoms'; stylex.props(css.color(color)); `); const local = transform(` @@ -461,7 +461,7 @@ describe('@stylexjs/babel-plugin', () => { "import _inject from "@stylexjs/stylex/lib/stylex-inject"; var _inject2 = _inject; import stylex from 'stylex'; - import * as css from '@stylexjs/utility-styles'; + import * as css from '@stylexjs/atoms'; _inject2({ ltr: ".x14rh7hd{color:var(--x-color)}", priority: 3000 @@ -485,7 +485,7 @@ describe('@stylexjs/babel-plugin', () => { test('inline static with inline dynamic', () => { const output = transform(` import stylex from 'stylex'; - import * as css from '@stylexjs/utility-styles'; + import * as css from '@stylexjs/atoms'; stylex.props(css.display.flex, css.color(color)); `); expect(output).toContain('.x78zum5{display:flex}'); @@ -497,7 +497,7 @@ describe('@stylexjs/babel-plugin', () => { test('inline static with create dynamic', () => { const output = transform(` import stylex from 'stylex'; - import * as css from '@stylexjs/utility-styles'; + import * as css from '@stylexjs/atoms'; const styles = stylex.create({ opacity: (o) => ({ opacity: o }), }); @@ -511,7 +511,7 @@ describe('@stylexjs/babel-plugin', () => { test('inline dynamic with create dynamic', () => { const output = transform(` import stylex from 'stylex'; - import * as css from '@stylexjs/utility-styles'; + import * as css from '@stylexjs/atoms'; const styles = stylex.create({ opacity: (o) => ({ opacity: o }), }); @@ -521,7 +521,7 @@ describe('@stylexjs/babel-plugin', () => { "import _inject from "@stylexjs/stylex/lib/stylex-inject"; var _inject2 = _inject; import stylex from 'stylex'; - import * as css from '@stylexjs/utility-styles'; + import * as css from '@stylexjs/atoms'; _inject2({ ltr: ".xb4nw82{opacity:var(--x-opacity)}", priority: 3000 @@ -565,14 +565,14 @@ describe('@stylexjs/babel-plugin', () => { test('inline static + inline dynamic coexist', () => { const inline = transform(` import stylex from 'stylex'; - import * as css from '@stylexjs/utility-styles'; + import * as css from '@stylexjs/atoms'; stylex.props(css.display.flex, css.color(color)); `); expect(inline).toMatchInlineSnapshot(` "import _inject from "@stylexjs/stylex/lib/stylex-inject"; var _inject2 = _inject; import stylex from 'stylex'; - import * as css from '@stylexjs/utility-styles'; + import * as css from '@stylexjs/atoms'; _inject2({ ltr: ".x78zum5{display:flex}", priority: 3000 @@ -604,7 +604,7 @@ describe('@stylexjs/babel-plugin', () => { test('inline static + create dynamic', () => { const output = transform(` import stylex from 'stylex'; - import * as css from '@stylexjs/utility-styles'; + import * as css from '@stylexjs/atoms'; const styles = stylex.create({ opacity: (o) => ({ opacity: o }), }); @@ -614,7 +614,7 @@ describe('@stylexjs/babel-plugin', () => { "import _inject from "@stylexjs/stylex/lib/stylex-inject"; var _inject2 = _inject; import stylex from 'stylex'; - import * as css from '@stylexjs/utility-styles'; + import * as css from '@stylexjs/atoms'; _inject2({ ltr: ".xb4nw82{opacity:var(--x-opacity)}", priority: 3000 @@ -646,7 +646,7 @@ describe('@stylexjs/babel-plugin', () => { test('inline dynamic + create dynamic', () => { const output = transform(` import stylex from 'stylex'; - import * as css from '@stylexjs/utility-styles'; + import * as css from '@stylexjs/atoms'; const styles = stylex.create({ opacity: (o) => ({ opacity: o }), }); @@ -656,7 +656,7 @@ describe('@stylexjs/babel-plugin', () => { "import _inject from "@stylexjs/stylex/lib/stylex-inject"; var _inject2 = _inject; import stylex from 'stylex'; - import * as css from '@stylexjs/utility-styles'; + import * as css from '@stylexjs/atoms'; _inject2({ ltr: ".xb4nw82{opacity:var(--x-opacity)}", priority: 3000 @@ -694,11 +694,11 @@ describe('@stylexjs/babel-plugin', () => { }); describe('with options', () => { - test('dev/debug classnames for utility-styles', () => { + test('dev/debug classnames for atoms', () => { const inline = transform( ` import stylex from 'stylex'; - import * as css from '@stylexjs/utility-styles'; + import * as css from '@stylexjs/atoms'; stylex.props(css.display.flex); `, { @@ -713,7 +713,7 @@ describe('@stylexjs/babel-plugin', () => { "import _inject from "@stylexjs/stylex/lib/stylex-inject"; var _inject2 = _inject; import stylex from 'stylex'; - import * as css from '@stylexjs/utility-styles'; + import * as css from '@stylexjs/atoms'; _inject2({ ltr: ".display-x78zum5{display:flex}", priority: 3000 diff --git a/packages/@stylexjs/babel-plugin/src/index.js b/packages/@stylexjs/babel-plugin/src/index.js index 84fc9c8ce..9ec62f850 100644 --- a/packages/@stylexjs/babel-plugin/src/index.js +++ b/packages/@stylexjs/babel-plugin/src/index.js @@ -38,7 +38,7 @@ import { LOGICAL_FLOAT_END_VAR, } from './shared/preprocess-rules/legacy-expand-shorthands'; import transformStyleXDefineMarker from './visitors/stylex-define-marker'; -import { createUtilityStylesVisitor } from '@stylexjs/utility-styles/babel-transform'; +import { createUtilityStylesVisitor } from '@stylexjs/atoms/babel-transform'; import { convertObjectToAST } from './utils/js-to-ast'; const NAME = 'stylex'; @@ -162,13 +162,13 @@ function styleXTransform(): PluginObj<> { skipStylexPropsChildren(path, state); }, }); - // Run utility-styles visitor first to transform x.prop.value patterns - // This runs BEFORE stylex.props so that utility styles are already + // Run atoms visitor first to transform x.prop.value patterns + // This runs BEFORE stylex.props so that atomic styles are already // compiled when stylex.props processes them - const utilityStylesVisitor = createUtilityStylesVisitor(state, { + const atomsVisitor = createUtilityStylesVisitor(state, { convertObjectToAST, }); - path.traverse(utilityStylesVisitor); + path.traverse(atomsVisitor); path.traverse({ CallExpression(path: NodePath) { diff --git a/packages/@stylexjs/babel-plugin/src/visitors/stylex-props.js b/packages/@stylexjs/babel-plugin/src/visitors/stylex-props.js index baea183f3..3c0466e50 100644 --- a/packages/@stylexjs/babel-plugin/src/visitors/stylex-props.js +++ b/packages/@stylexjs/babel-plugin/src/visitors/stylex-props.js @@ -106,7 +106,7 @@ export default function transformStylexProps( disableImports: true, }; - // Pre-process: compile any raw style objects (from utility-styles) before the main loop + // Pre-process: compile any raw style objects (from atoms) before the main loop // This transforms { property: 'value' } into { propKey: 'className', $$css: true } for (const argPath of argsPath) { compileRawStyleObjects(argPath, state, evaluatePathFnConfig); @@ -283,7 +283,7 @@ export default function transformStylexProps( }); } - // Hoist inline CSS objects (from utility-styles) to module level + // Hoist inline CSS objects (from atoms) to module level // Raw objects are pre-compiled in compileRawStyleObjects, so this only hoists compiled objects // eslint-disable-next-line no-inner-declarations function ObjectExpression(objPath: NodePath) { @@ -545,7 +545,7 @@ function isCalleeMemberExpression( } /** - * Pre-compile raw style objects from utility-styles. + * Pre-compile raw style objects from atoms. * This transforms { property: 'value' } into { propKey: 'className', $$css: true } * and registers the CSS injection. */ diff --git a/packages/@stylexjs/stylex/package.json b/packages/@stylexjs/stylex/package.json index 9697db767..355f81d71 100644 --- a/packages/@stylexjs/stylex/package.json +++ b/packages/@stylexjs/stylex/package.json @@ -37,7 +37,7 @@ "test": "cross-env BABEL_ENV=test jest --coverage" }, "dependencies": { - "@stylexjs/utility-styles": "0.17.5", + "@stylexjs/atoms": "0.17.5", "css-mediaquery": "^0.1.2", "invariant": "^2.2.4", "styleq": "0.2.1" diff --git a/packages/@stylexjs/stylex/src/stylex.js b/packages/@stylexjs/stylex/src/stylex.js index 6939a3dec..c00bb3cf8 100644 --- a/packages/@stylexjs/stylex/src/stylex.js +++ b/packages/@stylexjs/stylex/src/stylex.js @@ -285,5 +285,5 @@ _legacyMerge.viewTransitionClass = viewTransitionClass; export const legacyMerge: IStyleX = _legacyMerge; -// Re-export utility-styles for cleaner imports -export { default as x } from '@stylexjs/utility-styles'; +// Re-export atoms for cleaner imports +export { default as x } from '@stylexjs/atoms'; diff --git a/packages/@stylexjs/utility-styles/src/index.d.ts b/packages/@stylexjs/utility-styles/src/index.d.ts deleted file mode 100644 index 81fa0c043..000000000 --- a/packages/@stylexjs/utility-styles/src/index.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { StyleXClassNameFor, StyleXStyles } from '@stylexjs/stylex'; -import type { Properties } from 'csstype'; - -type UtilValue = { - [Key in string | number]: StyleXClassNameFor; -} & ((value: V) => StyleXStyles); - -type UtilStyles = { - [Key in keyof Properties]: UtilValue< - Properties[Key] - >; -}; - -declare const utilityStyles: UtilStyles; - -export = utilityStyles;