Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions packages/@stylexjs/atoms/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# @stylexjs/atoms

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 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/atoms';

function Example({ color }) {
return (
<div
{...stylex.props(
x.display.flex,
x.flexDirection.column,
x.padding._16px,
x.width['calc(100% - 20cqi)'],
x.color(color),
)}
/>
);
}
```

### Static values

Static styles are expressed via property access and are fully resolved at
compile time.

```js
x.display.flex;
x.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
x.padding._16px;
x.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
x.fontSize['1.25rem'];
x.width['calc(100% - 20cqi)'];
x.gridTemplateColumns['1fr minmax(0, 3fr)'];
```

### Dynamic values

Dynamic styles use call syntax and should be used sparingly.

```js
x.color(color);
x.marginLeft(offset);
```
26 changes: 26 additions & 0 deletions packages/@stylexjs/atoms/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "@stylexjs/atoms",
"version": "0.17.5",
"description": "Atomic style helpers for StyleX.",
"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"
},
"sideEffects": false,
"peerDependencies": {
"@stylexjs/stylex": "^0.17.5"
},
"dependencies": {
"csstype": "^3.1.3"
},
"files": [
"src"
]
}
232 changes: 232 additions & 0 deletions packages/@stylexjs/atoms/src/babel-transform.js
Original file line number Diff line number Diff line change
@@ -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 ATOMS_SOURCE = '@stylexjs/atoms';

/**
* 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 === ATOMS_SOURCE
) {
return true;
}

if (
binding.path.isImportNamespaceSpecifier() &&
binding.path.parent.type === 'ImportDeclaration' &&
binding.path.parent.source.value === ATOMS_SOURCE
) {
return true;
}

if (
binding.path.isImportDefaultSpecifier() &&
binding.path.parent.type === 'ImportDeclaration' &&
binding.path.parent.source.value === ATOMS_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,
};
44 changes: 44 additions & 0 deletions packages/@stylexjs/atoms/src/index.d.ts
Original file line number Diff line number Diff line change
@@ -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<V> = 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<V> = (value: V) => StyleXStyles<{ [key: string]: V }>;

/**
* Each CSS property provides both static access and dynamic function call
*/
type AtomProperty<V> = {
[Key in string | number]: StaticAtom<V>;
} & DynamicAtom<V>;

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<CSSValue>]-?: AtomProperty<
NonNullable<Properties<CSSValue>[Key]>
>;
};

declare const atoms: Atoms;

export = atoms;
Loading
Loading