Skip to content

Commit d0121e7

Browse files
cursoragentskovhus
andcommitted
Resolve CSS variable fallbacks via adapter
Co-authored-by: Kenneth Skovhus <skovhus@users.noreply.github.com>
1 parent 729c571 commit d0121e7

3 files changed

Lines changed: 92 additions & 20 deletions

File tree

src/__tests__/transform.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5778,6 +5778,52 @@ export const App = () => (
57785778
});
57795779

57805780
describe("var() rewriter — adapter contract", () => {
5781+
it("should retry adapter resolution without fallback and drop the CSS var default when resolved", () => {
5782+
const source = `
5783+
import styled from "styled-components";
5784+
5785+
const Container = styled.div\`
5786+
border-radius: var(--control-border-radius, 4px);
5787+
\`;
5788+
5789+
export const App = () => <Container>content</Container>;
5790+
`;
5791+
5792+
const seenFallbacks: Array<string | undefined> = [];
5793+
const noFallbackAdapter = {
5794+
...fixtureAdapter,
5795+
resolveValue(ctx: ResolveValueContext) {
5796+
if (ctx.kind === "cssVariable" && ctx.name === "--control-border-radius") {
5797+
seenFallbacks.push(ctx.fallback);
5798+
if (ctx.fallback) {
5799+
return undefined;
5800+
}
5801+
return {
5802+
expr: "vars.controlBorderRadius",
5803+
imports: [
5804+
{
5805+
from: { kind: "specifier" as const, value: "./vars.stylex" },
5806+
names: [{ imported: "vars" }],
5807+
},
5808+
],
5809+
};
5810+
}
5811+
return fixtureAdapter.resolveValue(ctx);
5812+
},
5813+
} satisfies Adapter;
5814+
5815+
const result = transformWithWarnings(
5816+
{ source, path: "test.tsx" },
5817+
{ jscodeshift: j, j, stats: () => {}, report: () => {} },
5818+
{ adapter: noFallbackAdapter },
5819+
);
5820+
5821+
expect(result.code).not.toBeNull();
5822+
expect(seenFallbacks).toEqual(["4px", undefined]);
5823+
expect(result.code).toContain("borderRadius: vars.controlBorderRadius");
5824+
expect(result.code).not.toContain("var(--control-border-radius, 4px)");
5825+
});
5826+
57815827
it("should not pass placeholder sentinels to adapter when var() default contains a dynamic interpolation", () => {
57825828
const source = `
57835829
import styled from "styled-components";

src/internal/css-vars.ts

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@
55
import type { JSCodeshift } from "jscodeshift";
66
import type { ImportSpec, ResolveValueContext, ResolveValueResult } from "../adapter.js";
77

8+
export type CssVarCall = {
9+
start: number;
10+
end: number;
11+
name: string;
12+
fallback?: string;
13+
};
14+
815
export function rewriteCssVarsInString(args: {
916
raw: string;
1017
filePath: string;
@@ -22,14 +29,32 @@ export function findCssVarCallsInString(raw: string): CssVarCall[] {
2229
return findCssVarCalls(raw);
2330
}
2431

25-
type ExpressionKind = Parameters<JSCodeshift["expressionStatement"]>[0];
32+
export function resolveCssVarCall(args: {
33+
call: CssVarCall;
34+
definedValue?: string;
35+
filePath: string;
36+
resolveValue: (ctx: ResolveValueContext) => ResolveValueResult | undefined;
37+
}): ResolveValueResult | undefined {
38+
const { call, definedValue, filePath, resolveValue } = args;
39+
const baseContext = {
40+
kind: "cssVariable" as const,
41+
name: call.name,
42+
filePath,
43+
...(definedValue ? { definedValue } : {}),
44+
};
45+
const result = resolveValue({
46+
...baseContext,
47+
...(call.fallback ? { fallback: call.fallback } : {}),
48+
});
49+
// Adapter-backed variables supersede CSS defaults. Some adapters only recognize the
50+
// canonical variable name, so retry without the fallback before preserving raw var().
51+
if (result || !call.fallback) {
52+
return result;
53+
}
54+
return resolveValue(baseContext);
55+
}
2656

27-
type CssVarCall = {
28-
start: number;
29-
end: number;
30-
name: string;
31-
fallback?: string;
32-
};
57+
type ExpressionKind = Parameters<JSCodeshift["expressionStatement"]>[0];
3358

3459
function findCssVarCalls(raw: string): CssVarCall[] {
3560
const out: CssVarCall[] = [];
@@ -122,13 +147,11 @@ function rewriteCssVarsInStringImpl(args: {
122147
if (c.start > last) {
123148
segments.push({ kind: "text", value: raw.slice(last, c.start) });
124149
}
125-
const definedValue = definedVars.get(c.name);
126-
const res = resolveValue({
127-
kind: "cssVariable",
128-
name: c.name,
150+
const res = resolveCssVarCall({
151+
call: c,
152+
definedValue: definedVars.get(c.name),
129153
filePath,
130-
...(c.fallback ? { fallback: c.fallback } : {}),
131-
...(definedValue ? { definedValue } : {}),
154+
resolveValue,
132155
});
133156
if (!res) {
134157
segments.push({ kind: "text", value: raw.slice(c.start, c.end) });

src/internal/transform-css-vars.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55
import type { JSCodeshift } from "jscodeshift";
66
import type { ImportSpec, ResolveValueContext, ResolveValueResult } from "../adapter.js";
7-
import { findCssVarCallsInString, rewriteCssVarsInString } from "./css-vars.js";
7+
import { findCssVarCallsInString, resolveCssVarCall, rewriteCssVarsInString } from "./css-vars.js";
88
import { isAstNode } from "./utilities/jscodeshift-utils.js";
99

1010
export function rewriteCssVarsInStyleObject(
@@ -204,20 +204,23 @@ function rewriteCssVarsInTemplateLiteral(
204204
const replacements: Replacement[] = [];
205205

206206
for (const call of calls) {
207-
const definedValue = ctx.definedVars.get(call.name);
208207
// Strip placeholder sentinels from the fallback before forwarding to the adapter so
209208
// adapter logic that inspects `ctx.fallback` (validation, parsing, expression generation)
210209
// never sees synthetic interpolation markers. When the fallback consists entirely of
211210
// placeholders/whitespace, omit it altogether.
212211
const cleanedFallback = call.fallback
213212
? call.fallback.replace(placeholderPattern, "").trim().replace(/,\s*$/, "")
214213
: undefined;
215-
const res = ctx.resolveValue({
216-
kind: "cssVariable",
217-
name: call.name,
214+
const res = resolveCssVarCall({
215+
call: {
216+
start: call.start,
217+
end: call.end,
218+
name: call.name,
219+
...(cleanedFallback ? { fallback: cleanedFallback } : {}),
220+
},
221+
definedValue: ctx.definedVars.get(call.name),
218222
filePath: ctx.filePath,
219-
...(cleanedFallback ? { fallback: cleanedFallback } : {}),
220-
...(definedValue ? { definedValue } : {}),
223+
resolveValue: ctx.resolveValue,
221224
});
222225
if (!res) {
223226
continue;

0 commit comments

Comments
 (0)