diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 6656c27..712ebcb 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -24,3 +24,12 @@ jobs:
- name: Run tests
run: yarn test
+
+ - name: Smoke source-condition package imports
+ run: |
+ node -C cssx-ts --input-type=module -e "import { cssx, useCssxLayer } from 'cssxjs'; if (typeof cssx !== 'function' || typeof useCssxLayer !== 'function') throw new Error('cssxjs source-condition import failed')"
+
+ - name: Smoke built package imports
+ run: |
+ yarn workspace @cssxjs/css-to-rn build
+ node --input-type=module -e "import { compileCss, resolveCssx } from '@cssxjs/css-to-rn'; const sheet = compileCss('.root { color: red; }'); const result = resolveCssx({ styleName: 'root', layers: sheet }); if (result.props.style.color !== 'red') throw new Error('built css-to-rn import failed')"
diff --git a/.gitignore b/.gitignore
index ab51119..b2be2c2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,6 +11,7 @@
# Node dependencies
node_modules
+packages/*/dist
# npm-debug log
npm-debug.*
diff --git a/AGENTS.md b/AGENTS.md
index bea3b03..02a1990 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -2,7 +2,7 @@
Read this first, then use `architecture.md` for the detailed system map.
-CSSX is a monorepo for a CSS-in-JS toolchain. Users write `styl`, `css`, or optional `pug` templates plus `styleName` and `part` props. Babel compiles that authoring syntax into style objects and runtime calls. The runtime matches class selectors, applies CSS variables/media queries, supports component parts, and can memoize with teamplay.
+CSSX is a monorepo for a CSS-in-JS toolchain. Users write `css`, `styl`, or optional `pug` templates plus `styleName` and `part` props. Babel compiles that authoring syntax into compiled CSS sheet IR and runtime calls. The unified `@cssxjs/css-to-rn` package owns CSS parsing, CSS value resolution, React Native/web style transformation, runtime caching, variables, media/dimension tracking, and React subscriptions.
## Start Here
@@ -12,11 +12,11 @@ CSSX is a monorepo for a CSS-in-JS toolchain. Users write `styl`, `css`, or opti
## Package Map
-- `packages/cssxjs/`: public `cssxjs` facade, CLI, wrappers, package exports.
-- `packages/runtime/`: `process()`, `matcher()`, variables, dimensions, platform helpers, teamplay caching.
-- `packages/loaders/`: Stylus/CSS loaders and direct compiler wrappers.
-- `packages/babel-plugin-rn-stylename-inline/`: compiles inline `css` and `styl` templates.
-- `packages/babel-plugin-rn-stylename-to-style/`: rewrites JSX `styleName`, `part`, old `*StyleName`, and helper calls.
+- `packages/css-to-rn/`: unified compiler/runtime engine. Start here for CSS parsing, selector IR, value resolution, property transforms, caching, `cssx()`, `useRuntimeCss()`, variables, and dimensions.
+- `packages/cssxjs/`: public `cssxjs` facade, CLI, package exports, runtime compatibility wrappers, Babel preset wrapper, loader wrappers, and Metro wrappers.
+- `packages/loaders/`: Stylus/CSS loaders and direct compiler wrappers. CSS compilation delegates to `@cssxjs/css-to-rn`.
+- `packages/babel-plugin-rn-stylename-inline/`: compiles inline `css` and `styl` templates, including local template interpolation lowering.
+- `packages/babel-plugin-rn-stylename-to-style/`: rewrites JSX `styleName`, `part`, old `*StyleName`, and helper calls into runtime calls.
- `packages/babel-preset-cssxjs/`: transform ordering and public Babel options.
- `packages/bundler/`: Metro hot-reload path for separate style files.
- `packages/eslint-plugin-cssxjs/`: wrapper around React Pug ESLint processor.
@@ -26,13 +26,14 @@ CSSX is a monorepo for a CSS-in-JS toolchain. Users write `styl`, `css`, or opti
## Core Contracts
- `__CSS_GLOBAL__` and `__CSS_LOCAL__` connect the inline Babel plugin to the JSX/runtime plugin.
-- Compiled style metadata `__hash__`, `__vars`, and `__hasMedia` connects loaders to cached and uncached runtime processing.
-- Runtime calls have this shape: `runtime(styleName, fileStyles, globalStyles, localStyles, inlineStyleProps)`.
-- Style priority is file styles, then global templates, then local templates, then inline props.
-- Selector specificity is approximated by class count only.
+- Runtime calls generated by Babel keep the compatibility shape `runtime(styleName, fileStyles, globalStyles, localStyles, inlineStyleProps)`.
+- `cssxjs/runtime/*` wrappers adapt that call shape to `@cssxjs/css-to-rn` platform entrypoints.
+- Style priority is file/imported sheets, then global templates, then local templates, then inline props.
+- Compiled sheets are JSON-serializable IR. Runtime cache/tracking state must stay outside the sheet.
- `part='root'` maps to `style`; other parts map to `{partName}Style`.
-- `css`/`styl` template interpolation is intentionally unsupported.
-- Cached runtime is selected by `cache: 'teamplay'` or by importing `observer` from `teamplay` or `startupjs`.
+- `:hover` and `:active` compile to `hoverStyle` and `activeStyle`.
+- Local JS template interpolation is lowered to synthetic `var(--__cssx_dynamic_N)` slots and passed as `values`.
+- `cache: 'teamplay'` remains accepted as a Babel option for compatibility, but runtime caching is owned by `@cssxjs/css-to-rn`, not Teamplay.
## Commands
@@ -51,7 +52,7 @@ yarn test
Run targeted tests:
```sh
-cd packages/runtime && yarn test
+cd packages/css-to-rn && npm test
cd packages/babel-plugin-rn-stylename-inline && yarn test
cd packages/babel-plugin-rn-stylename-to-style && yarn test
```
@@ -70,9 +71,10 @@ yarn start
## Change Guidance
-- For runtime matching changes, update `packages/runtime/test/matcher.mjs` and `packages/runtime/test/process.mjs`.
-- For Babel changes, update the relevant Jest snapshots.
-- For public API or behavior changes, update `docs/` and `architecture.md`.
+- For CSS parsing, selector, value, transform, cache, variable, media, or React tracking behavior, update `packages/css-to-rn/test/engine/**` or `packages/css-to-rn/test/react/**`.
+- For inline template or interpolation compilation, update `packages/babel-plugin-rn-stylename-inline` snapshots.
+- For JSX `styleName`/`part` behavior, update `packages/babel-plugin-rn-stylename-to-style` snapshots.
+- For public API or behavior changes, update `docs/`, `architecture.md`, and this guide.
- For Pug, type checking, or ESLint behavior, check whether the implementation lives in `@react-pug/*`; this repo often only wraps it.
- For separate style files, check both Babel `compileCssImports` behavior and Metro transformer behavior.
- Prefer current source code and `docs/` over older package READMEs when they conflict.
diff --git a/README.md b/README.md
index c2d384a..7601c5f 100644
--- a/README.md
+++ b/README.md
@@ -23,6 +23,18 @@ Install the following extension for full CSSX support with Pug and CSS/Stylus in
[`vscode-react-pug-tsx`](https://marketplace.visualstudio.com/items?itemName=startupjs.vscode-react-pug-tsx)
+## Credits
+
+CSSX's unified CSS-to-React-Native compiler/runtime was inspired by and replaces
+the separate roles previously handled by:
+
+- [`css-to-react-native`](https://github.com/styled-components/css-to-react-native)
+- [`css-to-react-native-transform`](https://github.com/kristerkari/css-to-react-native-transform)
+
+The runtime and API design also benefited from studying:
+
+- [`cssta`](https://github.com/jacobp100/cssta)
+
## License
MIT
diff --git a/architecture.md b/architecture.md
index bc38792..34eb266 100644
--- a/architecture.md
+++ b/architecture.md
@@ -1,44 +1,43 @@
# CSSX Architecture
-CSSX is a CSS-in-JS system for React Native, react-native-web, and pure React web targets. Its public API lets users write `styl`, `css`, and optional `pug` tagged template literals, apply styles with `styleName`, expose child component override points with `part`, and update CSS variables at runtime.
+CSSX is a CSS-in-JS system for React Native, react-native-web, and pure React web targets. Users write `css`, `styl`, and optional `pug` templates, apply styles with `styleName`, expose child component override points with `part`, and update CSS variables at runtime.
-Most work happens at build time. Babel compiles template literals and `.cssx.*` imports into plain style objects, then rewrites JSX so elements receive a spread of runtime-generated style props. The runtime is deliberately small: it matches class names to compiled selectors, applies CSS variables and media queries, handles `:part()` style props, and optionally memoizes results with teamplay.
+The current architecture centers on `@cssxjs/css-to-rn`. That package owns the unified CSS-to-style pipeline: CSS parsing, canonical sheet IR, selector matching, CSS variable/interpolation resolution, React Native/web property transformation, runtime caching, dimensions/media tracking, and React subscription helpers. The older separate runtime package has been removed from the active dependency graph.
## Repository Map
-- `docs/`: public documentation served by Rspress. Start here for expected user-facing behavior.
-- `packages/cssxjs/`: umbrella package published as `cssxjs`. It exposes the public entrypoints, CLI, runtime wrappers, Babel preset wrapper, loader wrappers, and Metro wrappers.
-- `packages/runtime/`: style matching, CSS variable state, media-query dimension state, platform helper injection, and cached/non-cached runtime entrypoints.
-- `packages/loaders/`: webpack-compatible style loaders plus direct compiler helpers used by Babel.
-- `packages/babel-plugin-rn-stylename-inline/`: compiles inline `css` and `styl` template literals into module/function-scoped style objects.
-- `packages/babel-plugin-rn-stylename-to-style/`: rewrites `styleName`, `part`, `*StyleName`, and `styl(...)`/`css(...)` function calls into runtime calls.
-- `packages/babel-preset-cssxjs/`: composes syntax plugins, React Pug transform, inline style compilation, and `styleName`/`part` transform.
+- `docs/`: public documentation served by Rspress.
+- `packages/css-to-rn/`: unified compiler and runtime engine.
+- `packages/cssxjs/`: umbrella package published as `cssxjs`; exports public APIs, runtime compatibility wrappers, Babel/Metro wrappers, CLI, and loader wrappers.
+- `packages/loaders/`: webpack-compatible loaders plus compiler helpers used by Babel and Metro. Stylus still compiles to CSS here; CSS compilation delegates to `@cssxjs/css-to-rn`.
+- `packages/babel-plugin-rn-stylename-inline/`: compiles inline `css` and `styl` tagged templates.
+- `packages/babel-plugin-rn-stylename-to-style/`: rewrites JSX `styleName`, `part`, old `*StyleName`, and helper calls into runtime calls.
+- `packages/babel-preset-cssxjs/`: composes syntax plugins, React Pug, inline style compilation, and `styleName`/`part` rewriting.
- `packages/bundler/`: Metro config and transformer support for separate `.cssx.styl` and `.cssx.css` files.
- `packages/eslint-plugin-cssxjs/`: facade over `@react-pug/eslint-plugin-react-pug`.
- `example/`: simple web example using Babel plus esbuild directly.
-- `docs-theme/` and `rspress.config.ts`: documentation theme and syntax highlighting configuration.
-The repository uses Yarn workspaces and Lerna. Root `package.json` requires Node `>=22` and defines the main scripts.
+The repository uses Yarn workspaces and Lerna. Root `package.json` requires Node `>=22`.
## Public API Surface
-The published `cssxjs` package exposes:
+The `cssxjs` package exposes:
-- `styl` and `css`: template tags processed away by Babel, and function forms used as `styl(styleName, inlineStyleProps)` / `css(...)` after Babel rewrites them.
+- `css` and `styl`: Babel-processed template tags, plus helper-call forms after Babel rewriting.
- `pug`: template tag processed by `@react-pug/babel-plugin-react-pug`.
-- `variables`: observable runtime CSS variable overrides.
-- `setDefaultVariables` and `defaultVariables`: default CSS variable registry.
-- `dimensions`: observable screen width state for media-query invalidation.
-- `matcher`: advanced/internal class selector matcher.
-- `cssxjs/babel`: Babel preset wrapper.
-- `cssxjs/metro-config` and `cssxjs/metro-babel-transformer`: Metro integration wrappers.
+- `cssx`: runtime helper from `@cssxjs/css-to-rn/react`.
+- `variables`, `defaultVariables`, `setDefaultVariables`: runtime CSS variable registries.
+- `useRuntimeCss`, `useCssxSheet`, `useCssxTemplate`: React helpers for runtime-generated CSS and local template values.
+- `CssxProvider`, `configureCssx`, `useCssxConfig`: optional runtime configuration.
+- `cssxjs/runtime`, `cssxjs/runtime/web`, `cssxjs/runtime/react-native`, and `teamplay` compatibility runtime paths used by Babel-generated code.
+- `cssxjs/babel`, `cssxjs/metro-config`, and `cssxjs/metro-babel-transformer`.
- `cssxjs check`: CLI bridge to `@react-pug/check-types`.
-`packages/cssxjs/index.js` intentionally makes `css`, `styl`, and `pug` throw at runtime. If a user sees those errors, their file did not go through the Babel pipeline.
+`packages/cssxjs/index.js` intentionally makes direct unprocessed `css`, `styl`, and `pug` calls throw. Seeing those errors means the file did not go through the Babel pipeline.
-## End-to-End Build Flow
+## End-To-End Flow
-### 1. Authoring
+### Authoring
Users write components like:
@@ -63,7 +62,7 @@ function Button ({ variant, children }) {
}
```
-Parent components can target the exposed parts from outside:
+Parent components can target exposed parts from outside:
```jsx
function Toolbar () {
@@ -78,92 +77,68 @@ function Toolbar () {
}
```
-The core authoring constructs are:
+Parts are only addressable from outside the component exposing them. Inside a component, style the inner element directly with its own class selector.
-- class-like `styleName` values: strings, arrays, and object flags.
-- `part` attributes with compile-time-static names.
-- `:part(name)` selectors in CSS/Stylus, used by parent/outside styles to target child component parts.
-- runtime CSS variables through `var(--name, fallback)`.
-- media queries and viewport units.
-- optional Pug templates and embedded terminal `style` blocks.
+### Babel Preset
-### 2. Babel Preset
+`packages/babel-preset-cssxjs/index.js` configures transforms in this order:
-`packages/babel-preset-cssxjs/index.js` configures the transform stack:
-
-1. Syntax support for JSX, TypeScript, and TSX depending on filename.
+1. JSX/TypeScript syntax plugins.
2. `@react-pug/babel-plugin-react-pug` when `transformPug !== false`.
3. `@cssxjs/babel-plugin-rn-stylename-inline` when `transformCss !== false`.
4. `@cssxjs/babel-plugin-rn-stylename-to-style` when `transformCss !== false`.
-This order matters. Pug must become JSX before CSSX rewrites JSX attributes. Inline CSS/Stylus templates must compile before `styleName` references are converted into runtime calls.
+This order matters. Pug must become JSX before CSSX rewrites JSX attributes, and inline CSS/Stylus templates must compile before `styleName` references are converted into runtime calls.
-Preset options:
+Important options:
- `platform`: passed to style compilers. Defaults to `web` or Babel caller platform.
-- `reactType`: chooses runtime target, currently `web` or `react-native`.
-- `cache`: chooses cached runtime, currently only `teamplay`.
-- `transformPug`: disables Pug transformation when false.
-- `transformCss`: disables CSS/Stylus and `styleName` transformation when false.
+- `reactType`: chooses runtime target, `web` or `react-native`.
+- `cache`: accepts `teamplay` for compatibility. It still affects generated import paths, but runtime caching is now internal to `@cssxjs/css-to-rn`.
+- `transformPug` and `transformCss`: disable the corresponding transforms when false.
-### 3. Pug Transform
+### Inline Template Compilation
-Pug support is provided by external `@react-pug/*` packages. CSSX wraps those packages through:
+`packages/babel-plugin-rn-stylename-inline/index.js` handles `css` and `styl` tagged templates imported from magic imports. Defaults are `cssxjs` and `startupjs`.
-- `cssxjs/babel/plugin-react-pug`
-- `cssxjs check`
-- `eslint-plugin-cssxjs`
+Behavior:
-Current CSSX docs recommend terminal embedded style blocks inside Pug templates:
+- Imported aliases are supported.
+- Module-level templates become `const __CSS_GLOBAL__ = compiledSheet`.
+- Function-level templates become a top-level compiled sheet plus function-local `const __CSS_LOCAL__ = compiledSheet`.
+- Local JS template interpolation is supported. Expressions are lowered to synthetic declaration-value variables:
```jsx
-return pug`
- View.card
- Text.title= title
-
- style(lang='styl')
- .card
- padding 2u
+css`
+ .root {
+ color: ${color};
+ padding: ${pad} 2u;
+ }
`
```
-The React Pug Babel plugin turns this into JSX plus local `styl` or `css` templates, which are then handled by CSSX's inline style plugin.
-
-### 4. Inline Style Compilation
-
-`packages/babel-plugin-rn-stylename-inline/index.js` processes `css` and `styl` tagged template literals imported from magic imports. The default magic imports are `cssxjs` and `startupjs`.
-
-Important behavior:
-
-- Only imported `css`/`styl` identifiers are processed. Aliases are supported.
-- Template interpolation is rejected. Dynamic values should use CSS variables or inline `style`.
-- Module-level templates become a top-level `const __CSS_GLOBAL__ = ...`.
-- Function-level templates become a top-level compiled object plus a function-local `const __CSS_LOCAL__ = ...`.
-- The plugin removes processed template expressions.
-- Compilation is delegated to `@cssxjs/loaders/compilers`.
+compiles as a sheet containing `var(--__cssx_dynamic_0)` and `var(--__cssx_dynamic_1)`, while the function receives:
-The generated names come from `packages/runtime/constants.cjs`:
-
-- `GLOBAL_NAME`: `__CSS_GLOBAL__`
-- `LOCAL_NAME`: `__CSS_LOCAL__`
-
-Those names are part of the transform/runtime contract.
-
-### 5. Style File Imports and JSX Rewriting
+```js
+const __CSS_LOCAL__ = {
+ sheet: _localCssInstance,
+ values: [color, pad]
+}
+```
-`packages/babel-plugin-rn-stylename-to-style/index.js` is the main JSX transform. It has three jobs.
+Interpolations are allowed only in function-scoped local `css`/`styl` templates and only in declaration values. Selectors, property names, media queries, exports, and module-level templates remain static.
-First, it handles style file imports. Default extensions are `cssx.css` and `cssx.styl`, so imports such as `import './Button.cssx.styl'` are style imports. In tests the plugin is often configured with `extensions: ['styl', 'css']`.
+### JSX Rewriting
-When `compileCssImports` is true, Babel reads and compiles the file itself and replaces the import with a compiled `const`. This is convenient but means changes to the separate style file may require restarting or clearing Babel cache. When false, the import stays in place and the bundler must compile it.
+`packages/babel-plugin-rn-stylename-to-style/index.js` handles JSX styling attributes and helper calls.
-Second, it rewrites JSX styling attributes. A JSX opening element with `styleName`, `style`, or part style props becomes a spread call:
+A JSX opening element with `styleName`, `style`, or part style props becomes a spread call:
```jsx
```
-becomes conceptually:
+conceptually becomes:
```jsx
```
-The runtime call returns an object containing `style` and any `{part}Style` props.
+The runtime call returns an object containing `style` and any `{part}Style`, `hoverStyle`, or `activeStyle` props.
-Third, it rewrites function calls to imported `styl`/`css` identifiers. This supports the public spread helper form:
+The same runtime call shape is used for public helper forms like:
```jsx
```
-The helper call is replaced with the same runtime call shape used for JSX attributes.
-
-Runtime import paths are chosen from plugin options and imports:
+Runtime import paths are selected by plugin options:
- default: `cssxjs/runtime`
- `reactType: 'web'`: `cssxjs/runtime/web`
@@ -195,166 +168,183 @@ Runtime import paths are chosen from plugin options and imports:
- `cache: 'teamplay'`: `cssxjs/runtime/teamplay`
- both `reactType` and `cache`: `cssxjs/runtime/web-teamplay` or `cssxjs/runtime/react-native-teamplay`
-If the file imports an `observer` named import from `teamplay` or `startupjs`, the plugin auto-selects `cache: 'teamplay'`.
-
-## Style Compilation
-
-### Loader Chain
+The `teamplay` paths are compatibility wrappers around the same new runtime.
-The style compiler path is:
+## `@cssxjs/css-to-rn`
-1. Stylus input goes through `stylusToCssLoader` to become CSS.
-2. CSS input goes through `cssToReactNativeLoader`.
-3. `cssToReactNativeLoader` calls `@startupjs/css-to-react-native-transform` to produce React Native style objects.
+This package is TypeScript-first ESM and uses Node's strip-only TS support for tests via the custom export condition `cssx-ts`.
-`packages/loaders/compilers/*` wrap the loaders for synchronous direct use from Babel and strip the generated `module.exports =` prefix.
+### Exports
-### Stylus Loader
+- Root `@cssxjs/css-to-rn`: isomorphic compiler/resolver APIs.
+- `@cssxjs/css-to-rn/react`: React runtime helpers with conditional web/native behavior.
+- `@cssxjs/css-to-rn/web`: web-targeted helpers.
+- `@cssxjs/css-to-rn/react-native`: React Native-targeted helpers.
-`packages/loaders/stylusToCssLoader.js`:
+`react` and `react-native` are optional peer dependencies.
-- creates a Stylus compiler for the source.
-- sets `filename` for error reporting/import resolution.
-- defines `$PLATFORM` and `__WEB__`, `__IOS__`, `__ANDROID__`, etc. when a platform is provided.
-- auto-imports `@startupjs/ui/styles/index.styl` and `@startupjs-ui/core/styles/index.styl` if those packages are installed.
-- auto-imports `styles/index.styl` from `process.cwd()` if present.
-- applies `patchStylusAddUnit()` once.
+### Canonical Sheet IR
-`patchStylusAddUnit()` monkey-patches Stylus units so `1u` is converted to `8px` during Stylus compilation.
+`compileCss()` and `compileCssTemplate()` return JSON-serializable sheets:
-### CSS-to-RN Loader
+```ts
+interface CompiledCssSheet {
+ version: 1
+ id: string
+ sourceId?: string
+ contentHash: string
+ rules: CssxRule[]
+ keyframes: Record
+ exports?: Record
+ metadata: CssxMetadata
+ diagnostics: CssxDiagnostic[]
+ error?: CssxDiagnostic
+}
+```
-`packages/loaders/cssToReactNativeLoader.js`:
+Rules preserve:
-- calls `@startupjs/css-to-react-native-transform` with media queries, part selectors, and keyframes enabled.
-- supports `:export { ... }` values and converts exported Stylus values into JS values.
-- adds `__hash__` to the compiled object for memoization keys.
-- adds `__vars` with sorted CSS variable names when `var(...)` is present.
-- adds `__hasMedia` when top-level `@media` rules exist.
-- returns JS source in the shape `module.exports = { ... }`.
+- selector text.
+- class list.
+- logical part target, or `null` for root.
+- class-count specificity.
+- source order.
+- optional media condition.
+- declaration order and source locations.
-The metadata fields are consumed by `packages/runtime/process.js` and `packages/runtime/processCached.js`; changing them requires coordinated runtime updates.
+The sheet must remain serializable. Cache state, subscriptions, and runtime trackers live outside the sheet.
-## Runtime
+### Compiler
-Runtime entrypoints live in `packages/runtime/entrypoints/*`. Each entrypoint:
+`src/compiler.ts` parses CSS with the lightweight `css` parser. Runtime mode returns an empty diagnostic sheet on syntax errors. Build mode throws for errors that should fail Babel/loader builds.
-1. injects platform helpers through `setPlatformHelpers()`.
-2. initializes the dimensions updater.
-3. exports either the normal `process` function or the teamplay-cached `process` function.
+Build mode validates static declaration values through the shared value resolver
+and property transformer. Unsupported static constructs such as
+layout-dependent `calc()` expressions, unsupported transform functions, and
+unsupported background images fail during Babel/loader compilation.
+Declarations containing `var()` or template slots are deferred to runtime
+validation because their final value is not knowable at build time.
-The facade package re-exports these entrypoints from `packages/cssxjs/runtime/*` and provides both default and named `runtime` exports, because the Babel plugin imports `{ runtime as _runtime }`.
+Supported selectors:
-### Platform Helpers
+- `.root`
+- `.root.active`
+- `.root:part(label)`
+- `.root.active:part(icon)`
+- `.root:hover`
+- `.root:active`
+- `:export`
-`packages/runtime/platformHelpers/index.js` stores the active helper implementation. Helpers provide:
+`:hover` maps to `hoverStyle`; `:active` maps to `activeStyle`. Unsupported selectors are ignored with diagnostics in runtime mode.
-- `getDimensions()`
-- `getPlatform()`
-- `isPureReact()`
-- `initDimensionsUpdater()`
+`:root` custom-property declarations and declaration-level custom properties are intentionally not used as defaults. Use `setDefaultVariables()` for defaults.
-`platformHelpers/web.js` uses `window.innerWidth`/`innerHeight`, falls back to `1024x768` without `window`, reports platform `web`, and marks pure React mode true.
+### Value Resolution
-`platformHelpers/react-native.js` uses React Native `Dimensions` and `Platform`, reports pure React mode false, and listens for dimension changes.
+`src/values.ts` resolves declaration value strings before property transformation:
-The runtime logs and throws if helpers are missing, which usually means Babel imported the wrong runtime entrypoint.
+1. Replace interpolation slots from `values`.
+2. Recursively resolve nested `var()`.
+3. Resolve `u`, viewport units, and supported `calc()`.
+4. Return dependencies for variables and dimensions.
-### Variables and Dimensions
+Variable priority is:
-`packages/runtime/variables.js` exports:
+1. runtime `variables['--name']`
+2. `defaultVariables['--name']`
+3. inline fallback `var(--name, fallback)`
-- default observable `variables` object.
-- mutable `defaultVariables`.
-- `setDefaultVariables()`.
+Unresolved variables, cycles, depth limits, invalid interpolations, and unsupported `calc()` invalidate only the containing declaration. Earlier fallback declarations in the same rule still apply.
-Resolution order is:
+`1u = 8px`. Viewport units resolve from current dimensions. `calc()` supports arithmetic that can reduce to a concrete numeric or pixel value; layout-dependent percentages are unsupported.
-1. runtime `variables['--name']`
-2. `defaultVariables['--name']`
-3. inline fallback from `var(--name, fallback)`
+### Property Transformation
-`packages/runtime/dimensions.js` exports an observable `{ width: 0 }` singleton plus an initialization flag.
+`src/transform/index.ts` turns final CSS declaration values into React Native/web style props. It supports:
-Both observables come from `@nx-js/observer-util`. The uncached runtime reads these observables while processing styles; the cached runtime reads them in its `forceUpdateWhenChanged` hook.
+- raw camelCase property pass-through.
+- margin/padding/border/border-radius/border-width/border-color shorthands.
+- transform arrays.
+- text-shadow.
+- box-shadow string pass-through.
+- `filter` string pass-through.
+- animation and transition shorthands/longhands.
+- keyframe object inlining for Reanimated v4 style props.
+- `background-image` and limited `background` shorthand.
-### `process()`
+For React Native, `background-image` becomes `experimental_backgroundImage`. Only `linear-gradient()` and `radial-gradient()` are emitted; image URLs and other image functions are diagnosed and dropped.
-`packages/runtime/process.js` is the main runtime function:
+### Resolver And Caching
-```js
-process(styleName, fileStyles, globalStyles, localStyles, inlineStyleProps)
-```
+`src/resolve.ts` composes the compiler, value resolver, property transformer, cascade, and caching.
-It:
+Public functions:
-1. transforms each style object:
- - replaces CSS variables when `__vars` exists.
- - listens to dimensions when `__hasMedia` exists.
- - applies media queries and viewport units through vendored processors.
-2. calls `matcher()`.
-3. flattens nested specificity arrays into single style objects.
-4. adjusts pure React values such as numeric `lineHeight` to `px` strings.
-5. applies runtime `u` unit replacement for string values that still contain `u`.
+- `resolveCssx(options)`
+- `cssx(styleName, layers, inlineStyleProps?, options?)`
+- `createCssxCache()`
-### `matcher()`
+Resolver order:
-`packages/runtime/matcher.js` is intentionally simple and class-only.
+1. Normalize `styleName` with classcat-like semantics.
+2. Normalize one or more sheet layers.
+3. Match selectors by class set.
+4. Filter inactive media rules.
+5. Group by output prop: `style`, `{part}Style`, `hoverStyle`, `activeStyle`.
+6. Apply cascade by layer, specificity, and source order.
+7. Resolve dynamic values declaration-by-declaration.
+8. Transform final declarations.
+9. Inline only keyframes referenced by active animation declarations.
+10. Merge inline style props last.
-Input `styleName` is normalized through an embedded classcat-style function. Supported shapes are strings, arrays, and object flags.
+Runtime caches are bounded. Static cache keys include sheet identity, style names, and a JSON/hash of inline style props. Dynamic signatures include only used variables, used media matches, and dimensions when actually used. Interpolated local templates keep one effective cache entry per tracked sheet call shape; changing values replaces the same cache slot instead of growing historical variants.
-For each selector in each style object:
+`JSON.stringify()` is intentionally used for inline style value hashing. Cyclic inline style objects are treated as uncacheable.
-- `:part(name)` or `::part(name)` targets prop `nameStyle`.
-- no part selector targets root prop `style`.
-- `part(root)` is handled by Babel as root `style`, not by the matcher.
-- selectors are matched by checking whether every class in the selector exists in the normalized `styleName`.
-- selector specificity is approximated by number of classes.
+### React Runtime
-Application order is:
+`src/react/**` adds React integration without making `cssx()` a hook.
-1. file styles
-2. global inline templates
-3. local inline templates
-4. inline style props
+Key pieces:
-Because `process()` flattens and `Object.assign`s in that order, later layers override earlier layers. Within each layer, selectors with more classes override selectors with fewer classes.
+- `store.ts`: `variables`, `defaultVariables`, `setDefaultVariables()`, dimensions/media state, microtask-batched notifications.
+- `tracker.ts`: `TrackedCssxSheet`, committed dependency snapshots, per-tracker cache.
+- `cssx.ts`: ergonomic `cssx()` wrapper that delegates to `resolveCssx()` and records dependencies into tracked sheets during render.
+- `hooks.ts`: `useCssxSheet()`, `useRuntimeCss()`, `useCssxTemplate()`, `useCssxLayer()`.
+- `config.ts`: optional `CssxProvider`, `configureCssx()`, and `useCssxConfig()`.
-There is also a legacy matcher mode when `inlineStyleProps` is omitted. It returns only root style arrays and exists for older `*StyleName` conversion behavior.
+`useCssxSheet()` starts a render-local dependency collection before render and commits it in a layout/effect phase. If a render is aborted, for example because a component throws a promise into Suspense, the pending dependencies are not committed and do not leak global subscriptions.
-### Cached Runtime
+Variable writes and deletes notify subscribers once per microtask. Subscribers only rerender when a variable they actually used changes. Viewport-unit subscribers are tied to dimension changes. Media-query dependencies store the match value observed during the committed render; dimension changes and platform media adapter changes only rerender subscribers whose committed media result changed. Browser `matchMedia` is used on web when available, and tests can install a media-query adapter for non-DOM media features such as `prefers-color-scheme`, `hover`, and `pointer`. Web resize uses leading plus trailing debounced updates.
-`packages/runtime/processCached.js` wraps `process()` with `teamplay/cache` `singletonMemoize`.
+## Loaders And Separate Files
-The cache normalizer hashes:
+Stylus remains separate from CSS-to-RN transformation:
-- `styleName`
-- each style object's `__hash__` or full object
-- `inlineStyleProps`
+1. `stylusToCssLoader` compiles Stylus to CSS and preserves current project/UI auto-import behavior.
+2. `cssToReactNativeLoader` calls `compileCss()` or `compileCssTemplate()` from `@cssxjs/css-to-rn`.
+3. The loader emits `module.exports = `.
-The cache invalidation hook watches:
+`cssToReactNativeLoader` still handles `:export` compatibility by exposing exports as top-level properties on the emitted object. It also adds `__hash__` for old generated-code compatibility, but the new runtime uses sheet IDs and its own cache.
-- `dimensions.width` when any style object has `__hasMedia`.
-- specific variables listed in `__vars`.
+The loader is CommonJS because Babel and webpack loader APIs are synchronous CommonJS. In normal Node >=22 usage it can require the ESM package directly. Jest's CommonJS runtime cannot, so plugin tests use the Teamplay-style TS/Jest setup and a test-only child-process fallback when Jest intercepts ESM loading.
-The cached runtime depends on `teamplay` being installed. It is selected explicitly with `cache: 'teamplay'` or implicitly by importing `observer` from `teamplay` or `startupjs`.
+Metro separate-file support lives in `packages/bundler`. Inline templates do not need Metro loader setup.
## Component Parts
-Parts are a two-sided compile-time and runtime protocol.
-
-Parts are only addressable from the outside. A component styles its own elements with its own class selectors, such as `.text`; parent components use `:part(text)` against the child's exposed `part='text'` element.
+Parts are a compile-time/runtime protocol.
-On the parent side, a selector like:
+On the parent side:
```stylus
.card:part(title)
color red
```
-is compiled as a selector that `matcher()` returns under `titleStyle` when the parent element has styleName `card`.
+resolves under `titleStyle` when the parent element has `styleName='card'`.
-On the child side, JSX like:
+On the child side:
```jsx
function Card ({ title }) {
@@ -362,9 +352,9 @@ function Card ({ title }) {
}
```
-is rewritten so the closest likely React component accepts `titleStyle` and appends it to the element's root `style` prop. If props are destructured, the Babel plugin injects missing part style variables into the destructuring pattern. If no props parameter exists, it creates one.
+is rewritten so the closest likely React component accepts `titleStyle` and appends it to that element's `style` prop. If props are destructured, the Babel plugin injects missing part style variables into the destructuring pattern. If no props parameter exists, it creates one.
-`part='root'` is special. It maps to `style`, so parent styles for a component's own class can reach the component's root element without a `rootStyle` prop.
+`part='root'` maps to the normal `style` prop.
Part names must be statically knowable. Supported `part` values are:
@@ -372,33 +362,9 @@ Part names must be statically knowable. Supported `part` values are:
- arrays of string literals and object expressions.
- object expressions with static keys and dynamic truthy/falsy values.
-Unsupported dynamic part names intentionally throw at build time.
+Unsupported dynamic part names throw at build time.
-## CSS Semantics and Limits
-
-Supported features are constrained by React Native style capabilities and `@startupjs/css-to-react-native-transform`.
-
-Supported in current code and docs:
-
-- class selectors and compound class selectors.
-- `&` parent selector in Stylus.
-- `:part(name)` and `::part(name)`.
-- CSS variables in full or compound values.
-- media queries.
-- viewport units through the vendored dynamic style processor.
-- keyframes, animation, and transition output from the CSS-to-RN transformer.
-- `u` unit, where `1u = 8px`.
-- `:export` blocks in style files.
-
-Not supported by design:
-
-- expression interpolation inside `css` or `styl` template literals.
-- descendant selectors.
-- attribute selectors.
-- web pseudo-classes such as `:hover`, `:focus`, and `:active`.
-- pseudo-elements such as `::before` and `::after`.
-
-## Pug, Type Checking, and Linting
+## Pug, Type Checking, And Linting
CSSX does not implement the Pug parser itself. It wraps React Pug tooling:
@@ -414,79 +380,44 @@ npx cssxjs check [files...] [--project ]
and delegates to `packages/cssxjs/check.js`, which re-exports `@react-pug/check-types`.
-`eslint-plugin-cssxjs` is a package-name facade over `@react-pug/eslint-plugin-react-pug`, so changes to lint behavior usually belong upstream unless the wrapper API changes.
-
-## Metro and Separate Style Files
-
-Inline `css`/`styl` templates are handled by Babel and do not require Metro configuration.
-
-Separate `.cssx.styl` files need bundler support for hot reloading. `packages/bundler/metro-config.js`:
-
-- starts from Expo, React Native 0.73+, or older Metro default config.
-- sets `babelTransformerPath` to CSSX's Metro transformer.
-- adds `css` and `styl` to `resolver.sourceExts`.
-- enables package exports.
-- disables Expo's CSS support when using Expo defaults.
-
-`packages/bundler/metro-babel-transformer.js`:
-
-- compiles `.styl` through Stylus then CSS-to-RN.
-- compiles `.css` through CSS-to-RN.
-- passes resulting JS source to the upstream Metro Babel transformer.
-
-This path is primarily for imported style files and hot reloading. The preferred component-local path remains inline templates or Pug embedded style blocks.
-
-## Example App
-
-`example/` is a pure web demonstration:
-
-- `example/server.js` starts an HTTP server on port 3000.
-- `example/_serveClient.js` runs Babel with `cssxjs/babel`, then bundles with esbuild from memory.
-- `example/client.tsx` demonstrates Pug, embedded Stylus, `styleName`, `part`, media queries, and external `.cssx.styl` import.
-
-Run it with:
-
-```sh
-yarn start
-```
-
-from the repository root.
-
## Testing
-Root script:
+Run everything:
```sh
yarn test
```
-This loops over every `packages/*` directory and runs each package's `yarn test`.
-
Useful targeted tests:
```sh
-cd packages/runtime && yarn test
+cd packages/css-to-rn && npm test
cd packages/babel-plugin-rn-stylename-inline && yarn test
cd packages/babel-plugin-rn-stylename-to-style && yarn test
```
-Runtime tests live in `packages/runtime/test/*.mjs`.
+`@cssxjs/css-to-rn` tests:
+
+- `test/engine/**`: parser IR, value resolution, property transforms, resolver cascade, cache behavior.
+- `test/react/**`: variable batching, dependency tracking, media adapter invalidation, aborted-render safety, tracked cache references, React 19 hook/Suspense behavior.
Babel plugin tests use `babel-plugin-tester` and Jest snapshots in:
- `packages/babel-plugin-rn-stylename-inline/__tests__/`
- `packages/babel-plugin-rn-stylename-to-style/__tests__/`
-Many packages currently have placeholder tests that print `No tests yet`.
+The inline plugin test package uses a small TypeScript Jest transformer modeled after Teamplay because Jest cannot otherwise load TS/ESM workspace sources through custom export conditions.
## Maintenance Constraints
-- Treat `__CSS_GLOBAL__`, `__CSS_LOCAL__`, `__hash__`, `__vars`, and `__hasMedia` as cross-package contracts.
-- Keep Babel transform order intact unless the replacement order is tested.
-- Keep runtime import wrappers in `packages/cssxjs/runtime/*` compatible with the named `runtime` import used by the Babel plugin.
-- If selector matching changes, update `matcher` tests and process integration tests together.
-- If CSS variable metadata changes, update both cached and uncached runtime paths.
-- If media-query metadata changes, update dimensions invalidation in cached and uncached runtime paths.
-- If part injection changes, update tests for destructured props, named props, nested render functions, `root`, and dynamic parts.
-- If default style file extensions change, update docs, Babel plugin defaults, Metro expectations, and tests together.
-- Be careful with old package READMEs. Some historical README text still references StartupJS-era names or older defaults; prefer current code and `docs/` for public behavior.
+- Keep `__CSS_GLOBAL__`, `__CSS_LOCAL__`, and the Babel runtime call shape compatible unless both Babel plugins and runtime wrappers change together.
+- Keep compiled sheet IR JSON-serializable.
+- Keep `@cssxjs/css-to-rn` as the single owner of selector matching, value resolution, property transformation, caching, variables, and dimension/media dependency tracking.
+- Do not reintroduce Teamplay or `@nx-js/observer-util` as runtime cache/subscription requirements.
+- Keep Stylus-to-CSS separate from CSS-to-style transformation.
+- For selector or cascade changes, update resolver tests and Babel snapshots as needed.
+- For value syntax changes, update value resolver and transform tests together.
+- For interpolation changes, update inline Babel snapshots and resolver cache tests.
+- For part injection changes, update tests for destructured props, named props, nested render functions, `root`, and dynamic parts.
+- For public API changes, update `docs/`, `AGENTS.md`, and this file.
+- Be careful with historical READMEs and changelogs. Prefer current code, current docs, and this architecture document when they conflict.
diff --git a/docs/api/babel.md b/docs/api/babel.md
index 4c2c979..30dd1c7 100644
--- a/docs/api/babel.md
+++ b/docs/api/babel.md
@@ -2,6 +2,9 @@
CSSX uses a Babel preset to transform styles at build time.
+For CSS strings that are generated in the client at runtime, use the
+[Runtime Compilation API](/api/runtime) instead.
+
## cssxjs/babel
The Babel preset that transforms CSSX syntax.
@@ -21,7 +24,6 @@ module.exports = {
|--------|------|---------|-------------|
| `platform` | `'web'` \| `'ios'` \| `'android'` | `'web'` | Target platform |
| `reactType` | `'react-native'` \| `'web'` | auto | React target type |
-| `cache` | `'teamplay'` | auto | Caching library |
| `transformPug` | `boolean` | `true` | Enable Pug transformation |
| `transformCss` | `boolean` | `true` | Enable CSS transformation |
@@ -32,8 +34,7 @@ module.exports = {
module.exports = {
presets: [
['cssxjs/babel', {
- transformPug: false, // Disable pug if not using it
- cache: 'teamplay' // Force teamplay caching
+ transformPug: false // Disable pug if not using it
}]
]
}
@@ -61,7 +62,7 @@ You can also set platform-specific variables in your Stylus code:
## Caching
-When `cache: 'teamplay'` is set (or auto-detected), the Babel transform generates code that integrates with [teamplay](https://github.com/startupjs/teamplay) for optimized style memoization.
+CSSX uses the built-in resolver cache by default.
See the [Caching guide](/guide/caching) for more details.
@@ -103,6 +104,7 @@ The Babel preset converts this into optimized runtime code that:
- Compiles Stylus to style objects at build time
- Connects `styleName` to the compiled styles
- Injects part style props automatically
+- Re-renders only when used CSS variables or matching media queries change
## TypeScript
diff --git a/docs/api/css.md b/docs/api/css.md
index 3182fdb..96f0b58 100644
--- a/docs/api/css.md
+++ b/docs/api/css.md
@@ -122,6 +122,39 @@ The custom `u` unit works in `css` too:
}
```
+Variables can appear anywhere CSS allows `var()`: whole values, parts of
+shorthands, comma-separated value chunks, and nested fallbacks.
+
+```css
+.card {
+ box-shadow: var(--shadow, 0 4px 12px rgba(0, 0, 0, 0.16));
+ border: var(--border-width, 1px) solid var(--border-color, #ddd);
+}
+```
+
+### JavaScript Interpolation
+
+Function-scoped `css` templates support JavaScript interpolation in CSS value
+positions:
+
+```jsx
+function Badge({ color, size }) {
+ return
+
+ css`
+ .badge {
+ background-color: ${color};
+ padding: ${size}px 12px;
+ }
+ `
+}
+```
+
+Interpolation is an alternative to `var()`. It is only supported in the same
+places a CSS value can use `var()`, and only inside function-scoped JS tagged
+templates. Module-level templates, imported CSS files, and runtime CSS strings
+must use plain CSS text.
+
### Part Selectors
```css
@@ -134,6 +167,46 @@ The custom `u` unit works in `css` too:
}
```
+### Hover and Active Styles
+
+CSSX maps `:hover` and `:active` to the same output as `:part(hover)` and
+`:part(active)`. Components can receive those props as `hoverStyle` and
+`activeStyle`.
+
+```css
+.button:hover {
+ background-color: #0056b3;
+}
+
+.button:active {
+ transform: scale(0.97);
+}
+```
+
+### Filters and Background Images
+
+React Native supports `filter` and experimental background gradients in current
+versions. CSSX passes `filter` through and maps `background-image` to
+`experimental_backgroundImage` on React Native.
+
+```css
+.hero {
+ filter: blur(8px) brightness(0.8);
+ background-image:
+ linear-gradient(0deg, white, rgba(238, 64, 53, 0.8), rgba(238, 64, 53, 0) 70%),
+ radial-gradient(circle, rgba(0, 0, 0, 0.2), transparent 70%);
+}
+```
+
+Only `linear-gradient()` and `radial-gradient()` background images are emitted
+for React Native. Other image values are ignored with a diagnostic.
+
+### Runtime CSS Strings
+
+For CSS text that is generated at runtime, use the
+[Runtime Compilation API](/api/runtime). Runtime strings must be plain CSS text
+and use `var()` for dynamic values.
+
## Limitations
The `css` template does **not** support:
@@ -141,6 +214,7 @@ The `css` template does **not** support:
- Stylus variables (`$var`)
- Stylus mixins
- Global `styles/index.styl` imports
+- JavaScript interpolation in module-level templates or runtime CSS strings
For these features, use the [styl template](/api/styl) instead.
@@ -154,9 +228,12 @@ For these features, use the [styl template](/api/styl) instead.
| Global imports | `styles/index.styl` | Not supported |
| `u` unit | Yes | Yes |
| CSS variables | Yes | Yes |
+| Function-scoped JS interpolation | Yes | Yes |
| Part selectors | Yes | Yes |
+| Runtime CSS strings | No | [Runtime API](/api/runtime) |
## See Also
- [styl Template](/api/styl) — Stylus syntax with variables and mixins
- [styleName Prop](/api/jsx-props) — Connect elements to styles
+- [Runtime Compilation](/api/runtime) — Compile generated CSS strings
diff --git a/docs/api/index.md b/docs/api/index.md
index 2502ccf..4e2ffdb 100644
--- a/docs/api/index.md
+++ b/docs/api/index.md
@@ -12,8 +12,12 @@ import {
variables,
setDefaultVariables,
defaultVariables,
- dimensions,
- matcher
+ cssx,
+ useRuntimeCss,
+ useCssxSheet,
+ useCssxTemplate,
+ CssxProvider,
+ configureCssx
} from 'cssxjs'
```
@@ -28,6 +32,8 @@ import {
- [styl() Function](/api/styl-function) — Apply styles via spread
- [JSX Props](/api/jsx-props) — `styleName`, `part`
- [CSS Variables](/api/variables) — Runtime theming
+- [Runtime Compilation](/api/runtime) — Compile generated CSS strings at runtime
+- [Caching](/guide/caching) — Built-in resolver cache behavior
**Configuration:**
- [Babel Config](/api/babel) — Preset options
@@ -43,5 +49,9 @@ import {
| `variables` | Observable object | Set CSS variable values at runtime |
| `setDefaultVariables` | Function | Set default CSS variable values |
| `defaultVariables` | Object | Read-only default variable values |
-| `dimensions` | Observable object | Current screen width for media queries |
-| `matcher` | Function | Internal style matching (advanced) |
+| `cssx` | Function | Resolve a runtime sheet and `styleName` to props |
+| `useRuntimeCss` | Hook | Compile runtime CSS text into a tracked sheet |
+| `useCssxSheet` | Hook | Track an already compiled sheet |
+| `useCssxTemplate` | Hook | Track a compiled sheet with interpolation values |
+| `CssxProvider` | Component | Provide runtime options to a subtree |
+| `configureCssx` | Function | Configure global runtime defaults |
diff --git a/docs/api/jsx-props.md b/docs/api/jsx-props.md
index b234ca5..9debcde 100644
--- a/docs/api/jsx-props.md
+++ b/docs/api/jsx-props.md
@@ -66,26 +66,30 @@ The pattern:
### Dynamic Styles
-For truly dynamic values, combine `styleName` with the `style` prop:
+For CSS values that come from props, prefer function-scoped template
+interpolation:
```jsx
import { View, Text } from 'react-native'
-function ProgressBar({ progress }) {
+function ProgressBar({ progress, color }) {
return (
-
+ {progress}%
)
styl`
.bar
+ width ${progress}%
height 20px
- background-color #4caf50
+ background-color ${color}
`
}
```
+For ad hoc overrides, combine `styleName` with the regular `style` prop.
+
---
## part
@@ -128,26 +132,24 @@ See the [Component Parts guide](/guide/component-parts) for detailed examples.
---
-## matcher
+## cssx()
-The internal function that matches `styleName` values against compiled styles. Advanced use only.
+The low-level runtime helper that resolves a compiled or runtime sheet and
+returns props to spread onto a component. Most components should use
+`styleName`; use `cssx()` when CSS arrives as a runtime string or when a custom
+component cannot use the Babel transform.
**Signature:**
```ts
-function matcher(
- styleName: string,
- fileStyles: object,
- globalStyles: object,
- localStyles: object,
- inlineStyleProps: object
+function cssx(
+ styleName: string | array | object,
+ sheet: string | CompiledCssSheet | TrackedCssxSheet,
+ inlineStyleProps?: object
): object
```
-**Parameters:**
-- `styleName` - Space-separated class names (supports classnames-like syntax)
-- `fileStyles` - Styles from the imported CSS file
-- `globalStyles` - Module-level `styl` styles
-- `localStyles` - Function-level `styl` styles
-- `inlineStyleProps` - Inline style overrides
+`cssx()` returns an object with `style` and any part style props such as
+`titleStyle`, `hoverStyle`, or `activeStyle`.
-**Returns:** An object with style props, including `style` and any `{part}Style` props.
+See [Runtime Compilation](/api/runtime) for generated CSS strings, diagnostics,
+tracking, and caching behavior.
diff --git a/docs/api/runtime.md b/docs/api/runtime.md
new file mode 100644
index 0000000..d07196b
--- /dev/null
+++ b/docs/api/runtime.md
@@ -0,0 +1,203 @@
+# Runtime Compilation
+
+Runtime compilation is for CSS text that is not known during Babel compilation,
+for example CSS generated by an AI system, loaded from a CMS, or edited inside a
+client-side builder.
+
+Most app code should still use `styleName` with `css` or `styl` templates. Use
+the runtime API when the CSS source is a string at render time.
+
+## Basic Usage
+
+```jsx
+import { cssx, useRuntimeCss } from 'cssxjs'
+
+function Button({ generatedCss, disabled, label }) {
+ const sheet = useRuntimeCss(generatedCss)
+
+ return (
+
+ {label}
+
+ )
+}
+```
+
+`useRuntimeCss()` compiles the string into a tracked sheet. `cssx()` resolves a
+`styleName` against that sheet and returns props such as `style`, `labelStyle`,
+`hoverStyle`, and `activeStyle`.
+
+## CSS Input
+
+Runtime input must be plain CSS text:
+
+```css
+.root {
+ padding: 12px 16px;
+ background: var(--button-bg, #1677ff);
+}
+
+.root.disabled {
+ opacity: 0.5;
+}
+
+.label {
+ color: var(--label-color, white);
+}
+```
+
+Runtime strings do not support Stylus syntax or JavaScript template
+interpolation. Use `var()` for dynamic values in generated CSS.
+
+## API
+
+```ts
+useRuntimeCss(cssText, options?)
+cssx(styleName, sheet, inlineStyleProps?, options?)
+```
+
+`styleName` accepts the same shapes as the JSX prop:
+
+```jsx
+cssx('card', sheet)
+cssx(['card', variant, { selected, disabled }], sheet)
+```
+
+`sheet` can be:
+
+- the `TrackedCssxSheet` returned by `useRuntimeCss()`
+- an already compiled sheet passed through `useCssxSheet()`
+- an array of sheets, ordered from lowest to highest priority
+
+`inlineStyleProps` uses the same prop names that components receive:
+
+```jsx
+
+```
+
+Inline styles have the highest priority.
+
+## Diagnostics
+
+Runtime compilation is graceful by default. Invalid CSS does not throw during
+render. The returned sheet contains diagnostics and any rules that could still
+be compiled.
+
+```jsx
+const sheet = useRuntimeCss(generatedCss)
+
+if (sheet.getSheet().diagnostics.length > 0) {
+ reportCssErrors(sheet.getSheet().diagnostics)
+}
+```
+
+Diagnostics include a severity, code, message, and line/column when available.
+This makes runtime compilation suitable for AI-generated CSS because the app can
+show or feed back errors without crashing.
+
+Build-time template compilation is stricter where Babel needs the module to be
+compiled correctly.
+
+## Variables And Updates
+
+Runtime CSS supports `var()` in the same places as build-time CSSX styles:
+whole values, parts of shorthands, comma-separated chunks, nested fallbacks, and
+complex values such as shadows and gradients.
+
+```css
+.card {
+ border: var(--border-width, 1px) solid var(--border-color, #ddd);
+ box-shadow: var(--shadow, 0 4px 12px rgba(0, 0, 0, 0.16));
+}
+```
+
+Only variables used by the resolved element are tracked. If `--border-color`
+changes, elements that used it update. If an unrelated variable changes, they do
+not.
+
+## Media Queries
+
+Runtime CSS can use media queries:
+
+```css
+.layout {
+ padding: 24px;
+}
+
+@media (max-width: 640px) {
+ .layout {
+ padding: 12px;
+ }
+}
+```
+
+CSSX subscribes only to media queries used by committed renders. Dimension and
+media updates invalidate only affected elements.
+
+## Caching
+
+`useRuntimeCss()` recompiles only when the CSS string or target changes.
+`cssx()` caches the resolved props for the current inputs:
+
+- sheet identity and content hash
+- normalized `styleName`
+- runtime variable and media dependencies actually used
+- interpolation values for compiled templates
+- `JSON.stringify()` hash of inline style props
+
+When those inputs are unchanged, CSSX returns the same object references. When
+inputs change, it recalculates and replaces the previous cached entry instead of
+keeping unbounded variants.
+
+## Other Runtime Hooks
+
+Use these helpers for lower-level integrations:
+
+```ts
+useCssxSheet(compiledSheet, options?)
+useCssxTemplate(compiledSheet, values, options?)
+useCssxLayer(input, options?)
+CssxProvider
+configureCssx(options)
+```
+
+`useCssxSheet()` tracks an already compiled sheet. `useCssxTemplate()` is used by
+compiled local templates with JavaScript interpolation values. `useCssxLayer()`
+accepts strings, compiled sheets, tracked sheets, or layer objects and returns
+the tracked equivalent.
+
+`CssxProvider` and `configureCssx()` configure runtime defaults such as target
+and dimension debounce behavior.
+
+## Platform Resolution
+
+Import from `cssxjs` in application code:
+
+```js
+import { cssx, useRuntimeCss } from 'cssxjs'
+```
+
+CSSX resolves the correct web or React Native runtime through package export
+conditions. Expo and React Native use the React Native target; other bundlers
+use the web target by default.
+
+## When Not To Use It
+
+Use build-time `css` or `styl` templates when the CSS is authored in source
+files. Babel can then precompile the sheet, lower JavaScript interpolation, and
+connect `styleName` automatically.
+
+Runtime compilation is best reserved for CSS that truly arrives as data.
+
+## See Also
+
+- [Babel Config](/api/babel) - Build-time compilation
+- [css Template](/api/css) - Plain CSS templates
+- [JSX Props](/api/jsx-props) - `styleName`, `part`, and `cssx()`
+- [Caching](/guide/caching) - Resolver cache behavior
+- [CSS Variables](/api/variables) - Runtime theming
diff --git a/docs/api/styl.md b/docs/api/styl.md
index 5a3974a..96f2b40 100644
--- a/docs/api/styl.md
+++ b/docs/api/styl.md
@@ -208,6 +208,28 @@ CSSX adds a custom `u` unit where `1u = 8px` (Material Design grid):
See [CSS Variables](/api/variables) for runtime variable updates.
+### JavaScript Interpolation
+
+Function-scoped `styl` templates support JavaScript interpolation in CSS value
+positions:
+
+```jsx
+function Button({ color, spacing }) {
+ return
+
+ styl`
+ .button
+ background ${color}
+ padding ${spacing}px 12px
+ `
+}
+```
+
+Interpolation is lowered through the same runtime value path as `var()`, so it
+can be used for whole values, parts of shorthands, and values nested inside
+functions. It is not supported in module-level templates because there is no
+render-time value array there.
+
## Selectors
| Selector | Description |
@@ -216,10 +238,12 @@ See [CSS Variables](/api/variables) for runtime variable updates.
| `.class1.class2` | Multiple classes (same element) |
| `&.modifier` | Modifier class (used within parent) |
| `:part(name)` | Part selector |
+| `:hover` | Emits `hoverStyle`, same as `:part(hover)` |
+| `:active` | Emits `activeStyle`, same as `:part(active)` |
> **Note:** Descendant selectors (`.parent .child`) are not supported. Apply modifiers directly to each element that needs styling.
-> **Note:** Pseudo-classes (`:hover`, `:focus`, `:active`, etc.) and pseudo-elements (`::before`, `::after`) are not supported. Use state-based modifiers instead (e.g., `&.focused`, `&.active`).
+> **Note:** `:focus`, other pseudo-classes, and pseudo-elements (`::before`, `::after`) are not supported. Use state-based modifiers for those cases.
### Part Selector
@@ -248,12 +272,13 @@ When the same property is defined in multiple places (highest to lowest):
## Limitations
-- No expression interpolations: `` styl`color ${color}` `` is not allowed
-- Must be a plain template literal
-- For dynamic values, use CSS variables or the `style` prop
+- JavaScript interpolation is local-only: module-level `styl` templates must be plain template literals
+- Interpolation is value-only, not selector or property-name interpolation
+- For runtime-generated plain CSS strings, use the [Runtime Compilation API](/api/runtime)
## See Also
- [css Template](/api/css) — Plain CSS alternative
- [styl() Function](/api/styl-function) — Apply styles via spread
- [styleName Prop](/api/jsx-props) — Connect elements to styles
+- [Runtime Compilation](/api/runtime) — Compile generated CSS strings
diff --git a/docs/api/variables.md b/docs/api/variables.md
index d3a25b8..27ef2bc 100644
--- a/docs/api/variables.md
+++ b/docs/api/variables.md
@@ -6,7 +6,7 @@ CSSX provides a reactive system for CSS variables that works at runtime.
A reactive object for setting CSS variable values at runtime. Assigning values triggers automatic re-renders in components using those variables.
-**Type:** `Observable>`
+**Type:** `Record`
```jsx
import { variables } from 'cssxjs'
@@ -25,7 +25,8 @@ Object.assign(variables, {
```
**Reactivity:**
-When you assign to `variables`, all components using those CSS variables automatically re-render with the new values.
+When you assign to `variables`, components that used those specific variables in
+their resolved styles automatically re-render with the new values.
```jsx
import { Pressable, Text } from 'react-native'
@@ -81,7 +82,8 @@ setDefaultVariables({
## defaultVariables
-A read-only object containing the default variable values set by `setDefaultVariables`.
+A reactive object containing the default variable values set by
+`setDefaultVariables`.
**Type:** `Record`
@@ -91,24 +93,6 @@ import { defaultVariables } from 'cssxjs'
console.log(defaultVariables['--primary-color']) // '#007bff'
```
----
-
-## dimensions
-
-A reactive object containing the current screen width. Used internally for media query support.
-
-**Type:** `Observable<{ width: number }>`
-
-```jsx
-import { dimensions } from 'cssxjs'
-
-console.log(dimensions.width) // e.g., 375
-```
-
-The `width` property automatically updates when the screen size changes, triggering re-renders in components using media queries.
-
----
-
## Variable Resolution Order
CSS variables resolve in this priority (highest first):
@@ -126,3 +110,11 @@ styl`
color var(--color, green) // Will be 'red'
`
```
+
+`var()` supports nested fallbacks and complex CSS values:
+
+```stylus
+.card
+ box-shadow var(--card-shadow, 0 4px 12px rgba(0, 0, 0, 0.16))
+ border var(--border-width, 1px) solid var(--border-color, #ddd)
+```
diff --git a/docs/guide/animations.md b/docs/guide/animations.md
index b64ce5f..bd3755f 100644
--- a/docs/guide/animations.md
+++ b/docs/guide/animations.md
@@ -323,8 +323,14 @@ CSSX compiles animations in a way Reanimated v4 expects:
This means you write standard CSS and get native-compatible animations automatically.
+Animation and transition declarations use the same value resolver as other CSSX
+styles, so values may use `var()` and local template interpolation wherever CSS
+values are supported. Animation names, keyframe names, and the `@keyframes`
+block structure must remain statically knowable so CSSX can inline the matching
+keyframes for Reanimated.
+
## Next Steps
-- [Caching](/guide/caching) — Performance optimization with teamplay
+- [Caching](/guide/caching) — Built-in style caching
- [Examples](/examples/) — More code examples
- [styl Template](/api/styl) — Full syntax reference
diff --git a/docs/guide/caching.md b/docs/guide/caching.md
index a372bf2..adbe8c3 100644
--- a/docs/guide/caching.md
+++ b/docs/guide/caching.md
@@ -1,59 +1,36 @@
-# Caching with teamplay
+# Caching
-CSSX can cache style computations to improve rendering performance. This is particularly useful when components re-render frequently but their styles don't change.
-
-> **Note:** Caching currently requires the [teamplay](https://github.com/startupjs/teamplay) library. In future versions, CSSX may include built-in caching that works independently.
+CSSX caches resolved style props by default and tracks the runtime dependencies
+used by each element.
## How It Works
-Without caching, CSSX computes styles on every render:
+For Babel-compiled styles, generated code calls the CSSX runtime with the
+compiled sheet and the current `styleName` value. For runtime CSS strings,
+`useRuntimeCss()` compiles the string and wraps the compiled sheet in a tracked
+runtime object.
-```jsx
-import { View, Text } from 'react-native'
+The resolver caches the final props object for the current inputs:
-function Card({ title }) {
- // Style computation runs on EVERY render
- return (
-
- {title}
-
- )
+- the compiled sheet identity and content hash
+- the normalized `styleName`
+- local interpolation values
+- the JSON hash of inline style props
+- only the CSS variables and media queries that were actually used
- styl`
- .card
- padding 16px
- background white
- `
-}
-```
-
-With caching enabled, CSSX memoizes the results:
+When those inputs are unchanged, CSSX returns the same object references for
+`style`, `textStyle`, `hoverStyle`, `activeStyle`, and other part style props.
+That keeps React and React Native from seeing new style objects on every render.
-1. First render: computes and caches the style object
-2. Subsequent renders: returns the cached result instantly
-3. Cache invalidates automatically when:
- - CSS variable values change
- - Screen dimensions change (for media queries)
- - The `styleName` value changes
-
-## Setup
-
-### Step 1: Install teamplay
-
-```bash
-npm install teamplay
-```
+## No Setup Required
-### Step 2: Wrap Components with observer
-
-For caching to work, components using `styleName` must be wrapped with `observer`:
+Use `styleName` normally:
```jsx
-import { observer } from 'teamplay'
import { styl } from 'cssxjs'
import { View, Text } from 'react-native'
-const Card = observer(function Card({ title, children }) {
+function Card({ title, children }) {
return (
{title}
@@ -72,192 +49,90 @@ const Card = observer(function Card({ title, children }) {
.content
color #666
`
-})
-```
-
-That's it! The Babel transform automatically detects `observer` and enables the cached runtime.
-
-## Automatic Detection
-
-CSSX automatically enables caching when it detects `observer` imported from:
-- `teamplay`
-- `startupjs`
-
-No additional configuration is needed.
-
-## Manual Configuration
-
-You can force caching behavior in your Babel config:
-
-```js
-// babel.config.js
-module.exports = {
- presets: [
- ['cssxjs/babel', {
- cache: 'teamplay' // Always use teamplay caching
- }]
- ]
}
```
-## What Gets Cached
-
-The caching system stores:
-- Computed style objects for each unique `styleName` combination
-- Results of CSS variable substitutions
-- Media query evaluations
-
-### Cache Key Components
+The Babel preset inserts the runtime calls for you.
-Each cache entry is keyed by:
-1. The `styleName` value
-2. Current CSS variable values (if styles use `var()`)
-3. Current screen dimensions (if styles use media queries)
-4. Any inline style props
+## Dependency-Aware Updates
-### Automatic Invalidation
-
-The cache invalidates when reactive dependencies change:
+CSSX tracks the specific runtime dependencies used by each resolved element.
+Changing an unrelated variable does not invalidate that element.
```jsx
-import { variables } from 'cssxjs'
-import { observer } from 'teamplay'
-import { View, Text } from 'react-native'
+import { variables, styl } from 'cssxjs'
+import { View } from 'react-native'
-const ThemedCard = observer(function ThemedCard() {
- // Cache invalidates when --card-bg changes
- return (
-
- Themed content
-
- )
+function ThemedCard() {
+ return
styl`
.card
background var(--card-bg, white)
`
-})
+}
-// Later: changing this automatically re-renders affected components
-variables['--card-bg'] = '#f0f0f0'
+variables['--card-bg'] = '#f0f0f0' // ThemedCard updates
+variables['--text-color'] = 'red' // ThemedCard does not update
```
-## Performance Impact
-
-Caching is most beneficial when:
-- Components re-render frequently (lists, animations, form inputs)
-- Styles are complex (many classes, nested selectors)
-- Multiple components share the same styles
-
-Example with a list:
+Variable notifications are batched in a microtask. Media query updates use the
+runtime dimension store and browser media listeners when available, so CSSX only
+rerenders components whose committed media result changed. Web resize handling
+can be configured globally through `configureCssx()`.
```jsx
-import { observer } from 'teamplay'
-import { styl } from 'cssxjs'
-import { View, Text } from 'react-native'
-
-const ListItem = observer(function ListItem({ item, isSelected }) {
- return (
-
- {item.name}
- {item.price}
-
- )
+import { configureCssx } from 'cssxjs'
- styl`
- .item
- flex-direction row
- justify-content space-between
- padding 12px 16px
- border-bottom-width 1px
- border-bottom-color #eee
- &.selected
- background #e3f2fd
- .name
- font-weight 500
- .price
- color #666
- `
+configureCssx({
+ dimensionsDebounceMs: 50
})
-
-// Rendering 1000 items benefits significantly from caching
-function ProductList({ products, selectedId }) {
- return (
-
- {products.map(item => (
-
- ))}
-
- )
-}
```
-## Using with startupjs
+## Runtime CSS Strings
-If you're using the [startupjs](https://github.com/startupjs/startupjs) framework, caching is automatically configured. Just import `observer` from `startupjs`:
+For client-generated CSS, use `useRuntimeCss()` and `cssx()`. Runtime
+compilation has its own API reference covering diagnostics, subscriptions, and
+platform behavior: [Runtime Compilation](/api/runtime).
-```jsx
-import { observer, styl } from 'startupjs'
-import { View, Text } from 'react-native'
+## Inline Style Hashing
-export default observer(function MyComponent() {
- return (
-
- Content
-
- )
+Inline styles are deep-hashed with `JSON.stringify()`. This means callers can
+write natural inline objects without manually memoizing every object:
- styl`
- .box
- padding 16px
- `
-})
+```jsx
+
```
-## Best Practices
+If the inline style values serialize to the same JSON string, the cache can
+reuse the previous result.
-### Wrap All Styled Components
+## Template Interpolation Cache
-For consistent behavior, wrap any component that uses `styleName`:
+Function-scoped `css` and `styl` templates can use JavaScript interpolation in
+CSS value positions:
```jsx
-import { Pressable, Text } from 'react-native'
+function Button({ color }) {
+ return
-// Good: observer wrapper enables caching
-const Button = observer(function Button({ children }) {
- return (
-
- {children}
-
- )
- styl`.button { padding 12px 24px } .text { color white }`
-})
-
-// Without observer: no caching, styles compute every render
-function Button({ children }) {
- return (
-
- {children}
-
- )
- styl`.button { padding 12px 24px } .text { color white }`
+ css`
+ .button {
+ background-color: ${color};
+ }
+ `
}
```
-## Debugging
-
-To verify caching is working, you can check if components are using the teamplay runtime. In development, the imported runtime path will be one of:
-
-- `cssxjs/runtime/react-native-teamplay` (React Native with caching)
-- `cssxjs/runtime/web-teamplay` (Web with caching)
-- `cssxjs/runtime/react-native` (React Native without caching)
-- `cssxjs/runtime/web` (Web without caching)
+Each compiled template has one cache slot for its latest interpolation values.
+If `color` changes, CSSX recalculates the sheet result and replaces the previous
+cached variant instead of keeping every historical value combination.
## Next Steps
-- [Examples](/examples/) - Complete component examples
-- [API Reference](/api/) - Complete API documentation
+- [CSS Variables](/guide/variables) - Runtime theming
+- [Runtime Compilation](/api/runtime) - Generated CSS strings
+- [css Template](/api/css) - Plain CSS templates and interpolation
+- [Animations](/guide/animations) - Reanimated v4 output
diff --git a/docs/guide/index.md b/docs/guide/index.md
index 413f7e8..dc386df 100644
--- a/docs/guide/index.md
+++ b/docs/guide/index.md
@@ -137,7 +137,9 @@ Built-in `u` unit (1u = 8px) for consistent spacing:
### Performance Optimized
-Automatic style caching prevents unnecessary re-renders. With the optional teamplay integration, styles are memoized and only recalculated when dependencies change.
+Automatic style caching prevents unnecessary re-renders. Styles are memoized by
+sheet, `styleName`, inline styles, interpolation values, and only the variables
+or media queries that were actually used.
## How It Works
diff --git a/docs/guide/usage.md b/docs/guide/usage.md
index a10b061..3805d87 100644
--- a/docs/guide/usage.md
+++ b/docs/guide/usage.md
@@ -167,7 +167,26 @@ function Card({ variant, highlighted, compact, children }) {
## Dynamic Values
-For truly dynamic values, combine `styleName` with the `style` prop:
+For component props that should feed CSS values, use JavaScript interpolation in
+function-scoped `css` or `styl` templates:
+
+```jsx
+import { View } from 'react-native'
+
+function ProgressBar({ progress, color }) {
+ return
+
+ styl`
+ .bar
+ height 20px
+ width ${progress}%
+ background ${color}
+ `
+}
+```
+
+Interpolation is supported only in CSS value positions. For ad hoc overrides,
+combine `styleName` with the `style` prop:
```jsx
import { View } from 'react-native'
@@ -185,7 +204,8 @@ function ProgressBar({ progress }) {
}
```
-Or use [CSS Variables](/guide/variables) for runtime theming.
+Use [CSS Variables](/guide/variables) for app-wide runtime theming and shared
+tokens.
## Style Placement
@@ -246,11 +266,15 @@ CSSX runs on React Native, so not all CSS features are available.
| Compound selectors | `.card.featured` | Same element |
| Parent reference `&` | `&.active`, `&.disabled` | `styl` only |
| Part selectors | `:part(icon)`, `:part(text)` | |
+| Hover and active aliases | `:hover`, `:active` | Emits `hoverStyle` and `activeStyle` |
| CSS variables | `var(--color)`, `var(--size, 16px)` | |
+| JavaScript interpolation | ``color ${value}`` | Function-scoped templates only |
| Animations | `animation fadeIn 0.3s ease` | Reanimated v4 components only |
| Keyframes | `@keyframes fadeIn` | Reanimated v4 components only |
| Transitions | `transition background 0.2s` | Reanimated v4 components only |
| Media queries | `@media (min-width: 768px)` | |
+| Filters | `filter blur(8px)` | Current React Native versions |
+| Background gradients | `background-image linear-gradient(...)` | RN emits `experimental_backgroundImage` |
| Most CSS properties | `padding`, `margin`, `flex`, `color`, etc. | |
| Custom `u` unit | `padding 2u` | 1u = 8px |
@@ -260,44 +284,57 @@ CSSX runs on React Native, so not all CSS features are available.
| Feature | Alternative |
|---------|-------------|
-| `:hover` | Use `onPressIn`/`onPressOut` with `&.pressed` modifier |
| `:focus` | Use `onFocus`/`onBlur` with `&.focused` modifier |
-| `:active` | Use state with `&.active` modifier |
| `::before`, `::after` | Use a real element with its own styles |
| Descendant selectors | `.parent .child` — add modifier to child directly |
| Attribute selectors | `[type="text"]` — use class modifiers instead |
| `:first-child`, `:nth-child` | Handle in JS when rendering |
-| `linear-gradient`, `radial-gradient` | Use solid colors or images |
+| URL background images | Use platform image components |
-### Example: Replacing :hover
+### Hover and Active Props
-Instead of `:hover`, track state and use a modifier:
+CSSX emits `hoverStyle` and `activeStyle` for `:hover` and `:active`. Components
+can choose how to apply those props:
```jsx
import { useState } from 'react'
import { Pressable, Text } from 'react-native'
-function Button({ children, onPress }) {
+function InteractiveBox({ style, hoverStyle, activeStyle, children, onPress }) {
+ const [hovered, setHovered] = useState(false)
const [pressed, setPressed] = useState(false)
return (
setHovered(true)}
+ onHoverOut={() => setHovered(false)}
onPressIn={() => setPressed(true)}
onPressOut={() => setPressed(false)}
onPress={onPress}
>
- {children}
+ {children}
)
+}
+
+function Button({ children, onPress }) {
+ return (
+
+ {children}
+
+ )
styl`
.button
background #007bff
- &.pressed
+ &:hover
background #0056b3
+ &:active
+ transform scale(0.97)
+
.text
color white
`
diff --git a/docs/guide/variables.md b/docs/guide/variables.md
index b56eb35..421b8f6 100644
--- a/docs/guide/variables.md
+++ b/docs/guide/variables.md
@@ -90,7 +90,9 @@ function ThemeToggle() {
}
```
-When you assign to `variables`, all components using those variables automatically re-render.
+When you assign to `variables`, components that used those specific variables in
+their resolved styles automatically re-render. Unrelated variable changes do not
+invalidate the component.
## Variable Priority
@@ -112,7 +114,8 @@ styl`
## Using Variables in Complex Values
-Variables work within compound CSS values:
+Variables work within compound CSS values, nested fallbacks, shorthands, and
+comma-separated value chunks:
```jsx
styl`
@@ -122,6 +125,8 @@ styl`
border var(--border-width, 1px) solid var(--border-color, #ddd)
transform translateX(var(--translate-x, 0)) scale(var(--scale, 1))
+
+ background-image var(--hero-gradient, linear-gradient(0deg, white, transparent))
`
```
@@ -303,4 +308,4 @@ setDefaultVariables({
- [Pug Templates](/guide/pug) - Alternative JSX syntax
- [Animations](/guide/animations) - CSS transitions and keyframes
-- [Caching](/guide/caching) - Performance optimization with teamplay
+- [Caching](/guide/caching) - Built-in dependency-aware caching
diff --git a/docs/index.md b/docs/index.md
index e17c67c..6f2ac24 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -30,6 +30,6 @@ features:
details: Built-in 'u' unit (1u = 8px) for consistent spacing following Material Design guidelines
icon: 📐
- title: Performance Caching
- details: Optional style caching with teamplay prevents unnecessary re-renders for optimal performance
+ details: Built-in dependency-aware style caching reuses stable style props without observer wrappers
icon: 🚀
---
diff --git a/example/package.json b/example/package.json
index 4ab3f00..dc7255c 100644
--- a/example/package.json
+++ b/example/package.json
@@ -10,11 +10,12 @@
"@babel/core": "^7.0.0",
"cssxjs": "^0.3.0",
"esbuild": "^0.21.4",
- "react": "^18.3.1",
- "react-dom": "^18.3.1"
+ "react": "19.2.7",
+ "react-dom": "19.2.7"
},
"devDependencies": {
- "@types/react-dom": "^18.3.1",
+ "@types/react": "19.2.17",
+ "@types/react-dom": "19.2.3",
"cli-highlight": "^2.1.11"
}
}
diff --git a/package.json b/package.json
index 1f2af6f..f049cc5 100644
--- a/package.json
+++ b/package.json
@@ -20,15 +20,16 @@
},
"devDependencies": {
"@rspress/core": "^2.0.0",
- "@types/react": "~18.2.45",
+ "@types/react": "19.2.17",
+ "@types/react-dom": "19.2.3",
"eslint": "^9.39.4",
"eslint-plugin-cssxjs": "^0.3.0-alpha.0",
"husky": "^4.3.0",
"lerna": "^9.0.3",
"lint-staged": "^15.2.2",
"neostandard": "^0.13.0",
- "react": "^18.0.0",
- "react-dom": "^18.0.0",
+ "react": "19.2.7",
+ "react-dom": "19.2.7",
"ts-node": "^10.9.2",
"typescript": "^5.1.3"
},
diff --git a/packages/babel-plugin-rn-stylename-inline/__tests__/__snapshots__/index.spec.js.snap b/packages/babel-plugin-rn-stylename-inline/__tests__/__snapshots__/index.spec.js.snap
index a26100c..26612d2 100644
--- a/packages/babel-plugin-rn-stylename-inline/__tests__/__snapshots__/index.spec.js.snap
+++ b/packages/babel-plugin-rn-stylename-inline/__tests__/__snapshots__/index.spec.js.snap
@@ -25,17 +25,88 @@ myCss\`
import React from "react";
import { css as myCss, styl as myStyl, observer } from "cssxjs";
const __CSS_GLOBAL__ = {
- card: {
- color: "red",
- backgroundColor: "green",
+ version: 1,
+ id: "cssx_d20fva",
+ contentHash: "cssx_9sdtvn",
+ rules: [
+ {
+ selector: ".card",
+ classes: ["card"],
+ part: null,
+ specificity: 1,
+ order: 0,
+ media: null,
+ declarations: [
+ {
+ property: "color",
+ value: "red",
+ raw: "color: red",
+ order: 0,
+ line: 3,
+ column: 5,
+ },
+ {
+ property: "background-color",
+ value: "green",
+ raw: "background-color: green",
+ order: 1,
+ line: 4,
+ column: 5,
+ },
+ ],
+ },
+ ],
+ keyframes: {},
+ metadata: {
+ hasVars: false,
+ vars: [],
+ hasMedia: false,
+ hasViewportUnits: false,
+ hasInterpolations: false,
+ hasDynamicRuntimeDependencies: false,
+ hasAnimations: false,
+ hasTransitions: false,
},
- __hash__: 1762191192,
+ diagnostics: [],
+ __hash__: -2145056715,
};
const _localCssInstance = {
- card: {
- color: "#00f",
+ version: 1,
+ id: "cssx_6737ah",
+ contentHash: "cssx_bj97x3",
+ rules: [
+ {
+ selector: ".card",
+ classes: ["card"],
+ part: null,
+ specificity: 1,
+ order: 0,
+ media: null,
+ declarations: [
+ {
+ property: "color",
+ value: "#00f",
+ raw: "color: #00f",
+ order: 0,
+ line: 2,
+ column: 3,
+ },
+ ],
+ },
+ ],
+ keyframes: {},
+ metadata: {
+ hasVars: false,
+ vars: [],
+ hasMedia: false,
+ hasViewportUnits: false,
+ hasInterpolations: false,
+ hasDynamicRuntimeDependencies: false,
+ hasAnimations: false,
+ hasTransitions: false,
},
- __hash__: -1529996446,
+ diagnostics: [],
+ __hash__: 1523967940,
};
export default observer(function Card() {
const __CSS_LOCAL__ = _localCssInstance;
@@ -66,11 +137,50 @@ css\`
import React from "react";
import { css, observer } from "startupjs";
const __CSS_GLOBAL__ = {
- card: {
- color: "red",
- backgroundColor: "green",
+ version: 1,
+ id: "cssx_d20fva",
+ contentHash: "cssx_9sdtvn",
+ rules: [
+ {
+ selector: ".card",
+ classes: ["card"],
+ part: null,
+ specificity: 1,
+ order: 0,
+ media: null,
+ declarations: [
+ {
+ property: "color",
+ value: "red",
+ raw: "color: red",
+ order: 0,
+ line: 3,
+ column: 5,
+ },
+ {
+ property: "background-color",
+ value: "green",
+ raw: "background-color: green",
+ order: 1,
+ line: 4,
+ column: 5,
+ },
+ ],
+ },
+ ],
+ keyframes: {},
+ metadata: {
+ hasVars: false,
+ vars: [],
+ hasMedia: false,
+ hasViewportUnits: false,
+ hasInterpolations: false,
+ hasDynamicRuntimeDependencies: false,
+ hasAnimations: false,
+ hasTransitions: false,
},
- __hash__: 1762191192,
+ diagnostics: [],
+ __hash__: -2145056715,
};
export default observer(function Card() {
return ;
@@ -116,26 +226,126 @@ import React from "react";
import { css, styl } from "cssxjs";
import { View } from "react-native";
const __CSS_GLOBAL__ = {
- active: {
- backgroundColor: "#f00",
+ version: 1,
+ id: "cssx_e4ok3b",
+ contentHash: "cssx_sae16k",
+ rules: [
+ {
+ selector: ".active",
+ classes: ["active"],
+ part: null,
+ specificity: 1,
+ order: 0,
+ media: null,
+ declarations: [
+ {
+ property: "background-color",
+ value: "#f00",
+ raw: "background-color: #f00",
+ order: 0,
+ line: 2,
+ column: 3,
+ },
+ ],
+ },
+ ],
+ keyframes: {},
+ metadata: {
+ hasVars: false,
+ vars: [],
+ hasMedia: false,
+ hasViewportUnits: false,
+ hasInterpolations: false,
+ hasDynamicRuntimeDependencies: false,
+ hasAnimations: false,
+ hasTransitions: false,
},
- __hash__: -1767660834,
+ diagnostics: [],
+ __hash__: 1497110248,
};
const _localCssInstance2 = {
- root: {
- marginTop: 16,
- borderRadius: 8,
+ version: 1,
+ id: "cssx_8a2l4b",
+ contentHash: "cssx_fubghw",
+ rules: [
+ {
+ selector: ".root",
+ classes: ["root"],
+ part: null,
+ specificity: 1,
+ order: 0,
+ media: null,
+ declarations: [
+ {
+ property: "margin-top",
+ value: "16px",
+ raw: "margin-top: 16px",
+ order: 0,
+ line: 3,
+ column: 7,
+ },
+ {
+ property: "border-radius",
+ value: "8px",
+ raw: "border-radius: 8px",
+ order: 1,
+ line: 4,
+ column: 7,
+ },
+ ],
+ },
+ ],
+ keyframes: {},
+ metadata: {
+ hasVars: false,
+ vars: [],
+ hasMedia: false,
+ hasViewportUnits: false,
+ hasInterpolations: false,
+ hasDynamicRuntimeDependencies: false,
+ hasAnimations: false,
+ hasTransitions: false,
},
- __hash__: -1053412432,
+ diagnostics: [],
+ __hash__: 707783542,
};
const _localCssInstance = {
- root: {
- paddingTop: 8,
- paddingRight: 16,
- paddingBottom: 8,
- paddingLeft: 16,
+ version: 1,
+ id: "cssx_94aplp",
+ contentHash: "cssx_j0akch",
+ rules: [
+ {
+ selector: ".root",
+ classes: ["root"],
+ part: null,
+ specificity: 1,
+ order: 0,
+ media: null,
+ declarations: [
+ {
+ property: "padding",
+ value: "8px 16px",
+ raw: "padding: 8px 16px",
+ order: 0,
+ line: 2,
+ column: 3,
+ },
+ ],
+ },
+ ],
+ keyframes: {},
+ metadata: {
+ hasVars: false,
+ vars: [],
+ hasMedia: false,
+ hasViewportUnits: false,
+ hasInterpolations: false,
+ hasDynamicRuntimeDependencies: false,
+ hasAnimations: false,
+ hasTransitions: false,
},
- __hash__: -1823792365,
+ diagnostics: [],
+ __hash__: -1559627094,
};
export default function Card() {
const __CSS_LOCAL__ = _localCssInstance;
@@ -192,26 +402,126 @@ import React from "react";
import { css } from "cssxjs";
import { View } from "react-native";
const __CSS_GLOBAL__ = {
- active: {
- backgroundColor: "red",
+ version: 1,
+ id: "cssx_fgl1hb",
+ contentHash: "cssx_62w8qm",
+ rules: [
+ {
+ selector: ".active",
+ classes: ["active"],
+ part: null,
+ specificity: 1,
+ order: 0,
+ media: null,
+ declarations: [
+ {
+ property: "background-color",
+ value: "red",
+ raw: "background-color: red",
+ order: 0,
+ line: 3,
+ column: 5,
+ },
+ ],
+ },
+ ],
+ keyframes: {},
+ metadata: {
+ hasVars: false,
+ vars: [],
+ hasMedia: false,
+ hasViewportUnits: false,
+ hasInterpolations: false,
+ hasDynamicRuntimeDependencies: false,
+ hasAnimations: false,
+ hasTransitions: false,
},
- __hash__: -1812576046,
+ diagnostics: [],
+ __hash__: -1215509807,
};
const _localCssInstance2 = {
- root: {
- marginTop: 16,
- borderRadius: 8,
+ version: 1,
+ id: "cssx_8a2l4b",
+ contentHash: "cssx_fubghw",
+ rules: [
+ {
+ selector: ".root",
+ classes: ["root"],
+ part: null,
+ specificity: 1,
+ order: 0,
+ media: null,
+ declarations: [
+ {
+ property: "margin-top",
+ value: "16px",
+ raw: "margin-top: 16px",
+ order: 0,
+ line: 3,
+ column: 7,
+ },
+ {
+ property: "border-radius",
+ value: "8px",
+ raw: "border-radius: 8px",
+ order: 1,
+ line: 4,
+ column: 7,
+ },
+ ],
+ },
+ ],
+ keyframes: {},
+ metadata: {
+ hasVars: false,
+ vars: [],
+ hasMedia: false,
+ hasViewportUnits: false,
+ hasInterpolations: false,
+ hasDynamicRuntimeDependencies: false,
+ hasAnimations: false,
+ hasTransitions: false,
},
- __hash__: -1053412432,
+ diagnostics: [],
+ __hash__: 707783542,
};
const _localCssInstance = {
- root: {
- paddingTop: 8,
- paddingRight: 16,
- paddingBottom: 8,
- paddingLeft: 16,
+ version: 1,
+ id: "cssx_e3vlw8",
+ contentHash: "cssx_7lhxx3",
+ rules: [
+ {
+ selector: ".root",
+ classes: ["root"],
+ part: null,
+ specificity: 1,
+ order: 0,
+ media: null,
+ declarations: [
+ {
+ property: "padding",
+ value: "8px 16px",
+ raw: "padding: 8px 16px",
+ order: 0,
+ line: 3,
+ column: 7,
+ },
+ ],
+ },
+ ],
+ keyframes: {},
+ metadata: {
+ hasVars: false,
+ vars: [],
+ hasMedia: false,
+ hasViewportUnits: false,
+ hasInterpolations: false,
+ hasDynamicRuntimeDependencies: false,
+ hasAnimations: false,
+ hasTransitions: false,
},
- __hash__: -1823792365,
+ diagnostics: [],
+ __hash__: -202231319,
};
export default function Card() {
const __CSS_LOCAL__ = _localCssInstance;
@@ -262,20 +572,86 @@ import React from "react";
import { css } from "cssxjs";
import { View } from "react-native";
const __CSS_GLOBAL__ = {
- card: {
- paddingTop: 8,
- paddingRight: 16,
- paddingBottom: 8,
- paddingLeft: 16,
+ version: 1,
+ id: "cssx_5snslk",
+ contentHash: "cssx_c10gwr",
+ rules: [
+ {
+ selector: ".card",
+ classes: ["card"],
+ part: null,
+ specificity: 1,
+ order: 0,
+ media: null,
+ declarations: [
+ {
+ property: "padding",
+ value: "8px 16px",
+ raw: "padding: 8px 16px",
+ order: 0,
+ line: 3,
+ column: 5,
+ },
+ ],
+ },
+ {
+ selector: ".line",
+ classes: ["line"],
+ part: null,
+ specificity: 1,
+ order: 1,
+ media: null,
+ declarations: [
+ {
+ property: "margin-top",
+ value: "16px",
+ raw: "margin-top: 16px",
+ order: 0,
+ line: 6,
+ column: 5,
+ },
+ {
+ property: "border-radius",
+ value: "8px",
+ raw: "border-radius: 8px",
+ order: 1,
+ line: 7,
+ column: 5,
+ },
+ ],
+ },
+ {
+ selector: ".active",
+ classes: ["active"],
+ part: null,
+ specificity: 1,
+ order: 2,
+ media: null,
+ declarations: [
+ {
+ property: "background-color",
+ value: "red",
+ raw: "background-color: red",
+ order: 0,
+ line: 10,
+ column: 5,
+ },
+ ],
+ },
+ ],
+ keyframes: {},
+ metadata: {
+ hasVars: false,
+ vars: [],
+ hasMedia: false,
+ hasViewportUnits: false,
+ hasInterpolations: false,
+ hasDynamicRuntimeDependencies: false,
+ hasAnimations: false,
+ hasTransitions: false,
},
- line: {
- marginTop: 16,
- borderRadius: 8,
- },
- active: {
- backgroundColor: "red",
- },
- __hash__: 1310335761,
+ diagnostics: [],
+ __hash__: -1106277367,
};
export default function Card() {
return (
@@ -312,11 +688,50 @@ css\`
import React from "react";
import { css, observer } from "cssxjs";
const __CSS_GLOBAL__ = {
- card: {
- color: "red",
- backgroundColor: "green",
+ version: 1,
+ id: "cssx_d20fva",
+ contentHash: "cssx_9sdtvn",
+ rules: [
+ {
+ selector: ".card",
+ classes: ["card"],
+ part: null,
+ specificity: 1,
+ order: 0,
+ media: null,
+ declarations: [
+ {
+ property: "color",
+ value: "red",
+ raw: "color: red",
+ order: 0,
+ line: 3,
+ column: 5,
+ },
+ {
+ property: "background-color",
+ value: "green",
+ raw: "background-color: green",
+ order: 1,
+ line: 4,
+ column: 5,
+ },
+ ],
+ },
+ ],
+ keyframes: {},
+ metadata: {
+ hasVars: false,
+ vars: [],
+ hasMedia: false,
+ hasViewportUnits: false,
+ hasInterpolations: false,
+ hasDynamicRuntimeDependencies: false,
+ hasAnimations: false,
+ hasTransitions: false,
},
- __hash__: 1762191192,
+ diagnostics: [],
+ __hash__: -2145056715,
};
export default observer(function Card() {
return ;
@@ -355,20 +770,86 @@ import React from "react";
import { styl } from "cssxjs";
import { View } from "react-native";
const __CSS_GLOBAL__ = {
- card: {
- paddingTop: 8,
- paddingRight: 16,
- paddingBottom: 8,
- paddingLeft: 16,
- },
- line: {
- marginTop: 16,
- borderRadius: 8,
- },
- active: {
- backgroundColor: "#f00",
+ version: 1,
+ id: "cssx_ccmade",
+ contentHash: "cssx_oj63s5",
+ rules: [
+ {
+ selector: ".card",
+ classes: ["card"],
+ part: null,
+ specificity: 1,
+ order: 0,
+ media: null,
+ declarations: [
+ {
+ property: "padding",
+ value: "8px 16px",
+ raw: "padding: 8px 16px",
+ order: 0,
+ line: 2,
+ column: 3,
+ },
+ ],
+ },
+ {
+ selector: ".line",
+ classes: ["line"],
+ part: null,
+ specificity: 1,
+ order: 1,
+ media: null,
+ declarations: [
+ {
+ property: "margin-top",
+ value: "16px",
+ raw: "margin-top: 16px",
+ order: 0,
+ line: 5,
+ column: 3,
+ },
+ {
+ property: "border-radius",
+ value: "8px",
+ raw: "border-radius: 8px",
+ order: 1,
+ line: 6,
+ column: 3,
+ },
+ ],
+ },
+ {
+ selector: ".active",
+ classes: ["active"],
+ part: null,
+ specificity: 1,
+ order: 2,
+ media: null,
+ declarations: [
+ {
+ property: "background-color",
+ value: "#f00",
+ raw: "background-color: #f00",
+ order: 0,
+ line: 9,
+ column: 3,
+ },
+ ],
+ },
+ ],
+ keyframes: {},
+ metadata: {
+ hasVars: false,
+ vars: [],
+ hasMedia: false,
+ hasViewportUnits: false,
+ hasInterpolations: false,
+ hasDynamicRuntimeDependencies: false,
+ hasAnimations: false,
+ hasTransitions: false,
},
- __hash__: 553324671,
+ diagnostics: [],
+ __hash__: -156895245,
};
export default function Card() {
return (
@@ -404,17 +885,321 @@ styl\`
import React from "react";
import { styl, observer } from "cssxjs";
const __CSS_GLOBAL__ = {
- card: {
- color: "#f00",
- backgroundColor: "#008000",
+ version: 1,
+ id: "cssx_5vvl7n",
+ contentHash: "cssx_ask4pp",
+ rules: [
+ {
+ selector: ".card",
+ classes: ["card"],
+ part: null,
+ specificity: 1,
+ order: 0,
+ media: null,
+ declarations: [
+ {
+ property: "color",
+ value: "#f00",
+ raw: "color: #f00",
+ order: 0,
+ line: 2,
+ column: 3,
+ },
+ {
+ property: "background-color",
+ value: "#008000",
+ raw: "background-color: #008000",
+ order: 1,
+ line: 3,
+ column: 3,
+ },
+ ],
+ },
+ ],
+ keyframes: {},
+ metadata: {
+ hasVars: false,
+ vars: [],
+ hasMedia: false,
+ hasViewportUnits: false,
+ hasInterpolations: false,
+ hasDynamicRuntimeDependencies: false,
+ hasAnimations: false,
+ hasTransitions: false,
},
- __hash__: 772349652,
+ diagnostics: [],
+ __hash__: 1421483523,
};
export default observer(function Card() {
return ;
});
+`;
+
+exports[`@cssxjs/babel-plugin-rn-stylename-inline Local css interpolation declared after early return. Should error: Local css interpolation declared after early return. Should error 1`] = `
+
+import React from 'react'
+import { css } from 'cssxjs'
+import { View } from 'react-native'
+import { useThemeColor } from './theme'
+
+export default function Card ({ ready }) {
+ if (!ready) return
+ const color = useThemeColor('primary')
+ return
+
+ css\`
+ .loader {
+ color: \${color};
+ }
+ .root {
+ color: \${color};
+ }
+ \`
+}
+
+ ↓ ↓ ↓ ↓ ↓ ↓
+
+SyntaxError: unknown file: [@cssxjs/babel-plugin-rn-stylename-inline] Interpolated CSS value "color" is not available before the first return that can use local styles.
+Move the declaration before the first styled return, or pass the value through props/CSS variables.
+ 9 | return
+ 10 |
+> 11 | css\`
+ | ^
+ 12 | .loader {
+ 13 | color: \${color};
+ 14 | }
+
+`;
+
+exports[`@cssxjs/babel-plugin-rn-stylename-inline Local css interpolation: Local css interpolation 1`] = `
+
+import React from 'react'
+import { css } from 'cssxjs'
+import { View } from 'react-native'
+import { useThemeColor } from './theme'
+
+export default function Card ({ ready, pad }) {
+ const color = useThemeColor('primary')
+ if (!ready) return
+ return
+
+ css\`
+ .loader {
+ color: \${color};
+ }
+ .root {
+ color: \${color};
+ padding: \${pad} 2u;
+ }
+ \`
+}
+
+ ↓ ↓ ↓ ↓ ↓ ↓
+
+import React from "react";
+import { css } from "cssxjs";
+import { View } from "react-native";
+import { useThemeColor } from "./theme";
+const _localCssInstance = {
+ version: 1,
+ id: "cssx_h9kswq",
+ contentHash: "cssx_ytmomb",
+ rules: [
+ {
+ selector: ".loader",
+ classes: ["loader"],
+ part: null,
+ specificity: 1,
+ order: 0,
+ media: null,
+ declarations: [
+ {
+ property: "color",
+ value: "var(--__cssx_dynamic_0)",
+ raw: "color: var(--__cssx_dynamic_0)",
+ order: 0,
+ dynamicSlots: [0],
+ line: 3,
+ column: 7,
+ },
+ ],
+ },
+ {
+ selector: ".root",
+ classes: ["root"],
+ part: null,
+ specificity: 1,
+ order: 1,
+ media: null,
+ declarations: [
+ {
+ property: "color",
+ value: "var(--__cssx_dynamic_1)",
+ raw: "color: var(--__cssx_dynamic_1)",
+ order: 0,
+ dynamicSlots: [1],
+ line: 6,
+ column: 7,
+ },
+ {
+ property: "padding",
+ value: "var(--__cssx_dynamic_2) 2u",
+ raw: "padding: var(--__cssx_dynamic_2) 2u",
+ order: 1,
+ dynamicSlots: [2],
+ line: 7,
+ column: 7,
+ },
+ ],
+ },
+ ],
+ keyframes: {},
+ metadata: {
+ hasVars: true,
+ vars: ["--__cssx_dynamic_0", "--__cssx_dynamic_1", "--__cssx_dynamic_2"],
+ hasMedia: false,
+ hasViewportUnits: false,
+ hasInterpolations: true,
+ hasDynamicRuntimeDependencies: true,
+ hasAnimations: false,
+ hasTransitions: false,
+ },
+ diagnostics: [],
+ __hash__: -65713861,
+};
+export default function Card({ ready, pad }) {
+ const color = useThemeColor("primary");
+ const __CSS_LOCAL__ = {
+ sheet: _localCssInstance,
+ values: [color, color, pad],
+ };
+ if (!ready) return ;
+ return ;
+}
+
+
+`;
+
+exports[`@cssxjs/babel-plugin-rn-stylename-inline Reachable local css interpolation after early return. Should error: Reachable local css interpolation after early return. Should error 1`] = `
+
+import React from 'react'
+import { css } from 'cssxjs'
+import { View } from 'react-native'
+import { useThemeColor } from './theme'
+
+export default function Card ({ ready }) {
+ if (!ready) return
+ const color = useThemeColor('primary')
+
+ css\`
+ .root {
+ color: \${color};
+ }
+ \`
+
+ return
+}
+
+ ↓ ↓ ↓ ↓ ↓ ↓
+
+SyntaxError: unknown file: [@cssxjs/babel-plugin-rn-stylename-inline] Local css/styl templates must be declared before the first return, unless they are trailing CSSX style blocks at the end of the component.
+Move this template before the first return, or place it after all returns as the final component statement.
+ 8 | const color = useThemeColor('primary')
+ 9 |
+> 10 | css\`
+ | ^
+ 11 | .root {
+ 12 | color: \${color};
+ 13 | }
+
+`;
+
+exports[`@cssxjs/babel-plugin-rn-stylename-inline Reachable local css interpolation before return: Reachable local css interpolation before return 1`] = `
+
+import React from 'react'
+import { css } from 'cssxjs'
+import { View } from 'react-native'
+import { useThemeColor } from './theme'
+
+export default function Card ({ pad }) {
+ const color = useThemeColor('primary')
+
+ css\`
+ .root {
+ color: \${color};
+ padding: \${pad} 2u;
+ }
+ \`
+
+ return
+}
+
+ ↓ ↓ ↓ ↓ ↓ ↓
+
+import React from "react";
+import { css } from "cssxjs";
+import { View } from "react-native";
+import { useThemeColor } from "./theme";
+const _localCssInstance = {
+ version: 1,
+ id: "cssx_fjh55d",
+ contentHash: "cssx_4m17p8",
+ rules: [
+ {
+ selector: ".root",
+ classes: ["root"],
+ part: null,
+ specificity: 1,
+ order: 0,
+ media: null,
+ declarations: [
+ {
+ property: "color",
+ value: "var(--__cssx_dynamic_0)",
+ raw: "color: var(--__cssx_dynamic_0)",
+ order: 0,
+ dynamicSlots: [0],
+ line: 3,
+ column: 7,
+ },
+ {
+ property: "padding",
+ value: "var(--__cssx_dynamic_1) 2u",
+ raw: "padding: var(--__cssx_dynamic_1) 2u",
+ order: 1,
+ dynamicSlots: [1],
+ line: 4,
+ column: 7,
+ },
+ ],
+ },
+ ],
+ keyframes: {},
+ metadata: {
+ hasVars: true,
+ vars: ["--__cssx_dynamic_0", "--__cssx_dynamic_1"],
+ hasMedia: false,
+ hasViewportUnits: false,
+ hasInterpolations: true,
+ hasDynamicRuntimeDependencies: true,
+ hasAnimations: false,
+ hasTransitions: false,
+ },
+ diagnostics: [],
+ __hash__: -1763352586,
+};
+export default function Card({ pad }) {
+ const color = useThemeColor("primary");
+ const __CSS_LOCAL__ = {
+ sheet: _localCssInstance,
+ values: [color, pad],
+ };
+ return ;
+}
+
+
`;
exports[`@cssxjs/babel-plugin-rn-stylename-inline Should remove css and styl from cssxjs import: Should remove css and styl from cssxjs import 1`] = `
diff --git a/packages/babel-plugin-rn-stylename-inline/__tests__/index.spec.js b/packages/babel-plugin-rn-stylename-inline/__tests__/index.spec.js
index 189e584..4161ff7 100644
--- a/packages/babel-plugin-rn-stylename-inline/__tests__/index.spec.js
+++ b/packages/babel-plugin-rn-stylename-inline/__tests__/index.spec.js
@@ -197,6 +197,93 @@ pluginTester({
background-color: green;
}
\`
- `
+ `,
+ 'Local css interpolation': /* js */`
+ import React from 'react'
+ import { css } from 'cssxjs'
+ import { View } from 'react-native'
+ import { useThemeColor } from './theme'
+
+ export default function Card ({ ready, pad }) {
+ const color = useThemeColor('primary')
+ if (!ready) return
+ return
+
+ css\`
+ .loader {
+ color: \${color};
+ }
+ .root {
+ color: \${color};
+ padding: \${pad} 2u;
+ }
+ \`
+ }
+ `,
+ 'Local css interpolation declared after early return. Should error': {
+ error: /Interpolated CSS value "color" is not available before the first return/,
+ code: /* js */`
+ import React from 'react'
+ import { css } from 'cssxjs'
+ import { View } from 'react-native'
+ import { useThemeColor } from './theme'
+
+ export default function Card ({ ready }) {
+ if (!ready) return
+ const color = useThemeColor('primary')
+ return
+
+ css\`
+ .loader {
+ color: \${color};
+ }
+ .root {
+ color: \${color};
+ }
+ \`
+ }
+ `
+ },
+ 'Reachable local css interpolation before return': /* js */`
+ import React from 'react'
+ import { css } from 'cssxjs'
+ import { View } from 'react-native'
+ import { useThemeColor } from './theme'
+
+ export default function Card ({ pad }) {
+ const color = useThemeColor('primary')
+
+ css\`
+ .root {
+ color: \${color};
+ padding: \${pad} 2u;
+ }
+ \`
+
+ return
+ }
+ `,
+ 'Reachable local css interpolation after early return. Should error': {
+ error: /Local css\/styl templates must be declared before the first return/,
+ code: /* js */`
+ import React from 'react'
+ import { css } from 'cssxjs'
+ import { View } from 'react-native'
+ import { useThemeColor } from './theme'
+
+ export default function Card ({ ready }) {
+ if (!ready) return
+ const color = useThemeColor('primary')
+
+ css\`
+ .root {
+ color: \${color};
+ }
+ \`
+
+ return
+ }
+ `
+ }
}
})
diff --git a/packages/babel-plugin-rn-stylename-inline/index.js b/packages/babel-plugin-rn-stylename-inline/index.js
index 8be96ec..e694921 100644
--- a/packages/babel-plugin-rn-stylename-inline/index.js
+++ b/packages/babel-plugin-rn-stylename-inline/index.js
@@ -1,11 +1,11 @@
-const { GLOBAL_NAME, LOCAL_NAME } =
- require('@cssxjs/runtime/constants')
const template = require('@babel/template').default
const parser = require('@babel/parser')
const t = require('@babel/types')
const COMPILERS = require('@cssxjs/loaders/compilers')
const DEFAULT_MAGIC_IMPORTS = ['cssxjs', 'startupjs']
const DEFAULT_PLATFORM = 'web'
+const GLOBAL_NAME = '__CSS_GLOBAL__'
+const LOCAL_NAME = '__CSS_LOCAL__'
const buildConst = template(`
const %%variable%% = %%value%%
@@ -29,22 +29,28 @@ const getVisitor = ({ $program, usedCompilers }) => ({
// 0. process only templates which are in usedCompilers (imported from our library)
if (!shouldProcess($this, usedCompilers)) return
- // I. validate template
- validateTemplate($this)
-
const compiler = usedCompilers.get($this.node.tag.name)
+ const { source, expressions } = lowerTemplate($this.node.quasi)
+ const hasExpressions = expressions.length > 0
+
+ // I. find parent function or program
+ const $function = $this.getFunctionParent()
+ if (hasExpressions && !$function) {
+ throw $this.buildCodeFrameError(`
+ [@cssxjs/babel-plugin-rn-stylename-inline] Expression interpolations are supported only inside function-scoped css\`\` and styl\`\` templates.
+ `)
+ }
// II. compile template
- const source = $this.node.quasi.quasis[0]?.value?.raw || ''
const filename = state.file?.opts?.filename
const platform = state.opts?.platform || state.file?.opts?.caller?.platform || DEFAULT_PLATFORM
- const compiledString = compiler(source, filename, { platform })
+ const compiledString = compiler(source, filename, {
+ platform,
+ template: hasExpressions
+ })
const compiledExpression = parser.parseExpression(compiledString)
- // III. find parent function or program
- const $function = $this.getFunctionParent()
-
- // IV. LOCAL. if parent is function -- handle local
+ // III. LOCAL. if parent is function -- handle local
if ($function) {
// 1. define a `const` variable at the top of the file
// with the unique identifier
@@ -54,14 +60,21 @@ const getVisitor = ({ $program, usedCompilers }) => ({
value: compiledExpression
}))
- // 2. reassign this unique identifier to a constant LOCAL_NAME
+ const localValue = hasExpressions
+ ? t.objectExpression([
+ t.objectProperty(t.identifier('sheet'), localIdentifier),
+ t.objectProperty(t.identifier('values'), t.arrayExpression(expressions))
+ ])
+ : localIdentifier
+
+ // 2. reassign this unique identifier or local dynamic layer to a constant LOCAL_NAME
// in the scope of current function
- $function.get('body').unshiftContainer('body', buildConst({
+ insertLocalCss($function, $this, buildConst({
variable: t.identifier(LOCAL_NAME),
- value: localIdentifier
- }))
+ value: localValue
+ }), expressions, usedCompilers)
- // V. GLOBAL. if parent is program -- handle global
+ // IV. GLOBAL. if parent is program -- handle global
} else {
// 1. define a `const` variable at the top of the file
// with the constant GLOBAL_NAME
@@ -71,7 +84,7 @@ const getVisitor = ({ $program, usedCompilers }) => ({
}))
}
- // VI. Remove template expression after processing
+ // V. Remove template expression after processing
$this.remove()
// TODO: Throw error if global styles were already added or
@@ -92,20 +105,166 @@ function insertAfterImports ($program, expressionStatement) {
}
}
+function insertLocalCss ($function, $template, statement, expressions, usedCompilers) {
+ const $body = $function.get('body')
+ if (!$body.isBlockStatement()) {
+ $body.replaceWith(t.blockStatement([
+ t.returnStatement($body.node)
+ ]))
+ }
+
+ const $statement = $template.getStatementParent()
+ const $functionBody = $function.get('body')
+ const $firstReturn = findFirstReturnStatement($functionBody)
+
+ if ($statement?.parentPath !== $functionBody) {
+ $functionBody.unshiftContainer('body', statement)
+ return
+ }
+
+ const $target = isTrailingStyleTemplateStatement($statement, usedCompilers)
+ ? ($firstReturn || $statement)
+ : $statement
+
+ validateLocalCssPosition($functionBody, $firstReturn, $target, $template)
+
+ validateInterpolationBindings($function, $functionBody, $target, expressions, $template)
+
+ $target.insertBefore(statement)
+}
+
+function validateLocalCssPosition ($functionBody, $firstReturn, $target, $template) {
+ if (!$firstReturn || $target.node !== $template.getStatementParent()?.node) return
+
+ const statements = $functionBody.get('body')
+ const returnIndex = statements.findIndex($statement => $statement.node === $firstReturn.node)
+ const targetIndex = statements.findIndex($statement => $statement.node === $target.node)
+ if (returnIndex < 0 || targetIndex < 0 || targetIndex < returnIndex) return
+
+ throw $template.buildCodeFrameError([
+ '[@cssxjs/babel-plugin-rn-stylename-inline] Local css/styl templates must be declared before the first return, unless they are trailing CSSX style blocks at the end of the component.',
+ 'Move this template before the first return, or place it after all returns as the final component statement.'
+ ].join('\n'))
+}
+
+function isTrailingStyleTemplateStatement ($statement, usedCompilers) {
+ let $current = $statement
+ while ($current?.node) {
+ if (!isStyleTemplateStatement($current, usedCompilers)) return false
+ $current = $current.getNextSibling()
+ }
+ return true
+}
+
+function isStyleTemplateStatement ($statement, usedCompilers) {
+ if (!$statement.isExpressionStatement()) return false
+ const expression = $statement.get('expression')
+ if (!expression.isTaggedTemplateExpression()) return false
+ return shouldProcess(expression, usedCompilers)
+}
+
+function findFirstReturnStatement ($functionBody) {
+ return $functionBody.get('body').find($statement => statementCanReturn($statement))
+}
+
+function statementCanReturn ($statement) {
+ if ($statement.isReturnStatement()) return true
+
+ let canReturn = false
+ $statement.traverse({
+ Function ($nestedFunction) {
+ $nestedFunction.skip()
+ },
+ ReturnStatement ($return) {
+ canReturn = true
+ $return.stop()
+ }
+ })
+ return canReturn
+}
+
+function validateInterpolationBindings ($function, $functionBody, $target, expressions, $template) {
+ if (!$target || expressions.length === 0) return
+
+ const statements = $functionBody.get('body')
+ const targetIndex = statements.findIndex($statement => $statement.node === $target.node)
+ if (targetIndex < 0) return
+
+ for (const name of getReferencedNames(expressions)) {
+ const binding = $template.scope.getBinding(name)
+ if (!binding) continue
+ if (binding.kind === 'module' || binding.kind === 'param' || binding.kind === 'hoisted') continue
+ if (binding.path.getFunctionParent() !== $function) continue
+
+ const $bindingStatement = binding.path.getStatementParent()
+ const bindingIndex = statements.findIndex($statement => $statement.node === $bindingStatement?.node)
+ if (bindingIndex >= 0 && bindingIndex < targetIndex) continue
+
+ throw $template.buildCodeFrameError([
+ `[@cssxjs/babel-plugin-rn-stylename-inline] Interpolated CSS value "${name}" is not available before the first return that can use local styles.`,
+ 'Move the declaration before the first styled return, or pass the value through props/CSS variables.'
+ ].join('\n'))
+ }
+}
+
+function getReferencedNames (expressions) {
+ const names = new Set()
+ for (const expression of expressions) collectReferencedNames(expression, names)
+ return names
+}
+
+function collectReferencedNames (node, names) {
+ if (!node) return
+
+ if (t.isIdentifier(node)) {
+ names.add(node.name)
+ return
+ }
+
+ if (t.isFunction(node)) return
+
+ if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
+ collectReferencedNames(node.object, names)
+ if (node.computed) collectReferencedNames(node.property, names)
+ return
+ }
+
+ if (t.isObjectProperty(node)) {
+ if (node.computed) collectReferencedNames(node.key, names)
+ collectReferencedNames(node.value, names)
+ return
+ }
+
+ const keys = t.VISITOR_KEYS[node.type] || []
+ for (const key of keys) {
+ const value = node[key]
+ if (Array.isArray(value)) {
+ for (const child of value) collectReferencedNames(child, names)
+ } else {
+ collectReferencedNames(value, names)
+ }
+ }
+}
+
function shouldProcess ($template, usedCompilers) {
if (!$template.get('tag').isIdentifier()) return
if (!usedCompilers.has($template.node.tag.name)) return
return true
}
-function validateTemplate ($template) {
- const { node: { quasi } } = $template
+function lowerTemplate (quasi) {
+ let source = ''
+ const expressions = []
- if (quasi.expressions.length > 0) {
- throw $template.buildCodeFrameError(`
- [@cssxjs/babel-plugin-rn-stylename-inline] Expression interpolations are not supported in css\`\` and styl\`\`.
- `)
+ for (let index = 0; index < quasi.quasis.length; index++) {
+ source += quasi.quasis[index]?.value?.raw || ''
+ const expression = quasi.expressions[index]
+ if (!expression) continue
+ source += `var(--__cssx_dynamic_${expressions.length})`
+ expressions.push(expression)
}
+
+ return { source, expressions }
}
function getUsedCompilers ($program, state) {
diff --git a/packages/babel-plugin-rn-stylename-inline/package.json b/packages/babel-plugin-rn-stylename-inline/package.json
index f345aee..2e75d1c 100644
--- a/packages/babel-plugin-rn-stylename-inline/package.json
+++ b/packages/babel-plugin-rn-stylename-inline/package.json
@@ -14,7 +14,7 @@
],
"main": "index.js",
"scripts": {
- "test": "jest"
+ "test": "NO_COLOR=1 FORCE_COLOR=0 NODE_OPTIONS=\"${NODE_OPTIONS:-} --experimental-vm-modules -C cssx-ts\" jest --runInBand"
},
"author": "Pavel Zhukov",
"license": "MIT",
@@ -26,12 +26,25 @@
"@babel/parser": "^7.0.0",
"@babel/template": "^7.4.0",
"@babel/types": "^7.0.0",
- "@cssxjs/loaders": "^0.3.0",
- "@cssxjs/runtime": "^0.3.0"
+ "@cssxjs/loaders": "^0.3.0"
},
"devDependencies": {
"@babel/plugin-syntax-jsx": "^7.0.0",
"babel-plugin-tester": "^9.1.0",
"jest": "^30.0.4"
+ },
+ "jest": {
+ "transform": {
+ "^.+\\.ts$": "./test/ts-transform.cjs"
+ },
+ "testEnvironmentOptions": {
+ "customExportConditions": [
+ "cssx-ts",
+ "node"
+ ]
+ },
+ "extensionsToTreatAsEsm": [
+ ".ts"
+ ]
}
}
diff --git a/packages/babel-plugin-rn-stylename-inline/test/ts-transform.cjs b/packages/babel-plugin-rn-stylename-inline/test/ts-transform.cjs
new file mode 100644
index 0000000..525047c
--- /dev/null
+++ b/packages/babel-plugin-rn-stylename-inline/test/ts-transform.cjs
@@ -0,0 +1,17 @@
+const ts = require('typescript')
+
+module.exports = {
+ process (sourceText, sourcePath) {
+ const result = ts.transpileModule(sourceText, {
+ fileName: sourcePath,
+ compilerOptions: {
+ target: ts.ScriptTarget.ES2022,
+ module: ts.ModuleKind.ESNext,
+ sourceMap: false,
+ inlineSourceMap: false,
+ importsNotUsedAsValues: ts.ImportsNotUsedAsValues.Remove
+ }
+ })
+ return { code: result.outputText }
+ }
+}
diff --git a/packages/babel-plugin-rn-stylename-to-style/README.md b/packages/babel-plugin-rn-stylename-to-style/README.md
index c73dde5..5ca2c3e 100644
--- a/packages/babel-plugin-rn-stylename-to-style/README.md
+++ b/packages/babel-plugin-rn-stylename-to-style/README.md
@@ -131,8 +131,7 @@ so these files shouldn't frequently change.
**Default:** `undefined`
-Whether to use integration with some caching library. Currently supported ones:
-- `"teamplay"`
+Legacy compatibility option. New projects should omit it.
#### `platform`
diff --git a/packages/babel-plugin-rn-stylename-to-style/__tests__/__snapshots__/index.spec.js.snap b/packages/babel-plugin-rn-stylename-to-style/__tests__/__snapshots__/index.spec.js.snap
index 0161a57..a2086bf 100644
--- a/packages/babel-plugin-rn-stylename-to-style/__tests__/__snapshots__/index.spec.js.snap
+++ b/packages/babel-plugin-rn-stylename-to-style/__tests__/__snapshots__/index.spec.js.snap
@@ -18,40 +18,38 @@ function Test ({ items, ...props }) {
↓ ↓ ↓ ↓ ↓ ↓
import _css from "./index.styl";
-import { runtime as _runtime } from "cssxjs/runtime";
+import {
+ runtime as _runtime,
+ useCssxLayer as _useCssxLayer,
+} from "cssxjs/runtime";
const _cssx = _runtime;
function Test({ itemStyle: _itemStyle, style: _style, items, ...props }) {
+ const _local = _useCssxLayer(
+ typeof __CSS_LOCAL__ !== "undefined" && __CSS_LOCAL__
+ );
+ const _global = _useCssxLayer(
+ typeof __CSS_GLOBAL__ !== "undefined" && __CSS_GLOBAL__
+ );
+ const _file__css = _useCssxLayer(_css);
return (
{(() => {
const __pugEachResult = [];
for (const item of items) {
__pugEachResult.push(
{item}
@@ -83,9 +81,19 @@ function Test ({ style, active, submit, disabled }) {
↓ ↓ ↓ ↓ ↓ ↓
import _css from "./index.styl";
-import { runtime as _runtime } from "cssxjs/runtime";
+import {
+ runtime as _runtime,
+ useCssxLayer as _useCssxLayer,
+} from "cssxjs/runtime";
const _cssx = _runtime;
function Test({ style, active, submit, disabled }) {
+ const _local = _useCssxLayer(
+ typeof __CSS_LOCAL__ !== "undefined" && __CSS_LOCAL__
+ );
+ const _global = _useCssxLayer(
+ typeof __CSS_GLOBAL__ !== "undefined" && __CSS_GLOBAL__
+ );
+ const _file__css = _useCssxLayer(_css);
const titleStyle = {
color: "red",
fontWeight: "bold",
@@ -99,39 +107,27 @@ function Test({ style, active, submit, disabled }) {
active,
},
],
- _css,
- typeof __CSS_GLOBAL__ !== "undefined" && __CSS_GLOBAL__,
- typeof __CSS_LOCAL__ !== "undefined" && __CSS_LOCAL__,
+ _file__css,
+ _global,
+ _local,
{
style: style,
}
)}
>
Title
Description
@@ -141,9 +137,9 @@ function Test({ style, active, submit, disabled }) {
submit,
disabled,
},
- _css,
- typeof __CSS_GLOBAL__ !== "undefined" && __CSS_GLOBAL__,
- typeof __CSS_LOCAL__ !== "undefined" && __CSS_LOCAL__,
+ _file__css,
+ _global,
+ _local,
{
style: {
color: "pink",
@@ -176,50 +172,26 @@ function Test () {
↓ ↓ ↓ ↓ ↓ ↓
import _css from "./index.styl";
-import { runtime as _runtime } from "cssxjs/runtime";
+import {
+ runtime as _runtime,
+ useCssxLayer as _useCssxLayer,
+} from "cssxjs/runtime";
const _cssx = _runtime;
function Test() {
+ const _local = _useCssxLayer(
+ typeof __CSS_LOCAL__ !== "undefined" && __CSS_LOCAL__
+ );
+ const _global = _useCssxLayer(
+ typeof __CSS_GLOBAL__ !== "undefined" && __CSS_GLOBAL__
+ );
+ const _file__css = _useCssxLayer(_css);
return (
-
-
- Title
-
-
+
+ Title
+
Description
-
@@ -243,9 +215,19 @@ function Test ({ style, active, submit, disabled, titleStyle }) {
↓ ↓ ↓ ↓ ↓ ↓
import _css from "./index.styl";
-import { runtime as _runtime } from "cssxjs/runtime";
+import {
+ runtime as _runtime,
+ useCssxLayer as _useCssxLayer,
+} from "cssxjs/runtime";
const _cssx = _runtime;
function Test({ style, active, submit, disabled, titleStyle }) {
+ const _local = _useCssxLayer(
+ typeof __CSS_LOCAL__ !== "undefined" && __CSS_LOCAL__
+ );
+ const _global = _useCssxLayer(
+ typeof __CSS_GLOBAL__ !== "undefined" && __CSS_GLOBAL__
+ );
+ const _file__css = _useCssxLayer(_css);
return (
);
@@ -331,20 +307,23 @@ export default observer(Layout)
↓ ↓ ↓ ↓ ↓ ↓
import { observer, useBackPress } from "startupjs";
-import { runtime as _runtime } from "cssxjs/runtime/teamplay";
+import {
+ runtime as _runtime,
+ useCssxLayer as _useCssxLayer,
+} from "cssxjs/runtime/teamplay";
const _cssx = _runtime;
function Layout({ style, children }) {
+ const _local = _useCssxLayer(
+ typeof __CSS_LOCAL__ !== "undefined" && __CSS_LOCAL__
+ );
+ const _global = _useCssxLayer(
+ typeof __CSS_GLOBAL__ !== "undefined" && __CSS_GLOBAL__
+ );
return (
{children}
@@ -377,7 +356,10 @@ function Menu ({ style, children, value, variant, activeBorder, iconPosition, ac
↓ ↓ ↓ ↓ ↓ ↓
import { observer, useBackPress } from "startupjs";
-import { runtime as _runtime } from "cssxjs/runtime/teamplay";
+import {
+ runtime as _runtime,
+ useCssxLayer as _useCssxLayer,
+} from "cssxjs/runtime/teamplay";
const _cssx = _runtime;
function Menu({
style,
@@ -389,30 +371,24 @@ function Menu({
activeColor,
...props
}) {
+ const _local = _useCssxLayer(
+ typeof __CSS_LOCAL__ !== "undefined" && __CSS_LOCAL__
+ );
+ const _global = _useCssxLayer(
+ typeof __CSS_GLOBAL__ !== "undefined" && __CSS_GLOBAL__
+ );
return (