Skip to content

Commit d7d8c7a

Browse files
committed
Strengthen css-to-rn runtime edge coverage
1 parent 4eb3954 commit d7d8c7a

18 files changed

Lines changed: 977 additions & 79 deletions

File tree

.github/workflows/test.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,12 @@ jobs:
2424

2525
- name: Run tests
2626
run: yarn test
27+
28+
- name: Smoke source-condition package imports
29+
run: |
30+
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')"
31+
32+
- name: Smoke built package imports
33+
run: |
34+
yarn workspace @cssxjs/css-to-rn build
35+
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')"

architecture.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,13 @@ The sheet must remain serializable. Cache state, subscriptions, and runtime trac
218218

219219
`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.
220220

221+
Build mode validates static declaration values through the shared value resolver
222+
and property transformer. Unsupported static constructs such as
223+
layout-dependent `calc()` expressions, unsupported transform functions, and
224+
unsupported background images fail during Babel/loader compilation.
225+
Declarations containing `var()` or template slots are deferred to runtime
226+
validation because their final value is not knowable at build time.
227+
221228
Supported selectors:
222229

223230
- `.root`
@@ -303,12 +310,12 @@ Key pieces:
303310
- `store.ts`: `variables`, `defaultVariables`, `setDefaultVariables()`, dimensions/media state, microtask-batched notifications.
304311
- `tracker.ts`: `TrackedCssxSheet`, committed dependency snapshots, per-tracker cache.
305312
- `cssx.ts`: ergonomic `cssx()` wrapper that delegates to `resolveCssx()` and records dependencies into tracked sheets during render.
306-
- `hooks.ts`: `useCssxSheet()`, `useCompiledCss()`, `useCssxTemplate()`.
313+
- `hooks.ts`: `useCssxSheet()`, `useCompiledCss()`, `useCssxTemplate()`, `useCssxLayer()`.
307314
- `config.ts`: optional `CssxProvider`, `configureCssx()`, and `useCssxConfig()`.
308315

309316
`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.
310317

311-
Variable writes and deletes notify subscribers once per microtask. Subscribers only rerender when a variable they actually used changes. Media and viewport-unit subscribers are tied to dimension changes. Web resize uses leading plus trailing debounced updates.
318+
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.
312319

313320
## Loaders And Separate Files
314321

@@ -392,7 +399,7 @@ cd packages/babel-plugin-rn-stylename-to-style && yarn test
392399
`@cssxjs/css-to-rn` tests:
393400

394401
- `test/engine/**`: parser IR, value resolution, property transforms, resolver cascade, cache behavior.
395-
- `test/react/**`: variable batching, dependency tracking, aborted-render safety, tracked cache references.
402+
- `test/react/**`: variable batching, dependency tracking, media adapter invalidation, aborted-render safety, tracked cache references, React 19 hook/Suspense behavior.
396403

397404
Babel plugin tests use `babel-plugin-tester` and Jest snapshots in:
398405

docs/guide/caching.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,9 @@ variables['--text-color'] = 'red' // ThemedCard does not update
7676
```
7777

7878
Variable notifications are batched in a microtask. Media query updates use the
79-
runtime dimension store, and web resize handling can be configured globally
80-
through `configureCssx()`.
79+
runtime dimension store and browser media listeners when available, so CSSX only
80+
rerenders components whose committed media result changed. Web resize handling
81+
can be configured globally through `configureCssx()`.
8182

8283
```jsx
8384
import { configureCssx } from 'cssxjs'

example/package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@
1010
"@babel/core": "^7.0.0",
1111
"cssxjs": "^0.3.0",
1212
"esbuild": "^0.21.4",
13-
"react": "^18.3.1",
14-
"react-dom": "^18.3.1"
13+
"react": "19.2.7",
14+
"react-dom": "19.2.7"
1515
},
1616
"devDependencies": {
17-
"@types/react-dom": "^18.3.1",
17+
"@types/react": "19.2.17",
18+
"@types/react-dom": "19.2.3",
1819
"cli-highlight": "^2.1.11"
1920
}
2021
}

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,16 @@
2020
},
2121
"devDependencies": {
2222
"@rspress/core": "^2.0.0",
23-
"@types/react": "~18.2.45",
23+
"@types/react": "19.2.17",
24+
"@types/react-dom": "19.2.3",
2425
"eslint": "^9.39.4",
2526
"eslint-plugin-cssxjs": "^0.3.0-alpha.0",
2627
"husky": "^4.3.0",
2728
"lerna": "^9.0.3",
2829
"lint-staged": "^15.2.2",
2930
"neostandard": "^0.13.0",
30-
"react": "^18.0.0",
31-
"react-dom": "^18.0.0",
31+
"react": "19.2.7",
32+
"react-dom": "19.2.7",
3233
"ts-node": "^10.9.2",
3334
"typescript": "^5.1.3"
3435
},

