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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 202 additions & 0 deletions packages/@stylexjs/babel-plugin/__tests__/evaluation-import-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -476,4 +476,206 @@ describe('Evaluation of imported values works based on configuration', () => {
`);
});
});

describe('Arithmetic on imported tokens compiles to calc()', () => {
const varName = (key) =>
`var(--${options.classNamePrefix}${hash(
`otherFile.stylex.js//MyTheme.${key}`,
)})`;

const transformStyle = (value) =>
transform(`
import stylex from 'stylex';
import { MyTheme } from 'otherFile.stylex';
const styles = stylex.create({
box: {
zIndex: ${value},
}
});
stylex(styles.box);
`).code;

test('token + token', () => {
expect(transformStyle('MyTheme.a + MyTheme.b')).toContain(
`calc(${varName('a')} + ${varName('b')})`,
);
});

test('token + number and number + token', () => {
expect(transformStyle('MyTheme.a + 4')).toContain(
`calc(${varName('a')} + 4)`,
);
expect(transformStyle('4 + MyTheme.a')).toContain(
`calc(4 + ${varName('a')})`,
);
});

test('token - number, token * number, token / number', () => {
expect(transformStyle('MyTheme.a - 1')).toContain(
`calc(${varName('a')} - 1)`,
);
expect(transformStyle('MyTheme.a * 2')).toContain(
`calc(${varName('a')} * 2)`,
);
expect(transformStyle('MyTheme.a / 2')).toContain(
`calc(${varName('a')} / 2)`,
);
});

test('unary minus on a token', () => {
expect(transformStyle('-MyTheme.a')).toContain(
`calc(-1 * ${varName('a')})`,
);
});

test('nested arithmetic flattens to parens', () => {
expect(transformStyle('(MyTheme.a + MyTheme.b) * 2')).toContain(
`calc((${varName('a')} + ${varName('b')}) * 2)`,
);
});

test('token + jammed string throws instead of emitting broken CSS', () => {
// A unit-string operand is excluded from calc() addition on purpose:
// `token + '4px'` vs `token + ' 4px'` (list shorthand) must not
// silently mean different things. And since the concatenation
// 'var(--x)10px' is invalid CSS, it fails instead.
expect(() => transformStyle("MyTheme.a + '10px'")).toThrow(
/would\s+produce invalid CSS/,
);
expect(() => transformStyle("MyTheme.a + 'px'")).toThrow(
/would\s+produce invalid CSS/,
);
expect(() => transformStyle("'10px' + MyTheme.a")).toThrow(
/would\s+produce invalid CSS/,
);
});

test('token + separated string stays list concatenation', () => {
const code = transformStyle("MyTheme.a + ' 4px'");
expect(code).not.toContain('calc');
expect(code).toContain(`${varName('a')} 4px`);
});

test('string concatenation with non-numeric strings is preserved', () => {
const code = transform(`
import stylex from 'stylex';
import { MyTheme } from 'otherFile.stylex';
const styles = stylex.create({
box: {
fontFamily: 'Arial, ' + MyTheme.font,
}
});
stylex(styles.box);
`).code;
// The whitespace normalizer removes the space after the comma.
expect(code).toContain(`Arial,${varName('font')}`);
expect(code).not.toContain('calc');
});

test('template literal interpolation is preserved', () => {
const code = transform(`
import stylex from 'stylex';
import { MyTheme } from 'otherFile.stylex';
const styles = stylex.create({
box: {
width: \`\${MyTheme.a}px\`,
}
});
stylex(styles.box);
`).code;
expect(code).toContain(`${varName('a')}px`);
expect(code).not.toContain('calc');
});

test('unsupported operators throw a compile error', () => {
const unsupported = [
'MyTheme.a % 2',
'MyTheme.a ** 2',
'MyTheme.a & 1',
'~MyTheme.a',
'!MyTheme.a',
'+MyTheme.a',
];
for (const value of unsupported) {
expect(() => transformStyle(value)).toThrow(
/cannot be applied to a StyleX variable or constant/,
);
}
});

test('comparisons on tokens throw a compile error', () => {
expect(() => transformStyle('MyTheme.a > MyTheme.b ? 1 : 2')).toThrow(
/cannot be compared with ">" at compile time/,
);
expect(() => transformStyle("MyTheme.a === 'red' ? 1 : 2")).toThrow(
/cannot be compared with "===" at compile time/,
);
});

test('null and undefined guards on tokens still compile', () => {
expect(transformStyle('MyTheme.a != null ? 5 : 7')).toContain(
'z-index:5',
);
expect(transformStyle('MyTheme.a === undefined ? 5 : 7')).toContain(
'z-index:7',
);
expect(transformStyle('MyTheme.a ?? 7')).toContain(
`z-index:${varName('a')}`,
);
});

test('arithmetic in a computed style key throws a compile error', () => {
expect(() =>
transform(`
import stylex from 'stylex';
import { MyTheme } from 'otherFile.stylex';
const styles = stylex.create({
box: {
[MyTheme.a + MyTheme.b]: 'red',
}
});
stylex(styles.box);
`),
).toThrow(/cannot be used as a style property key/);
});

test('token misuse inside a dynamic style throws instead of degrading', () => {
expect(() =>
transform(`
import stylex from 'stylex';
import { MyTheme } from 'otherFile.stylex';
const styles = stylex.create({
box: (opacity) => ({
opacity,
zIndex: MyTheme.a === 'big' ? 1 : 2,
}),
});
stylex.props(styles.box(0.5));
`),
).toThrow(/cannot be compared with "===" at compile time/);
});

test('unicode custom property keys work with arithmetic', () => {
const { metadata } = transform(`
import stylex from 'stylex';
import { MyTheme } from 'otherFile.stylex';
const styles = stylex.create({
box: {
zIndex: MyTheme['--größe'] * 2,
}
});
stylex(styles.box);
`);
expect(metadata.stylex[0][1].ltr).toContain('calc(var(--größe) * 2)');
});

test('arithmetic with a non-numeric operand throws a compile error', () => {
expect(() => transformStyle("MyTheme.a - 'foo'")).toThrow(
/requires the other operand/,
);
expect(() => transformStyle("MyTheme.a * 'auto'")).toThrow(
/requires the other operand/,
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -1230,4 +1230,98 @@ describe('@stylexjs/babel-plugin', () => {
`);
});
});

describe('[transform] arithmetic on imported defineConsts (#1597)', () => {
function transformCrossFile(mainSource) {
const pluginOpts = {
unstable_moduleResolution: { type: 'haste' },
};

const tokens = transformSync(
`
import * as stylex from '@stylexjs/stylex';
export const consts = stylex.defineConsts({
A: 26,
B: 14,
D: 6,
gutter: '16px',
});
export const vars = stylex.defineVars({
gap: '8px',
});
`,
{
filename: '/src/app/constants.stylex.js',
parserOpts: { flow: 'all' },
babelrc: false,
plugins: [[stylexPlugin, pluginOpts]],
},
);

const main = transformSync(mainSource, {
filename: '/src/app/main.js',
parserOpts: { flow: 'all' },
babelrc: false,
plugins: [[stylexPlugin, pluginOpts]],
});

return [
...(tokens.metadata.stylex || []),
...(main.metadata.stylex || []),
];
}

test('numeric const arithmetic resolves to calc() with literal values', () => {
const metadata = transformCrossFile(`
import * as stylex from '@stylexjs/stylex';
import { consts } from 'constants.stylex';
export const styles = stylex.create({
box: {
zIndex: consts.A + consts.B - consts.D,
opacity: consts.A / 4,
},
});
`);

const css = stylexPlugin.processStylexRules(metadata, {
useLayers: false,
});
expect(css).toContain('z-index:calc((26 + 14) - 6)');
expect(css).toContain('opacity:calc(26 / 4)');
});

test('unit const arithmetic stays as calc() with substituted values', () => {
const metadata = transformCrossFile(`
import * as stylex from '@stylexjs/stylex';
import { consts } from 'constants.stylex';
export const styles = stylex.create({
box: {
paddingTop: consts.gutter * 2,
},
});
`);

const css = stylexPlugin.processStylexRules(metadata, {
useLayers: false,
});
expect(css).toContain('padding-top:calc(16px * 2)');
});

test('mixed const and defineVars arithmetic keeps the var() in calc()', () => {
const metadata = transformCrossFile(`
import * as stylex from '@stylexjs/stylex';
import { consts, vars } from 'constants.stylex';
export const styles = stylex.create({
box: {
marginTop: consts.A * vars.gap,
},
});
`);

const css = stylexPlugin.processStylexRules(metadata, {
useLayers: false,
});
expect(css).toMatch(/margin-top:calc\(26 \* var\(--[a-z0-9]+\)\)/);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,20 @@ describe('@stylexjs/babel-plugin', () => {
`);
});

test('arithmetic on same-group references compiles to calc()', () => {
const { metadata } = transform(`
import * as stylex from '@stylexjs/stylex';
export const layout = stylex.defineVars({
gap: '8px',
doubleGap: () => layout.gap * 2,
});
`);

expect(metadata.stylex[0][1].ltr).toContain(
'--x1gpkec6:calc(var(--x1kbodq4) * 2)',
);
});

test('same-group references can point to later keys', () => {
const { code, metadata } = transform(`
import * as stylex from '@stylexjs/stylex';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ describe('@stylexjs/babel-plugin', () => {
import stylex from 'stylex';
const variables = stylex.createTheme(genStyles(), {});
`);
}).toThrow(messages.nonStaticValue('createTheme'));
}).toThrow('Referenced constant is not defined.');

expect(() => {
transform(`
Expand All @@ -82,7 +82,7 @@ describe('@stylexjs/babel-plugin', () => {
import stylex from 'stylex';
const variables = stylex.createTheme({__varGroupHash__: 'x568ih9'}, genStyles());
`);
}).toThrow(messages.nonStaticValue('createTheme'));
}).toThrow('Referenced constant is not defined.');

expect(() => {
transform(`
Expand All @@ -102,7 +102,7 @@ describe('@stylexjs/babel-plugin', () => {
{__varGroupHash__: 'x568ih9', labelColor: 'var(--labelColorHash)'},
{[labelColor]: 'red',});
`);
}).toThrow(messages.nonStaticValue('createTheme'));
}).toThrow('Referenced constant is not defined.');
});

/* Values */
Expand Down Expand Up @@ -139,7 +139,7 @@ describe('@stylexjs/babel-plugin', () => {
{labelColor: labelColor,}
);
`);
}).toThrow(messages.nonStaticValue('createTheme'));
}).toThrow('Referenced constant is not defined.');

expect(() => {
transform(`
Expand All @@ -149,7 +149,7 @@ describe('@stylexjs/babel-plugin', () => {
{labelColor: labelColor(),}
);
`);
}).toThrow(messages.nonStaticValue('createTheme'));
}).toThrow('Referenced constant is not defined.');
});
});
});
Loading
Loading