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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
329 changes: 299 additions & 30 deletions src/__tests__/compiler/property.test.ts

Large diffs are not rendered by default.

692 changes: 692 additions & 0 deletions src/__tests__/native/non-inheriting-variables.test.tsx

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions src/__tests__/native/variable-cycles.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ test.each([
});
});

test("a cycle through :root resolves to the reader's fallback, not an internal one", () => {
registerCSS(
`:root { --a: var(--b, 12px); --b: var(--a); } .sample { width: var(--a, 37px); }`,
{ inlineVariables: false },
);
render(<View testID="sample" className="sample" />);
expect(screen.getByTestId("sample").props.style).toStrictEqual({ width: 37 });
});

test("a variable outside a cycle can use a fallback for the invalid variable", () => {
registerCSS(
`.sample { --a: var(--b); --b: var(--a); --c: var(--a, 42px); width: var(--c); }`,
Expand Down
129 changes: 129 additions & 0 deletions src/__tests__/native/vars.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,132 @@ test("vars", () => {
color: "blue",
});
});

const parentTestID = "parent";

const registration = `
@property --my-var {
syntax: "<length>";
inherits: false;
initial-value: 0px;
}
`;

const inheritingRegistration = `
@property --my-var {
syntax: "<length>";
inherits: true;
initial-value: 0px;
}
`;

// An element whose rules declare no variable publishes no context, so each carrier declares an unrelated one, twice to escape inlining
const publishesVariables = `
.has-vars { --trigger: 1px; }
.has-vars-too { --trigger: 2px; }
`;

test("an inline vars() non-inheriting property does not reach a descendant", () => {
registerCSS(`
${registration}
${publishesVariables}
.parent { --my-var: 1px; }
.other { --my-var: 2px; }
.child { width: var(--my-var); }
`);

render(
<View
testID={parentTestID}
className="has-vars"
style={vars({ "--my-var": 10 })}
>
<View testID={testID} className="child" />
</View>,
);

expect(screen.getByTestId(testID).props.style).toStrictEqual({ width: 0 });
});

test("an inline vars() non-inheriting property still applies to the element carrying it", () => {
registerCSS(`
${registration}
${publishesVariables}
.parent { --my-var: 1px; }
.other { --my-var: 2px; }
.self { width: var(--my-var); }
`);

render(
<View
testID={testID}
className="self has-vars"
style={vars({ "--my-var": 10 })}
/>,
);

expect(screen.getByTestId(testID).props.style).toStrictEqual({ width: 10 });
});

test("an inline vars() inheriting property still reaches a descendant", () => {
registerCSS(`
${inheritingRegistration}
${publishesVariables}
.parent { --my-var: 1px; }
.other { --my-var: 2px; }
.child { width: var(--my-var); }
`);

render(
<View
testID={parentTestID}
className="has-vars"
style={vars({ "--my-var": 10 })}
>
<View testID={testID} className="child" />
</View>,
);

expect(screen.getByTestId(testID).props.style).toStrictEqual({ width: 10 });
});

test("an inline vars() does not hand an ancestor's variable to the subtree", () => {
registerCSS(`
.ancestor { --shared: 1px; }
.ancestor-too { --shared: 2px; }
.middle { --shared: 50px; }
.middle-too { --shared: 60px; }
.child { width: var(--shared); }
`);

render(
<View className="ancestor">
<View className="middle" style={vars({ "--unrelated": 7 })}>
<View testID={testID} className="child" />
</View>
</View>,
);

expect(screen.getByTestId(testID).props.style).toStrictEqual({ width: 50 });
});

test("an inline vars() unregistered property still reaches a descendant", () => {
registerCSS(`
${publishesVariables}
.parent { --my-var: 1px; }
.other { --my-var: 2px; }
.child { width: var(--my-var); }
`);

render(
<View
testID={parentTestID}
className="has-vars"
style={vars({ "--my-var": 10 })}
>
<View testID={testID} className="child" />
</View>,
);

expect(screen.getByTestId(testID).props.style).toStrictEqual({ width: 10 });
});
57 changes: 52 additions & 5 deletions src/compiler/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type {
StyleRuleMapping,
UniqueVarInfo,
} from "./compiler.types";
import { isComputationallyIndependent } from "./computational-independence";
import { parseContainerCondition } from "./container-query";
import {
parseAngle,
Expand All @@ -37,6 +38,7 @@ import { inlineVariables } from "./inline-variables";
import { extractKeyFrames } from "./keyframes";
import { lightningcssLoader } from "./lightningcss-loader";
import { parseMediaQuery } from "./media-query";
import { isUniversalSelector } from "./selector-builder";
import { StylesheetBuilder } from "./stylesheet";
import { supportsConditionValid } from "./supports";

Expand Down Expand Up @@ -98,8 +100,33 @@ export function compile(code: Buffer | string, options: CompilerOptions = {}) {
options.inlineRem = effectiveRem;
}

// Collected in the first pass, since the inliner runs before the builder records registrations
const nonInheritedVariables = new Set<string>();
const universallyDeclaredVariables = new Set<string>();

const firstPassVisitor: Visitor<CustomAtRules> = {
Rule(rule) {
if (
rule.type === "property" &&
!rule.value.inherits &&
isValidRegistration(rule.value)
) {
nonInheritedVariables.add(rule.value.name);
}

if (
rule.type === "style" &&
rule.value.selectors.some(isUniversalSelector)
) {
const { declarations = [], importantDeclarations = [] } =
rule.value.declarations ?? {};
for (const declaration of [...declarations, ...importantDeclarations]) {
if (declaration.property === "custom") {
universallyDeclaredVariables.add(declaration.value.name);
}
}
}

if (
rule.type === "unknown" &&
["cssInterop", "react-native"].includes(rule.value.name)
Expand Down Expand Up @@ -166,7 +193,14 @@ export function compile(code: Buffer | string, options: CompilerOptions = {}) {
}
};
firstPassVisitor.StyleSheetExit = (sheet) => {
return inlineVariables(sheet, vars);
// A non-inheriting property reaches every element only from a universal rule, which each element declares for itself
const runtimeVariables = new Set(
[...nonInheritedVariables].filter(
(name) => !universallyDeclaredVariables.has(name),
),
);

return inlineVariables(sheet, vars, runtimeVariables);
};
}

Expand Down Expand Up @@ -421,21 +455,34 @@ function extractContainer(
}
}

function isValidRegistration({ initialValue }: PropertyRule): boolean {
return initialValue == null || isComputationallyIndependent(initialValue);
}

function extractPropertyRule(
propertyRule: PropertyRule,
builder: StylesheetBuilder,
) {
const { initialValue, name } = propertyRule;

if (initialValue == null) {
if (!isValidRegistration(propertyRule)) {
return;
}

const { inherits, initialValue, name } = propertyRule;

const varName = name.startsWith("--") ? name.slice(2) : name;

// Recorded before the early return below, as inherits is independent of initial-value
if (!inherits) {
builder.addNonInheritedVariable(varName);
}

if (initialValue == null) {
return;
}
const value = parsePropertyInitialValue(initialValue, builder);

if (value !== undefined) {
builder.addRootVariable(varName, value);
builder.addRegisteredInitialValue(varName, value);
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/compiler/compiler.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ export interface ReactNativeCssStyleSheet {
vr?: RootVariables;
/** Universal Variables */
vu?: RootVariables;
/** Non-Inheriting Variables */
vn?: string[];
/** Registered Initial Values */
vi?: RootVariables;
}

/******************************** Styles ********************************/
Expand Down
Loading