packages/css-to-rn/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,14 @@
6565
}
6666
},
6767
"devDependencies": {
68+
"@types/jsdom": "^28.0.3",
6869
"@types/node": "^22.8.1",
70+
"@types/react": "19.2.17",
71+
"@types/react-dom": "19.2.3",
72+
"jsdom": "^29.1.1",
6973
"mocha": "^8.4.0",
74+
"react": "19.2.7",
75+
"react-dom": "19.2.7",
7076
"typescript": "^6.0.3"
7177
},
7278
"license": "MIT"

packages/css-to-rn/src/compiler.ts

Lines changed: 79 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import valueParser from 'postcss-value-parser'
44
import { addDiagnostic, diagnostic } from './diagnostics.ts'
55
import { cssxHash } from './hash.ts'
66
import { parseSelector } from './selectors.ts'
7+
import { transformDeclarations } from './transform/index.ts'
8+
import { resolveCssValue } from './values.ts'
79
import type {
810
CompileCssOptions,
911
CompileCssTemplateOptions,
@@ -13,7 +15,8 @@ import type {
1315
CssxDiagnostic,
1416
CssxKeyframe,
1517
CssxMetadata,
16-
CssxRule
18+
CssxRule,
19+
CssxTarget
1720
} from './types.ts'
1821

1922
const VAR_RE = /var\(\s*(--[A-Za-z0-9_-]+)/
@@ -93,7 +96,7 @@ function compileCssInternal (
9396
for (const rule of ast.stylesheet?.rules ?? []) {
9497
if (rule.type === 'rule') {
9598
const styleRule = rule as CssStyleRuleAst
96-
compileRuleList(styleRule.selectors ?? [], styleRule.declarations ?? [], null, rules, state, orderRef(() => order++), isTemplate, exports)
99+
compileRuleList(styleRule.selectors ?? [], styleRule.declarations ?? [], null, rules, state, orderRef(() => order++), isTemplate, exports, options.target)
97100
continue
98101
}
99102

@@ -104,7 +107,7 @@ function compileCssInternal (
104107
if (!mediaIsValid && state.mode === 'build') continue
105108
for (const child of mediaRule.rules ?? []) {
106109
if (child.type !== 'rule') continue
107-
compileRuleList(child.selectors ?? [], child.declarations ?? [], media, rules, state, orderRef(() => order++), isTemplate, exports)
110+
compileRuleList(child.selectors ?? [], child.declarations ?? [], media, rules, state, orderRef(() => order++), isTemplate, exports, options.target)
108111
}
109112
continue
110113
}
@@ -113,7 +116,7 @@ function compileCssInternal (
113116
const keyframesRule = rule as CssKeyframesAst
114117
const name = keyframesRule.name
115118
if (!name) continue
116-
keyframes[name] = compileKeyframes(keyframesRule, state, orderRef(() => order++), isTemplate)
119+
keyframes[name] = compileKeyframes(keyframesRule, state, orderRef(() => order++), isTemplate, options.target)
117120
continue
118121
}
119122

@@ -149,8 +152,11 @@ function compileRuleList (
149152
state: CompileState,
150153
nextOrder: () => number,
151154
isTemplate: boolean,
152-
exports: Record<string, string>
155+
exports: Record<string, string>,
156+
target: CssxTarget | undefined
153157
): void {
158+
let compiledDeclarations: CssxDeclaration[] | undefined
159+
154160
for (const selector of selectors) {
155161
if (selector === ':export') {
156162
compileExports(declarations, exports, state, isTemplate)
@@ -172,6 +178,7 @@ function compileRuleList (
172178
continue
173179
}
174180
if (!parsed.result) continue
181+
compiledDeclarations ??= compileDeclarations(declarations, state, isTemplate, target)
175182

176183
output.push({
177184
selector: parsed.result.selector,
@@ -180,7 +187,7 @@ function compileRuleList (
180187
specificity: parsed.result.specificity,
181188
order: nextOrder(),
182189
media,
183-
declarations: compileDeclarations(declarations, state, isTemplate)
190+
declarations: compiledDeclarations
184191
})
185192
}
186193
}
@@ -209,7 +216,8 @@ function compileExports (
209216
function compileDeclarations (
210217
declarations: CssDeclarationAst[],
211218
state: CompileState,
212-
isTemplate: boolean
219+
isTemplate: boolean,
220+
target: CssxTarget | undefined
213221
): CssxDeclaration[] {
214222
const output: CssxDeclaration[] = []
215223
let order = 0
@@ -231,15 +239,18 @@ function compileDeclarations (
231239
}
232240

233241
const dynamicSlots = isTemplate ? getDynamicSlots(value) : undefined
234-
output.push({
242+
const compiledDeclaration: CssxDeclaration = {
235243
property,
236244
value,
237245
raw: `${property}: ${value}`,
238246
order: order++,
239247
dynamicSlots,
240248
line: declaration.position?.start?.line,
241249
column: declaration.position?.start?.column
242-
})
250+
}
251+
252+
validateBuildDeclaration(compiledDeclaration, state, target)
253+
output.push(compiledDeclaration)
243254
}
244255

245256
return output
@@ -249,19 +260,76 @@ function compileKeyframes (
249260
rule: CssKeyframesAst,
250261
state: CompileState,
251262
nextOrder: () => number,
252-
isTemplate: boolean
263+
isTemplate: boolean,
264+
target: CssxTarget | undefined
253265
): CssxKeyframe[] {
254266
const output: CssxKeyframe[] = []
255267
for (const frame of rule.keyframes ?? []) {
256268
output.push({
257269
selector: (frame.values ?? []).join(', '),
258-
declarations: compileDeclarations(frame.declarations ?? [], state, isTemplate),
270+
declarations: compileDeclarations(frame.declarations ?? [], state, isTemplate, target),
259271
order: nextOrder()
260272
})
261273
}
262274
return output
263275
}
264276

277+
function validateBuildDeclaration (
278+
declaration: CssxDeclaration,
279+
state: CompileState,
280+
target: CssxTarget | undefined
281+
): void {
282+
if (state.mode !== 'build') return
283+
284+
if (
285+
declaration.dynamicSlots?.length ||
286+
declaration.value.includes('var(')
287+
) {
288+
return
289+
}
290+
291+
const position = {
292+
line: declaration.line,
293+
column: declaration.column
294+
}
295+
const resolved = resolveCssValue(declaration.value, {
296+
dimensions: {
297+
width: 100,
298+
height: 100
299+
}
300+
})
301+
302+
if (!resolved.valid) {
303+
for (const item of resolved.diagnostics) {
304+
addDiagnostic(state, diagnostic(
305+
item.code,
306+
item.message,
307+
'error',
308+
position
309+
))
310+
}
311+
return
312+
}
313+
314+
const transformed = transformDeclarations([{
315+
property: declaration.property,
316+
value: resolved.value,
317+
raw: `${declaration.property}: ${resolved.value}`,
318+
order: declaration.order
319+
}], {
320+
platform: target ?? 'react-native'
321+
})
322+
323+
for (const item of transformed.diagnostics) {
324+
addDiagnostic(state, diagnostic(
325+
item.code,
326+
item.message,
327+
'error',
328+
position
329+
))
330+
}
331+
}
332+
265333
function validateMedia (
266334
rule: CssMediaAst,
267335
state: CompileState,

packages/css-to-rn/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export type {
3737
CssxCache,
3838
CssxDimensions,
3939
CssxLayerInput,
40+
CssxMediaQueryEvaluator,
4041
InlineStyleInput,
4142
ResolveCssxDependencies,
4243
ResolveCssxLayer,

packages/css-to-rn/src/react-native.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from './react/tracker.ts'
2121
import {
2222
configureDimensionsAdapter,
23+
configureMediaQueryAdapter,
2324
defaultVariables,
2425
flushMicrotasksForTests,
2526
getRuntimeSubscriberCountForTests,
@@ -119,6 +120,7 @@ export function useCssxTemplate (
119120
export const __cssxInternals = {
120121
clearRawCssCacheForTests,
121122
configureDimensionsAdapterForTests: configureDimensionsAdapter,
123+
configureMediaQueryAdapterForTests: configureMediaQueryAdapter,
122124
createTrackedCssxSheet,
123125
flushMicrotasksForTests,
124126
getRuntimeSubscriberCountForTests,

packages/css-to-rn/src/react/cssx.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
} from '../resolve.ts'
1212
import {
1313
evaluateMediaQuery,
14+
getMediaQueryEvaluator,
1415
getDefaultVariableValues,
1516
getDimensions,
1617
getDimensionsVersion,
@@ -67,6 +68,7 @@ export function cssx (
6768
variables: getVariableValues(),
6869
defaultVariables: getDefaultVariableValues(),
6970
dimensions: getDimensions(),
71+
mediaQueryEvaluator: getMediaQueryEvaluator(),
7072
cache: options.cache ?? normalized.cache
7173
})
7274

0 commit comments

Comments
 (0)