Skip to content

Commit bcb2a68

Browse files
authoredSep 8, 2021
Add support for keyframes (#812)
1 parent 8bc7fdc commit bcb2a68

31 files changed

+4164
-251
lines changed
 

‎.changeset/honest-plums-reply.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@compiled/babel-plugin': minor
3+
'@compiled/react': minor
4+
---
5+
6+
Add support for `keyframes`

‎.changeset/slimy-roses-relax.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@compiled/css': minor
3+
---
4+
5+
Add option to disable the autoprefixer by setting `process.env.AUTOPREFIXER` to `off`

‎examples/keyframes/globals.tsx

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
export {};
2+
3+
declare global {
4+
interface Window {
5+
runtime: {
6+
blueToIndigo: {
7+
from: string;
8+
to: string;
9+
};
10+
coralToPink: {
11+
from: string;
12+
to: string;
13+
};
14+
purpleToSlateBlue: {
15+
from: string;
16+
to: string;
17+
};
18+
};
19+
}
20+
}
21+
22+
Object.assign(window, {
23+
runtime: {
24+
blueToIndigo: {
25+
from: 'blue',
26+
to: 'indigo',
27+
},
28+
coralToPink: {
29+
from: 'coral',
30+
to: 'pink',
31+
},
32+
purpleToSlateBlue: {
33+
from: 'purple',
34+
to: 'slateblue',
35+
},
36+
},
37+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { keyframes } from '@compiled/react';
2+
3+
export const fadeOut = keyframes({
4+
from: {
5+
color: 'coral',
6+
opacity: 1,
7+
},
8+
to: {
9+
color: 'pink',
10+
opacity: 0,
11+
},
12+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { keyframes } from '@compiled/react';
2+
3+
export const fadeOut = keyframes`
4+
from {
5+
color: coral;
6+
opacity: 1;
7+
}
8+
to {
9+
color: pink;
10+
opacity: 0;
11+
}
12+
`;
+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { css } from '@compiled/react';
2+
3+
export default {
4+
title: 'css prop/keyframes (inline)',
5+
};
6+
7+
export const ShorthandObjectCallExpression = (): JSX.Element => (
8+
<div
9+
css={{
10+
'@keyframes fadeOut-socec': {
11+
from: {
12+
color: 'blue',
13+
opacity: 1,
14+
},
15+
to: {
16+
color: 'indigo',
17+
opacity: 0,
18+
},
19+
},
20+
animation: 'fadeOut-socec 2s ease-in-out infinite',
21+
}}>
22+
hello world
23+
</div>
24+
);
25+
26+
export const ShorthandTaggedTemplateExpression = (): JSX.Element => (
27+
<div
28+
css={css`
29+
@keyframes fadeOut-sttec {
30+
from {
31+
color: blue;
32+
opacity: 1;
33+
}
34+
to {
35+
color: indigo;
36+
opacity: 0;
37+
}
38+
}
39+
animation: fadeOut-sttec 2s ease-in-out infinite;
40+
`}>
41+
hello world
42+
</div>
43+
);
44+
45+
export const ObjectCallExpression = (): JSX.Element => (
46+
<div
47+
css={{
48+
'@keyframes fadeOut-ocec': {
49+
from: {
50+
color: 'blue',
51+
opacity: 1,
52+
},
53+
to: {
54+
color: 'indigo',
55+
opacity: 0,
56+
},
57+
},
58+
animationDuration: '2s',
59+
animationIterationCount: 'infinite',
60+
animationName: 'fadeOut-ocec',
61+
animationTimingFunction: 'ease-in-out',
62+
}}>
63+
hello world
64+
</div>
65+
);
66+
67+
export const TaggedTemplateExpression = (): JSX.Element => (
68+
<div
69+
css={css`
70+
@keyframes fadeOut-ttec {
71+
from {
72+
color: blue;
73+
opacity: 1;
74+
}
75+
to {
76+
color: indigo;
77+
opacity: 0;
78+
}
79+
}
80+
animation-duration: 2s;
81+
animation-iteration-count: infinite;
82+
animation-name: fadeOut-ttec;
83+
animation-timing-function: ease-in-out;
84+
`}>
85+
hello world
86+
</div>
87+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import { css, keyframes, styled } from '@compiled/react';
2+
3+
import '../keyframes/globals';
4+
import { fadeOut as shadowedFadeOut } from '../keyframes/object-call-expression';
5+
6+
// TODO Add css prop using a call expression mixin once supported: https://github.com/atlassian-labs/compiled/issues/789
7+
export default {
8+
title: 'keyframes/object call expression',
9+
};
10+
11+
const generateKeyframes = (fromColor: string, toColor: string) =>
12+
keyframes({
13+
from: {
14+
color: fromColor,
15+
opacity: 1,
16+
},
17+
to: {
18+
color: toColor,
19+
opacity: 0,
20+
},
21+
});
22+
23+
export const RuntimeKeyframes = (): JSX.Element => (
24+
<>
25+
<div
26+
css={`
27+
animation: ${generateKeyframes(
28+
window.runtime.blueToIndigo.from,
29+
window.runtime.blueToIndigo.to
30+
)}
31+
2s ease-in-out infinite;
32+
`}>
33+
blue to indigo
34+
</div>
35+
<div
36+
css={`
37+
animation-duration: 2s;
38+
animation-iteration-count: infinite;
39+
animation-name: ${generateKeyframes(
40+
window.runtime.coralToPink.from,
41+
window.runtime.coralToPink.to
42+
)};
43+
animation-timing-function: ease-in-out;
44+
`}>
45+
coral to pink
46+
</div>
47+
<div
48+
css={{
49+
animationDuration: '2s',
50+
animationIterationCount: 'infinite',
51+
animationName: generateKeyframes(
52+
window.runtime.purpleToSlateBlue.from,
53+
window.runtime.purpleToSlateBlue.to
54+
),
55+
animationTimingFunction: 'ease-in-out',
56+
}}>
57+
purple to slate blue
58+
</div>
59+
</>
60+
);
61+
62+
const fadeOut = keyframes({
63+
from: {
64+
color: 'blue',
65+
opacity: 1,
66+
},
67+
to: {
68+
color: 'indigo',
69+
opacity: 0,
70+
},
71+
});
72+
73+
const shadowedKeyframes = {
74+
fadeOut: keyframes({
75+
from: {
76+
color: 'purple',
77+
opacity: 1,
78+
},
79+
to: {
80+
color: 'slateblue',
81+
opacity: 0,
82+
},
83+
}),
84+
};
85+
86+
export const ShadowedKeyframes = (): JSX.Element => (
87+
<>
88+
<div
89+
css={`
90+
animation: ${fadeOut} 2s ease-in-out infinite;
91+
`}>
92+
blue to indigo
93+
</div>
94+
<div
95+
css={`
96+
animation-duration: 2s;
97+
animation-iteration-count: infinite;
98+
animation-name: ${shadowedFadeOut};
99+
animation-timing-function: ease-in-out;
100+
`}>
101+
coral to pink
102+
</div>
103+
<div
104+
css={{
105+
animationDuration: '2s',
106+
animationIterationCount: 'infinite',
107+
animationName: shadowedKeyframes.fadeOut,
108+
animationTimingFunction: 'ease-in-out',
109+
}}>
110+
purple to slate blue
111+
</div>
112+
</>
113+
);
114+
115+
const shorthandTaggedTemplateExpressionCss = css`
116+
animation: ${fadeOut} 2s ease-in-out infinite;
117+
`;
118+
119+
export const ShorthandCssPropTaggedTemplateExpression = (): JSX.Element => (
120+
<div css={shorthandTaggedTemplateExpressionCss}>blue to indigo</div>
121+
);
122+
123+
export const ShorthandInlineCssPropObjectCallExpression = (): JSX.Element => (
124+
<div css={{ animation: `${fadeOut} 2s ease-in-out infinite` }}>blue to indigo</div>
125+
);
126+
127+
export const ShorthandInlineCssPropTaggedTemplateExpression = (): JSX.Element => (
128+
<div
129+
css={css`
130+
animation: ${fadeOut} 2s ease-in-out infinite;
131+
`}>
132+
blue to indigo
133+
</div>
134+
);
135+
136+
const taggedTemplateExpressionCss = css`
137+
animation-duration: 2s;
138+
animation-iteration-count: infinite;
139+
animation-name: ${fadeOut};
140+
animation-timing-function: ease-in-out;
141+
`;
142+
143+
export const CssPropTaggedTemplateExpression = (): JSX.Element => (
144+
<div css={taggedTemplateExpressionCss}>blue to indigo</div>
145+
);
146+
147+
export const InlineCssPropObjectCallExpression = (): JSX.Element => (
148+
<div
149+
css={{
150+
animationDuration: '2s',
151+
animationIterationCount: 'infinite',
152+
animationName: fadeOut,
153+
animationTimingFunction: 'ease-in-out',
154+
}}>
155+
blue to indigo
156+
</div>
157+
);
158+
159+
export const InlineCssPropTaggedTemplateExpression = (): JSX.Element => (
160+
<div
161+
css={css`
162+
animation-duration: 2s;
163+
animation-iteration-count: infinite;
164+
animation-name: ${fadeOut};
165+
animation-timing-function: ease-in-out;
166+
`}>
167+
blue to indigo
168+
</div>
169+
);
170+
171+
const ShorthandObjectCallExpression = styled.div({
172+
animation: `${fadeOut} 2s ease-in-out infinite`,
173+
});
174+
175+
export const StyledShorthandObjectCallExpression = (): JSX.Element => (
176+
<ShorthandObjectCallExpression>blue to indigo</ShorthandObjectCallExpression>
177+
);
178+
179+
const ShorthandTaggedTemplateExpression = styled.div`
180+
animation: ${fadeOut} 2s ease-in-out infinite;
181+
`;
182+
183+
export const StyledShorthandTaggedTemplateExpression = (): JSX.Element => (
184+
<ShorthandTaggedTemplateExpression>blue to indigo</ShorthandTaggedTemplateExpression>
185+
);
186+
187+
const ObjectCallExpression = styled.div({
188+
animationDuration: '2s',
189+
animationIterationCount: 'infinite',
190+
animationName: fadeOut,
191+
animationTimingFunction: 'ease-in-out',
192+
});
193+
194+
export const StyledObjectCallExpression = (): JSX.Element => (
195+
<ObjectCallExpression>blue to indigo</ObjectCallExpression>
196+
);
197+
198+
const TaggedTemplateExpression = styled.div`
199+
animation-duration: 2s;
200+
animation-iteration-count: infinite;
201+
animation-name: ${fadeOut};
202+
animation-timing-function: ease-in-out;
203+
`;
204+
205+
export const StyledTaggedTemplateExpression = (): JSX.Element => (
206+
<TaggedTemplateExpression>blue to indigo</TaggedTemplateExpression>
207+
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import { css, keyframes, styled } from '@compiled/react';
2+
3+
import '../keyframes/globals';
4+
import { fadeOut as shadowedFadeOut } from '../keyframes/tagged-template-literal';
5+
6+
// TODO Add css prop using a call expression mixin once supported: https://github.com/atlassian-labs/compiled/issues/789
7+
export default {
8+
title: 'keyframes/tagged template expression',
9+
};
10+
11+
const generateKeyframes = (fromColor: string, toColor: string) =>
12+
keyframes`
13+
from {
14+
color: ${fromColor};
15+
opacity: 1;
16+
}
17+
to {
18+
color: ${toColor};
19+
opacity: 0;
20+
}
21+
`;
22+
23+
export const RuntimeKeyframes = (): JSX.Element => (
24+
<>
25+
<div
26+
css={`
27+
animation: ${generateKeyframes(
28+
window.runtime.blueToIndigo.from,
29+
window.runtime.blueToIndigo.to
30+
)}
31+
2s ease-in-out infinite;
32+
`}>
33+
blue to indigo
34+
</div>
35+
<div
36+
css={`
37+
animation-duration: 2s;
38+
animation-iteration-count: infinite;
39+
animation-name: ${generateKeyframes(
40+
window.runtime.coralToPink.from,
41+
window.runtime.coralToPink.to
42+
)};
43+
animation-timing-function: ease-in-out;
44+
`}>
45+
coral to pink
46+
</div>
47+
<div
48+
css={{
49+
animationDuration: '2s',
50+
animationIterationCount: 'infinite',
51+
animationName: generateKeyframes(
52+
window.runtime.purpleToSlateBlue.from,
53+
window.runtime.purpleToSlateBlue.to
54+
),
55+
animationTimingFunction: 'ease-in-out',
56+
}}>
57+
purple to slate blue
58+
</div>
59+
</>
60+
);
61+
62+
const fadeOut = keyframes`
63+
from {
64+
color: blue;
65+
opacity: 1;
66+
}
67+
to {
68+
color: indigo;
69+
opacity: 0;
70+
}
71+
`;
72+
73+
const shadowedKeyframes = {
74+
fadeOut: keyframes`
75+
from {
76+
color: purple;
77+
opacity: 1;
78+
}
79+
to {
80+
color: slateblue;
81+
opacity: 0;
82+
}
83+
`,
84+
};
85+
86+
export const ShadowedKeyframes = (): JSX.Element => (
87+
<>
88+
<div
89+
css={`
90+
animation: ${fadeOut} 2s ease-in-out infinite;
91+
`}>
92+
blue to indigo
93+
</div>
94+
<div
95+
css={`
96+
animation-duration: 2s;
97+
animation-iteration-count: infinite;
98+
animation-name: ${shadowedFadeOut};
99+
animation-timing-function: ease-in-out;
100+
`}>
101+
coral to pink
102+
</div>
103+
<div
104+
css={{
105+
animationDuration: '2s',
106+
animationIterationCount: 'infinite',
107+
animationName: shadowedKeyframes.fadeOut,
108+
animationTimingFunction: 'ease-in-out',
109+
}}>
110+
purple to slate blue
111+
</div>
112+
</>
113+
);
114+
115+
const shorthandTaggedTemplateExpressionCss = css`
116+
animation: ${fadeOut} 2s ease-in-out infinite;
117+
`;
118+
119+
export const ShorthandCssPropTaggedTemplateExpression = (): JSX.Element => (
120+
<div css={shorthandTaggedTemplateExpressionCss}>blue to indigo</div>
121+
);
122+
123+
export const ShorthandInlineCssPropObjectCallExpression = (): JSX.Element => (
124+
<div css={{ animation: `${fadeOut} 2s ease-in-out infinite` }}>blue to indigo</div>
125+
);
126+
127+
export const ShorthandInlineCssPropTaggedTemplateExpression = (): JSX.Element => (
128+
<div
129+
css={css`
130+
animation: ${fadeOut} 2s ease-in-out infinite;
131+
`}>
132+
blue to indigo
133+
</div>
134+
);
135+
136+
const taggedTemplateExpressionCss = css`
137+
animation-duration: 2s;
138+
animation-iteration-count: infinite;
139+
animation-name: ${fadeOut};
140+
animation-timing-function: ease-in-out;
141+
`;
142+
143+
export const CssPropTaggedTemplateExpression = (): JSX.Element => (
144+
<div css={taggedTemplateExpressionCss}>blue to indigo</div>
145+
);
146+
147+
export const InlineCssPropObjectCallExpression = (): JSX.Element => (
148+
<div
149+
css={{
150+
animationDuration: '2s',
151+
animationIterationCount: 'infinite',
152+
animationName: fadeOut,
153+
animationTimingFunction: 'ease-in-out',
154+
}}>
155+
blue to indigo
156+
</div>
157+
);
158+
159+
export const InlineCssPropTaggedTemplateExpression = (): JSX.Element => (
160+
<div
161+
css={css`
162+
animation-duration: 2s;
163+
animation-iteration-count: infinite;
164+
animation-name: ${fadeOut};
165+
animation-timing-function: ease-in-out;
166+
`}>
167+
blue to indigo
168+
</div>
169+
);
170+
171+
const ShorthandObjectCallExpression = styled.div({
172+
animation: `${fadeOut} 2s ease-in-out infinite`,
173+
});
174+
175+
export const StyledShorthandObjectCallExpression = (): JSX.Element => (
176+
<ShorthandObjectCallExpression>blue to indigo</ShorthandObjectCallExpression>
177+
);
178+
179+
const ShorthandTaggedTemplateExpression = styled.div`
180+
animation: ${fadeOut} 2s ease-in-out infinite;
181+
`;
182+
183+
export const StyledShorthandTaggedTemplateExpression = (): JSX.Element => (
184+
<ShorthandTaggedTemplateExpression>blue to indigo</ShorthandTaggedTemplateExpression>
185+
);
186+
187+
const ObjectCallExpression = styled.div({
188+
animationDuration: '2s',
189+
animationIterationCount: 'infinite',
190+
animationName: fadeOut,
191+
animationTimingFunction: 'ease-in-out',
192+
});
193+
194+
export const StyledObjectCallExpression = (): JSX.Element => (
195+
<ObjectCallExpression>blue to indigo</ObjectCallExpression>
196+
);
197+
198+
const TaggedTemplateExpression = styled.div`
199+
animation-duration: 2s;
200+
animation-iteration-count: infinite;
201+
animation-name: ${fadeOut};
202+
animation-timing-function: ease-in-out;
203+
`;
204+
205+
export const StyledTaggedTemplateExpression = (): JSX.Element => (
206+
<TaggedTemplateExpression>blue to indigo</TaggedTemplateExpression>
207+
);

‎examples/stories/styled-keyframes.tsx

+83
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { styled } from '@compiled/react';
2+
3+
export default {
4+
title: 'styled/keyframes (inline)',
5+
};
6+
7+
const StyledShorthandObjectCallExpression = styled.div({
8+
'@keyframes fadeOut-ssoce': {
9+
from: {
10+
color: 'blue',
11+
opacity: 1,
12+
},
13+
to: {
14+
color: 'indigo',
15+
opacity: 0,
16+
},
17+
},
18+
animation: 'fadeOut-ssoce 2s ease-in-out infinite',
19+
});
20+
21+
export const ShorthandObjectCallExpression = (): JSX.Element => (
22+
<StyledShorthandObjectCallExpression>hello world</StyledShorthandObjectCallExpression>
23+
);
24+
25+
const StyledShorthandTaggedTemplateExpression = styled.div`
26+
@keyframes fadeOut-sstte {
27+
from {
28+
color: blue;
29+
opacity: 1;
30+
}
31+
to {
32+
color: indigo;
33+
opacity: 0;
34+
}
35+
}
36+
animation: fadeOut-sstte 2s ease-in-out infinite;
37+
`;
38+
39+
export const ShorthandTaggedTemplateExpression = (): JSX.Element => (
40+
<StyledShorthandTaggedTemplateExpression>hello world</StyledShorthandTaggedTemplateExpression>
41+
);
42+
43+
const StyledObjectCallExpression = styled.div({
44+
'@keyframes fadeOut-soce': {
45+
from: {
46+
color: 'blue',
47+
opacity: 1,
48+
},
49+
to: {
50+
color: 'indigo',
51+
opacity: 0,
52+
},
53+
},
54+
animationDuration: '2s',
55+
animationIterationCount: 'infinite',
56+
animationName: 'fadeOut-soce',
57+
animationTimingFunction: 'ease-in-out',
58+
});
59+
60+
export const ObjectCallExpression = (): JSX.Element => (
61+
<StyledObjectCallExpression>hello world</StyledObjectCallExpression>
62+
);
63+
64+
const StyledTaggedTemplateExpression = styled.div`
65+
@keyframes fadeOut-stte {
66+
from {
67+
color: blue;
68+
opacity: 1;
69+
}
70+
to {
71+
color: indigo;
72+
opacity: 0;
73+
}
74+
}
75+
animation-duration: 2s;
76+
animation-iteration-count: infinite;
77+
animation-name: fadeOut-stte;
78+
animation-timing-function: ease-in-out;
79+
`;
80+
81+
export const TaggedTemplateExpression = (): JSX.Element => (
82+
<StyledTaggedTemplateExpression>hello world</StyledTaggedTemplateExpression>
83+
);

‎packages/babel-plugin/src/__tests__/test-utils.tsx

+8-3
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,25 @@ import { format } from 'prettier';
33

44
import babelPlugin from '../babel-plugin';
55

6-
export const transform = (code: string): string => {
6+
export type TransformOptions = {
7+
nonce?: string;
8+
};
9+
10+
export const transform = (code: string, options: TransformOptions = {}): string => {
11+
const { nonce } = options;
712
const fileResult = transformSync(code, {
813
babelrc: false,
914
comments: false,
1015
configFile: false,
11-
plugins: [babelPlugin],
16+
plugins: [[babelPlugin, { nonce }]],
1217
});
1318

1419
if (!fileResult || !fileResult.code) {
1520
return '';
1621
}
1722

1823
const { code: babelCode } = fileResult;
19-
const ifIndex = babelCode.indexOf('if (');
24+
const ifIndex = babelCode.indexOf('if (process.env.NODE_ENV');
2025
// Remove the imports from the code, and the styled components display name
2126
const snippet = babelCode
2227
.substring(babelCode.indexOf('const'), ifIndex === -1 ? babelCode.length : ifIndex)

‎packages/babel-plugin/src/babel-plugin.tsx

+21-5
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ export default declare<State>((api) => {
167167
return;
168168
}
169169

170-
(['styled', 'ClassNames', 'css'] as const).forEach((apiName) => {
170+
(['styled', 'ClassNames', 'css', 'keyframes'] as const).forEach((apiName) => {
171171
if (
172172
state.compiledImports &&
173173
t.isIdentifier(specifier.node?.imported) &&
@@ -188,32 +188,48 @@ export default declare<State>((api) => {
188188
return;
189189
}
190190

191+
if (
192+
t.isIdentifier(path.node.tag) &&
193+
path.node.tag.name === state.compiledImports?.keyframes
194+
) {
195+
state.pathsToCleanup.push({ path, action: 'replace' });
196+
return;
197+
}
198+
191199
if (!state.compiledImports?.styled) {
192200
return;
193201
}
194202

195-
visitStyledPath(path, { state, parentPath: path });
203+
visitStyledPath(path, { context: 'root', state, parentPath: path });
196204
},
197205
CallExpression(path, state) {
198206
if (!state.compiledImports) {
199207
return;
200208
}
201209

202-
visitStyledPath(path, { state, parentPath: path });
210+
if (
211+
t.isIdentifier(path.node.callee) &&
212+
path.node.callee.name === state.compiledImports?.keyframes
213+
) {
214+
state.pathsToCleanup.push({ path, action: 'replace' });
215+
return;
216+
}
217+
218+
visitStyledPath(path, { context: 'root', state, parentPath: path });
203219
},
204220
JSXElement(path, state) {
205221
if (!state.compiledImports?.ClassNames) {
206222
return;
207223
}
208224

209-
visitClassNamesPath(path, { state, parentPath: path });
225+
visitClassNamesPath(path, { context: 'root', state, parentPath: path });
210226
},
211227
JSXOpeningElement(path, state) {
212228
if (!state.compiledImports) {
213229
return;
214230
}
215231

216-
visitCssPropPath(path, { state, parentPath: path });
232+
visitCssPropPath(path, { context: 'root', state, parentPath: path });
217233
},
218234
},
219235
};

‎packages/babel-plugin/src/class-names/__tests__/behaviour.test.tsx

+183-122
Large diffs are not rendered by default.

‎packages/babel-plugin/src/class-names/index.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,8 @@ E.g: <ClassNames>{props => <div />}</ClassNames>`,
108108
*
109109
* `<ClassNames>{}</ClassNames>`
110110
*
111-
* @param path Babel path - expects to be a JSX opening element.
112-
* @param state Babel state - should house options and meta data used during the transformation.
111+
* @param path {NodePath} The opening JSX element
112+
* @param meta {Metadata} Useful metadata that can be used during the transformation
113113
*/
114114
export const visitClassNamesPath = (path: NodePath<t.JSXElement>, meta: Metadata): void => {
115115
if (

‎packages/babel-plugin/src/css-prop/__tests__/behaviour.test.tsx

+39
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ const transform = (code: string) => {
1111
};
1212

1313
describe('css prop behaviour', () => {
14+
beforeAll(() => {
15+
process.env.AUTOPREFIXER = 'off';
16+
});
17+
18+
afterAll(() => {
19+
delete process.env.AUTOPREFIXER;
20+
});
21+
1422
it('should not apply class name when no styles are present', () => {
1523
const actual = transform(`
1624
import '@compiled/react';
@@ -403,6 +411,37 @@ describe('css prop behaviour', () => {
403411
expect(actual).toInclude(':hover{color:red}');
404412
});
405413

414+
it('should handle an animation that references an inline @keyframes', () => {
415+
const actual = transform(`
416+
import { css } from '@compiled/react';
417+
418+
const helloWorld = css\`
419+
@keyframes fadeOut {
420+
from {
421+
opacity: 1;
422+
}
423+
50% {
424+
opacity: 0.5;
425+
}
426+
to {
427+
opacity: 0;
428+
}
429+
}
430+
431+
animation: fadeOut 2s ease-in-out;
432+
\`;
433+
434+
<div css={helloWorld}>hello world</div>
435+
`);
436+
437+
expect(actual).toIncludeMultiple([
438+
'const _2="._y44vk4ag{animation:fadeOut 2s ease-in-out}"',
439+
'const _="@keyframes fadeOut{0%{opacity:1}50%{opacity:0.5}to{opacity:0}}"',
440+
'<CS>{[_,_2]}</CS>',
441+
'className={ax(["_y44vk4ag"])}',
442+
]);
443+
});
444+
406445
it('should apply conditional logical expression object spread styles', () => {
407446
const actual = transform(`
408447
import '@compiled/react';

‎packages/babel-plugin/src/css-prop/index.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ const getJsxAttributeExpression = (node: t.JSXAttribute) => {
2121
*
2222
* `<div css={{}}>`
2323
*
24-
* @param path Babel path - expects to be a JSX opening element.
25-
* @param state Babel state - should house options and meta data used during the transformation.
24+
* @param path {NodePath} The opening JSX element
25+
* @param meta {Metadata} Useful metadata that can be used during the transformation
2626
*/
2727
export const visitCssPropPath = (path: NodePath<t.JSXOpeningElement>, meta: Metadata): void => {
2828
let cssPropIndex = -1;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
export const longhandCssPropObjectCallExpression = `
2+
<div css={{
3+
animationDuration: '2s',
4+
animationName: fadeOut,
5+
animationTimingFunction: 'ease-in-out',
6+
}} />
7+
`;
8+
9+
export const longhandCssPropTaggedTemplateExpression = `
10+
<div css={css\`
11+
animation-duration: 2s;
12+
animation-name: \${fadeOut};
13+
animation-timing-function: ease-in-out;
14+
\`} />
15+
`;
16+
17+
export const longhandStyledObjectCallExpression = `
18+
const StyledComponent = styled.div({
19+
animationDuration: '2s',
20+
animationName: fadeOut,
21+
animationTimingFunction: 'ease-in-out',
22+
});
23+
`;
24+
25+
export const longhandStyledTaggedTemplateExpression = `
26+
const StyledComponent = styled.div\`
27+
animation-duration: 2s;
28+
animation-name: \${fadeOut};
29+
animation-timing-function: ease-in-out;
30+
\`;
31+
`;
32+
33+
export const shorthandCssPropObjectCallExpression = `<div css={{ animation: \`\${fadeOut} 2s ease-in-out\` }} />`;
34+
35+
export const shorthandCssPropTaggedTemplateExpression = `<div css={css\`animation: \${fadeOut} 2s ease-in-out\`} />`;
36+
37+
export const shorthandStyledObjectCallExpression = `
38+
const StyledComponent = styled.div({
39+
animation: \`\${fadeOut} 2s ease-in-out\`,
40+
});
41+
`;
42+
43+
export const shorthandStyledTaggedTemplateExpression = `
44+
const StyledComponent = styled.div\`
45+
animation: \${fadeOut} 2s ease-in-out;
46+
\`;
47+
`;

‎packages/babel-plugin/src/keyframes/__tests__/call-expression.test.tsx

+1,493
Large diffs are not rendered by default.

‎packages/babel-plugin/src/keyframes/__tests__/tagged-template.test.tsx

+1,006
Large diffs are not rendered by default.

‎packages/babel-plugin/src/styled/__tests__/behaviour.test.tsx

+37
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ const transform = (code: string) => {
1111
};
1212

1313
describe('styled component behaviour', () => {
14+
beforeAll(() => {
15+
process.env.AUTOPREFIXER = 'off';
16+
});
17+
18+
afterAll(() => {
19+
delete process.env.AUTOPREFIXER;
20+
});
21+
1422
it('should generate styled object component code', () => {
1523
const actual = transform(`
1624
import { styled, ThemeProvider } from '@compiled/react';
@@ -319,6 +327,35 @@ describe('styled component behaviour', () => {
319327
`);
320328
});
321329

330+
it('should handle an animation that references an inline @keyframes', () => {
331+
const actual = transform(`
332+
import { styled } from '@compiled/react';
333+
334+
const ListItem = styled.div\`
335+
@keyframes fadeOut {
336+
from {
337+
opacity: 1;
338+
}
339+
50% {
340+
opacity: 0.5;
341+
}
342+
to {
343+
opacity: 0;
344+
}
345+
}
346+
347+
animation: fadeOut 2s ease-in-out;
348+
\`;
349+
`);
350+
351+
expect(actual).toIncludeMultiple([
352+
'const _2="._y44vk4ag{animation:fadeOut 2s ease-in-out}"',
353+
'const _="@keyframes fadeOut{0%{opacity:1}50%{opacity:0.5}to{opacity:0}}"',
354+
'<CS>{[_,_2]}</CS>',
355+
'className={ax(["_y44vk4ag",props.className])}',
356+
]);
357+
});
358+
322359
it('should not blow up with an expanding property', () => {
323360
expect(() =>
324361
transform(`

‎packages/babel-plugin/src/styled/index.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,8 @@ const hasInValidExpression = (node: t.TaggedTemplateExpression) => {
157157
*
158158
* `styled.div({})`
159159
*
160-
* @param path Babel path - expects to be a tagged template or call expression.
161-
* @param state Babel state - should house options and meta data used during the transformation.
160+
* @param path {NodePath} The tagged template or call expression
161+
* @param meta {Metadata} Useful metadata that can be used during the transformation
162162
*/
163163
export const visitStyledPath = (
164164
path: NodePath<t.TaggedTemplateExpression> | NodePath<t.CallExpression>,

‎packages/babel-plugin/src/types.tsx

+14-2
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,10 @@ export interface State extends PluginPass {
4949
* Means the `styled` api was found as `styledFunction` - as well as CSS prop is enabled in this module.
5050
*/
5151
compiledImports?: {
52-
styled?: string;
5352
ClassNames?: string;
5453
css?: string;
54+
keyframes?: string;
55+
styled?: string;
5556
};
5657

5758
/**
@@ -85,7 +86,7 @@ export interface State extends PluginPass {
8586
includedFiles: string[];
8687
}
8788

88-
export interface Metadata {
89+
interface CommonMetadata {
8990
/**
9091
* State of the current plugin run.
9192
*/
@@ -104,6 +105,17 @@ export interface Metadata {
104105
ownPath?: NodePath<any>;
105106
}
106107

108+
interface KeyframesMetadata extends CommonMetadata {
109+
context: 'keyframes';
110+
keyframe: string;
111+
}
112+
113+
interface RootMetadata extends CommonMetadata {
114+
context: 'root';
115+
}
116+
117+
export type Metadata = RootMetadata | KeyframesMetadata;
118+
107119
export interface Tag {
108120
/**
109121
* Name of the component.

‎packages/babel-plugin/src/utils/ast-builders.tsx

+17-16
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ export interface StyledTemplateOpts {
3838
* Hoists a sheet to the top of the module if its not already there.
3939
* Returns the referencing identifier.
4040
*
41-
* @param sheet Stylesheet
42-
* @param meta Plugin metadata
41+
* @param sheet {string} Stylesheet
42+
* @param meta {Metadata} Useful metadata that can be used during the transformation
4343
*/
4444
const hoistSheet = (sheet: string, meta: Metadata): t.Identifier => {
4545
if (meta.state.sheets[sheet]) {
@@ -290,8 +290,8 @@ export const buildDisplayName = (identifier: string, displayName: string = ident
290290
/**
291291
* Will return a generated AST for a Styled Component.
292292
*
293-
* @param opts Template options
294-
* @param meta Plugin metadata
293+
* @param opts {StyledTemplateOpts} Template options
294+
* @param meta {Metadata} Useful metadata that can be used during the transformation
295295
*/
296296
const styledTemplate = (opts: StyledTemplateOpts, meta: Metadata): t.Node => {
297297
const nonceAttribute = meta.state.opts.nonce ? `nonce={${meta.state.opts.nonce}}` : '';
@@ -374,8 +374,8 @@ const styledTemplate = (opts: StyledTemplateOpts, meta: Metadata): t.Node => {
374374
* This is primarily used for CSS prop and ClassNames apis.
375375
*
376376
* @param node Originating node
377-
* @param sheets Stylesheets
378-
* @param meta Metadata
377+
* @param sheets {string[]} Stylesheets
378+
* @param meta {Metadata} Useful metadata that can be used during the transformation
379379
*/
380380
export const compiledTemplate = (node: t.Expression, sheets: string[], meta: Metadata): t.Node => {
381381
const nonceAttribute = meta.state.opts.nonce ? `nonce={${meta.state.opts.nonce}}` : '';
@@ -434,19 +434,19 @@ export const conditionallyJoinExpressions = (
434434
/**
435435
* Returns a Styled Component AST.
436436
*
437-
* @param tag Styled tag either an inbuilt or user define
438-
* @param cssOutput CSS and variables to place onto the component
439-
* @param meta Plugin metadata
437+
* @param tag {Tag} Styled tag either an inbuilt or user define
438+
* @param cssOutput {CSSOutput} CSS and variables to place onto the component
439+
* @param meta {Metadata} Useful metadata that can be used during the transformation
440440
*/
441441
export const buildStyledComponent = (tag: Tag, cssOutput: CSSOutput, meta: Metadata): t.Node => {
442442
const unconditionalCss: string[] = [];
443443
const logicalCss: CssItem[] = [];
444444

445445
cssOutput.css.forEach((item) => {
446-
if (item.type === 'unconditional') {
447-
unconditionalCss.push(getItemCss(item));
448-
} else if (item.type === 'logical') {
446+
if (item.type === 'logical') {
449447
logicalCss.push(item);
448+
} else {
449+
unconditionalCss.push(getItemCss(item));
450450
}
451451
});
452452

@@ -506,7 +506,7 @@ export const getPropValue = (
506506
/**
507507
* Transforms CSS output into `sheets` and `classNames` ASTs.
508508
*
509-
* @param cssOutput CSSOutput
509+
* @param cssOutput {CSSOutput}
510510
*/
511511
const transformItemCss = (cssOutput: CSSOutput) => {
512512
const sheets: string[] = [];
@@ -525,9 +525,10 @@ const transformItemCss = (cssOutput: CSSOutput) => {
525525
);
526526
break;
527527

528-
case 'unconditional':
529528
default:
530-
classNames.push(t.stringLiteral(className));
529+
if (className) {
530+
classNames.push(t.stringLiteral(className));
531+
}
531532
break;
532533
}
533534
});
@@ -540,7 +541,7 @@ const transformItemCss = (cssOutput: CSSOutput) => {
540541
*
541542
* @param node Originating node
542543
* @param cssOutput CSS and variables to place onto the component
543-
* @param meta Plugin metadata
544+
* @param meta {Metadata} Useful metadata that can be used during the transformation
544545
*/
545546
export const buildCompiledComponent = (
546547
node: t.JSXElement,

‎packages/babel-plugin/src/utils/ast.tsx

+47-10
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ export const getPathOfNode = <TNode extends unknown>(
4141
/**
4242
* Returns `true` if the expression is using `css` from `@compiled/react`.
4343
*
44-
* @param node
45-
* @param meta
46-
* @returns
44+
* @param node {t.Expression} The expression that is being checked
45+
* @param meta {Metadata} Useful metadata that can be used during the transformation
46+
* @returns {boolean} Whether the node is a css usage from compiled
4747
*/
4848
export const isCompiledCSSTemplateLiteral = (
4949
node: t.Expression,
@@ -56,6 +56,42 @@ export const isCompiledCSSTemplateLiteral = (
5656
);
5757
};
5858

59+
/**
60+
* Returns `true` if the expression is using `keyframes` from `@compiled/react` as a tagged template expression.
61+
*
62+
* @param node {t.Node} The expression that is being checked
63+
* @param meta {Metadata} Useful metadata that can be used during the transformation
64+
* @returns {boolean} Whether the node is a compiled keyframe
65+
*/
66+
export const isCompiledKeyframesTaggedTemplateExpression = (
67+
node: t.Node,
68+
meta: Metadata
69+
): node is t.TaggedTemplateExpression => {
70+
return (
71+
t.isTaggedTemplateExpression(node) &&
72+
t.isIdentifier(node.tag) &&
73+
node.tag.name === meta.state.compiledImports?.keyframes
74+
);
75+
};
76+
77+
/**
78+
* Returns `true` if the expression is using `keyframes` from `@compiled/react` as a call expression.
79+
*
80+
* @param node {t.Node} The expression that is being checked
81+
* @param meta {Metadata} Useful metadata that can be used during the transformation
82+
* @returns {boolean} Whether the node is a compiled keyframe
83+
*/
84+
export const isCompiledKeyframesCallExpression = (
85+
node: t.Node,
86+
meta: Metadata
87+
): node is t.CallExpression => {
88+
return (
89+
t.isCallExpression(node) &&
90+
t.isIdentifier(node.callee) &&
91+
node.callee.name === meta.state.compiledImports?.keyframes
92+
);
93+
};
94+
5995
/**
6096
* Builds a code frame error from a passed in node.
6197
*
@@ -254,7 +290,7 @@ export const isPathReferencingAnyMutatedIdentifiers = (path: NodePath<any>): boo
254290
* else it will return the fallback node.
255291
*
256292
* @param node Node to evaluate
257-
* @param meta
293+
* @param meta {Metadata} Useful metadata that can be used during the transformation
258294
* @param fallbackNode Optional node to return if evaluation is not successful. Defaults to `node`.
259295
*/
260296
export const babelEvaluateExpression = (
@@ -397,8 +433,8 @@ export const resolveIdentifierComingFromDestructuring = ({
397433
* it will search recursively and resolve to `NumericalLiteral` node `10`.
398434
*
399435
* @param expression Node inside which we have to resolve the value
400-
* @param meta Plugin metadata
401-
* @param referenceName Reference name for which `binding` to be resolved
436+
* @param meta {Metadata} Useful metadata that can be used during the transformation
437+
* @param referenceName {string} Reference name for which `binding` to be resolved
402438
*/
403439
const resolveObjectPatternValueNode = (
404440
expression: t.Expression,
@@ -485,11 +521,11 @@ const getDestructuredObjectPatternKey = (node: t.ObjectPattern, referenceName: s
485521
* Will return the `node` of the a binding.
486522
* This function will follow import specifiers to return the actual `node`.
487523
*
488-
* When wanting to do futher traversal on the resulting `node` make sure to use the output `meta` as well.
524+
* When wanting to do further traversal on the resulting `node` make sure to use the output `meta` as well.
489525
* The `meta` will be for the resulting file it was found in.
490526
*
491-
* @param referenceName Reference name for which `binding` to be resolved
492-
* @param meta Plugin metadata
527+
* @param referenceName {string} Reference name for which `binding` to be resolved
528+
* @param meta {Metadata} Useful metadata that can be used during the transformation
493529
*/
494530
export const resolveBindingNode = (
495531
referenceName: string,
@@ -564,7 +600,8 @@ export const resolveBindingNode = (
564600
value: () => findDefaultExportModuleNode(ast),
565601
}));
566602
} else if (binding.path.isImportSpecifier()) {
567-
const exportName = binding.path.node.local.name;
603+
const { imported } = binding.path.node;
604+
const exportName = t.isIdentifier(imported) ? imported.name : imported.value;
568605

569606
({ foundNode, foundParentPath } = meta.state.cache.load({
570607
namespace: 'find-named-export-module-node',

‎packages/babel-plugin/src/utils/css-builders.tsx

+155-57
Large diffs are not rendered by default.

‎packages/babel-plugin/src/utils/evaluate-expression.tsx

+27-27
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ import traverse from '@babel/traverse';
44
import { Metadata } from '../types';
55

66
import {
7-
resolveBindingNode,
7+
babelEvaluateExpression,
88
getMemberExpressionMeta,
9+
getPathOfNode,
910
getValueFromObjectExpression,
10-
babelEvaluateExpression,
11+
isCompiledKeyframesCallExpression,
12+
resolveBindingNode,
1113
wrapNodeInIIFE,
12-
getPathOfNode,
13-
isCompiledCSSTemplateLiteral,
1414
} from './ast';
1515

1616
const createResultPair = (value: t.Expression, meta: Metadata) => ({
@@ -25,7 +25,7 @@ const createResultPair = (value: t.Expression, meta: Metadata) => ({
2525
* passing the `color` identifier to this function would return `'blue'`.
2626
*
2727
* @param expression Expression we want to interrogate.
28-
* @param state Babel state - should house options and meta data used during the transformation.
28+
* @param meta {Metadata} Useful metadata that can be used during the transformation
2929
*/
3030
const traverseIdentifier = (expression: t.Identifier, meta: Metadata) => {
3131
let value: t.Node | undefined | null = undefined;
@@ -51,7 +51,7 @@ const traverseIdentifier = (expression: t.Identifier, meta: Metadata) => {
5151
* return `value` as `10`.
5252
* @param expression Expression we want to interrogate.
5353
* @param accessPath An array of nested object keys
54-
* @param meta Meta data used during the transformation.
54+
* @param meta {Metadata} Useful metadata that can be used during the transformation
5555
*/
5656
const evaluateObjectExpression = (
5757
expression: t.Expression,
@@ -79,7 +79,7 @@ const evaluateObjectExpression = (
7979
*
8080
* @param expression Expression we want to interrogate.
8181
* @param accessPath An array of nested object keys
82-
* @param meta Meta data used during the transformation.
82+
* @param meta {Metadata} Useful metadata that can be used during the transformation
8383
*/
8484
const evaluateCallExpressionBindingMemberExpression = (
8585
expression: t.Expression,
@@ -113,7 +113,7 @@ const evaluateCallExpressionBindingMemberExpression = (
113113
*
114114
* @param expression Expression we want to interrogate.
115115
* @param accessPath An array of nested object keys
116-
* @param meta Meta data used during the transformation.
116+
* @param meta {Metadata} Useful metadata that can be used during the transformation
117117
*/
118118
const evaluateIdentifierBindingMemberExpression = (
119119
expression: t.Expression,
@@ -141,7 +141,7 @@ const evaluateIdentifierBindingMemberExpression = (
141141
* passing the `colors` identifier to this function would return `'blue'`.
142142
*
143143
* @param expression Expression we want to interrogate.
144-
* @param state Babel state - should house options and meta data used during the transformation.
144+
* @param meta {Metadata} Useful metadata that can be used during the transformation
145145
*/
146146
const traverseMemberExpression = (expression: t.MemberExpression, meta: Metadata) => {
147147
let value: t.Node | undefined | null = undefined;
@@ -182,7 +182,7 @@ const traverseMemberExpression = (expression: t.MemberExpression, meta: Metadata
182182
* passing the `size` identifier to this function would return `10` (it will recursively evaluate).
183183
*
184184
* @param expression Expression we want to interrogate.
185-
* @param state Babel state - should house options and meta data used during the transformation.
185+
* @param meta {Metadata} Useful metadata that can be used during the transformation
186186
*/
187187
const traverseFunction = (expression: t.Function, meta: Metadata) => {
188188
let value: t.Node | undefined | null = undefined;
@@ -215,7 +215,7 @@ const traverseFunction = (expression: t.Function, meta: Metadata) => {
215215
* we will look for its binding in own scope first, then parent scope.
216216
*
217217
* @param expression Expression we want to interrogate.
218-
* @param meta Meta data used during the transformation.
218+
* @param meta {Metadata} Useful metadata that can be used during the transformation
219219
*/
220220
const traverseCallExpression = (expression: t.CallExpression, meta: Metadata) => {
221221
const callee = expression.callee;
@@ -326,7 +326,7 @@ const traverseCallExpression = (expression: t.CallExpression, meta: Metadata) =>
326326
* and object expressions.
327327
*
328328
* @param expression Expression we want to interrogate.
329-
* @param state Babel state - should house options and meta data used during the transformation.
329+
* @param meta {Metadata} Useful metadata that can be used during the transformation
330330
*/
331331
export const evaluateExpression = (
332332
expression: t.Expression,
@@ -335,6 +335,12 @@ export const evaluateExpression = (
335335
let value: t.Node | undefined | null = undefined;
336336
let updatedMeta: Metadata = meta;
337337

338+
// --------------
339+
// NOTE: We are recursively calling evaluateExpression() which is then going to try and evaluate it
340+
// multiple times. This may or may not be a performance problem - when looking for quick wins perhaps
341+
// there is something we could do better here.
342+
// --------------
343+
338344
if (t.isIdentifier(expression)) {
339345
({ value, meta: updatedMeta } = traverseIdentifier(expression, updatedMeta));
340346
} else if (t.isMemberExpression(expression)) {
@@ -345,26 +351,20 @@ export const evaluateExpression = (
345351
({ value, meta: updatedMeta } = traverseCallExpression(expression, updatedMeta));
346352
}
347353

348-
if (t.isStringLiteral(value) || t.isNumericLiteral(value) || t.isObjectExpression(value)) {
354+
if (
355+
t.isStringLiteral(value) ||
356+
t.isNumericLiteral(value) ||
357+
t.isObjectExpression(value) ||
358+
t.isTaggedTemplateExpression(value) ||
359+
// TODO this should be more generic
360+
(value && isCompiledKeyframesCallExpression(value, updatedMeta))
361+
) {
349362
return createResultPair(value, updatedMeta);
350363
}
351364

352-
// --------------
353-
// NOTE: We are recursively calling evaluateExpression() which is then going to try and evaluate it
354-
// multiple times. This may or may not be a performance problem - when looking for quick wins perhaps
355-
// there is something we could do better here.
356-
// --------------
357-
358365
if (value) {
359-
if (isCompiledCSSTemplateLiteral(value, updatedMeta)) {
360-
// !! NOT GREAT !!
361-
// Sometimes we want to return the evaluated value instead of the original expression
362-
// however this is an edge case. When we implement keyframes we'll want to re-think this a little.
363-
return createResultPair(value, updatedMeta);
364-
}
365-
366366
// If we fail to statically evaluate `value` we will return `expression` instead.
367-
// It's preferrable to use the identifier than its result if it can't be statically evaluated.
367+
// It's preferable to use the identifier than its result if it can't be statically evaluated.
368368
// E.g. say we got the result of an identifier `foo` as `bar()` -- its more preferable to return
369369
// `foo` instead of `bar()` for a single source of truth.
370370
const babelEvaluatedNode = babelEvaluateExpression(value, updatedMeta, expression);

‎packages/babel-plugin/src/utils/types.tsx

+7-2
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,15 @@ export interface LogicalCssItem {
1414
css: string;
1515
}
1616

17-
export type CssItem = UnconditionalCssItem | LogicalCssItem;
17+
export interface SheetCssItem {
18+
type: 'sheet';
19+
css: string;
20+
}
21+
22+
export type CssItem = UnconditionalCssItem | LogicalCssItem | SheetCssItem;
1823

1924
export interface CSSOutput {
20-
css: Array<CssItem>;
25+
css: CssItem[];
2126
variables: {
2227
name: string;
2328
expression: t.Expression;

‎packages/css/src/transform.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export const transformCss = (css: string): { sheets: string[]; classNames: strin
3030
expandShorthands(),
3131
atomicifyRules({ callback: (className) => classNames.push(className) }),
3232
sortAtRulePseudos(),
33-
autoprefixer(),
33+
...(process.env.AUTOPREFIXER === 'off' ? [] : [autoprefixer()]),
3434
whitespace,
3535
extractStyleSheets({ callback: (sheet: string) => sheets.push(sheet) }),
3636
]).process(css, {

‎packages/react/src/index.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export { keyframes } from './keyframes';
12
export { styled } from './styled';
23
export { ClassNames } from './class-names';
34
export { default as css } from './css';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { keyframes } from '@compiled/react';
2+
3+
export const fadeOut = keyframes({
4+
from: {
5+
opacity: 1,
6+
},
7+
to: {
8+
opacity: 0,
9+
},
10+
});
11+
12+
export const namedFadeOut = fadeOut;
13+
14+
export default fadeOut;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
import { keyframes, styled } from '@compiled/react';
2+
import { render } from '@testing-library/react';
3+
4+
import defaultFadeOut, { fadeOut as shadowedFadeOut, namedFadeOut } from '../__fixtures__';
5+
6+
const getOpacity = (str: string | number) => str;
7+
8+
const getKeyframe = (name: string) => {
9+
const styles = Array.from(
10+
document.body.querySelectorAll('style'),
11+
(style) => style.innerHTML
12+
).join('\n');
13+
14+
return styles.substring(styles.indexOf(`@keyframes ${name}`));
15+
};
16+
17+
describe('keyframes', () => {
18+
describe('referenced through a css prop', () => {
19+
describe('render an animation', () => {
20+
it('given an object call expression argument', () => {
21+
const fadeOut = keyframes({
22+
from: {
23+
opacity: 1,
24+
},
25+
to: {
26+
opacity: 0,
27+
},
28+
});
29+
30+
const { getByText } = render(<div css={{ animationName: fadeOut }}>hello world</div>);
31+
32+
expect(getByText('hello world')).toHaveCompiledCss('animation-name', 'k1m8j3od');
33+
expect(getKeyframe('k1m8j3od')).toMatchInlineSnapshot(
34+
`"@keyframes k1m8j3od{0%{opacity:1}to{opacity:0}}"`
35+
);
36+
});
37+
38+
it('given a template literal call expression argument', () => {
39+
const fadeOut = keyframes(`from { opacity: 1; } to { opacity: 0; }`);
40+
const { getByText } = render(<div css={{ animationName: fadeOut }}>hello world</div>);
41+
42+
expect(getByText('hello world')).toHaveCompiledCss('animation-name', 'klmf72q');
43+
expect(getKeyframe('klmf72q')).toMatchInlineSnapshot(
44+
`"@keyframes klmf72q{0%{opacity:1}to{opacity:0}}"`
45+
);
46+
});
47+
48+
it('given a string call expression argument', () => {
49+
const fadeOut = keyframes('from { opacity: 1; } to { opacity: 0; }');
50+
const { getByText } = render(<div css={{ animationName: fadeOut }}>hello world</div>);
51+
52+
expect(getByText('hello world')).toHaveCompiledCss('animation-name', 'k1b0zjii');
53+
expect(getKeyframe('k1b0zjii')).toMatchInlineSnapshot(
54+
`"@keyframes k1b0zjii{0%{opacity:1}to{opacity:0}}"`
55+
);
56+
});
57+
58+
it('given a tagged template expression', () => {
59+
const fadeOut = keyframes`
60+
from { opacity: 1; }
61+
to { opacity: 0; }
62+
`;
63+
64+
const { getByText } = render(<div css={{ animationName: fadeOut }}>hello world</div>);
65+
66+
expect(getByText('hello world')).toHaveCompiledCss('animation-name', 'k1vk0ha6');
67+
expect(getKeyframe('k1vk0ha6')).toMatchInlineSnapshot(
68+
`"@keyframes k1vk0ha6{0%{opacity:1}to{opacity:0}}"`
69+
);
70+
});
71+
72+
it('defined in a default import', () => {
73+
const { getByText } = render(
74+
<div css={{ animationName: defaultFadeOut }}>hello world</div>
75+
);
76+
77+
expect(getByText('hello world')).toHaveCompiledCss('animation-name', 'k1m8j3od');
78+
expect(getKeyframe('k1m8j3od')).toMatchInlineSnapshot(
79+
`"@keyframes k1m8j3od{0%{opacity:1}to{opacity:0}}"`
80+
);
81+
});
82+
83+
it('defined in an imported named import', () => {
84+
const { getByText } = render(<div css={{ animationName: namedFadeOut }}>hello world</div>);
85+
86+
expect(getByText('hello world')).toHaveCompiledCss('animation-name', 'k1m8j3od');
87+
expect(getKeyframe('k1m8j3od')).toMatchInlineSnapshot(
88+
`"@keyframes k1m8j3od{0%{opacity:1}to{opacity:0}}"`
89+
);
90+
});
91+
92+
it('defined in a local named import', () => {
93+
const { getByText } = render(
94+
<div css={{ animationName: shadowedFadeOut }}>hello world</div>
95+
);
96+
97+
expect(getByText('hello world')).toHaveCompiledCss('animation-name', 'k1m8j3od');
98+
expect(getKeyframe('k1m8j3od')).toMatchInlineSnapshot(
99+
`"@keyframes k1m8j3od{0%{opacity:1}to{opacity:0}}"`
100+
);
101+
});
102+
103+
it('containing a call expression', () => {
104+
const from = 1;
105+
const to = 0;
106+
107+
const fadeOut = keyframes({
108+
from: {
109+
opacity: getOpacity(from),
110+
},
111+
to: {
112+
opacity: getOpacity(to),
113+
},
114+
});
115+
116+
const { getByText } = render(<div css={{ animationName: fadeOut }}>hello world</div>);
117+
118+
expect(getByText('hello world')).toHaveCompiledCss('animation-name', 'kbrsk95');
119+
expect(getKeyframe('kbrsk95')).toMatchInlineSnapshot(
120+
`"@keyframes kbrsk95{0%{opacity:1}to{opacity:0}}"`
121+
);
122+
});
123+
124+
it('containing an identifier referencing a constant numeric literal', () => {
125+
const fromOpacity = 1;
126+
const toOpacity = 0;
127+
128+
const fadeOut = keyframes({
129+
from: {
130+
opacity: fromOpacity,
131+
},
132+
to: {
133+
opacity: toOpacity,
134+
},
135+
});
136+
137+
const { getByText } = render(<div css={{ animationName: fadeOut }}>hello world</div>);
138+
139+
expect(getByText('hello world')).toHaveCompiledCss('animation-name', 'korwhog');
140+
expect(getKeyframe('korwhog')).toMatchInlineSnapshot(
141+
`"@keyframes korwhog{0%{opacity:1}to{opacity:0}}"`
142+
);
143+
});
144+
145+
it('containing an identifier referencing a call expression', () => {
146+
const fromOpacity = getOpacity(1);
147+
const toOpacity = getOpacity(0);
148+
149+
const fadeOut = keyframes({
150+
from: {
151+
opacity: fromOpacity,
152+
},
153+
to: {
154+
opacity: toOpacity,
155+
},
156+
});
157+
158+
const { getByText } = render(<div css={{ animationName: fadeOut }}>hello world</div>);
159+
160+
expect(getByText('hello world')).toHaveCompiledCss('animation-name', 'korwhog');
161+
expect(getKeyframe('korwhog')).toMatchInlineSnapshot(
162+
`"@keyframes korwhog{0%{opacity:1}to{opacity:0}}"`
163+
);
164+
});
165+
});
166+
});
167+
168+
describe('referenced through a styled component', () => {
169+
describe('render an animation', () => {
170+
it('given an object call expression argument', () => {
171+
const fadeOut = keyframes({
172+
from: {
173+
opacity: 1,
174+
},
175+
to: {
176+
opacity: 0,
177+
},
178+
});
179+
180+
const Component = styled.div({ animationName: fadeOut });
181+
182+
const { getByText } = render(<Component>hello world</Component>);
183+
184+
expect(getByText('hello world')).toHaveCompiledCss('animation-name', 'k1m8j3od');
185+
expect(getKeyframe('k1m8j3od')).toMatchInlineSnapshot(
186+
`"@keyframes k1m8j3od{0%{opacity:1}to{opacity:0}}"`
187+
);
188+
});
189+
190+
it('given a template literal call expression argument', () => {
191+
const fadeOut = keyframes(`from { opacity: 1; } to { opacity: 0; }`);
192+
const Component = styled.div({ animationName: fadeOut });
193+
const { getByText } = render(<Component>hello world</Component>);
194+
195+
expect(getByText('hello world')).toHaveCompiledCss('animation-name', 'klmf72q');
196+
expect(getKeyframe('klmf72q')).toMatchInlineSnapshot(
197+
`"@keyframes klmf72q{0%{opacity:1}to{opacity:0}}"`
198+
);
199+
});
200+
201+
it('given a string call expression argument', () => {
202+
const fadeOut = keyframes('from { opacity: 1; } to { opacity: 0; }');
203+
const Component = styled.div({ animationName: fadeOut });
204+
const { getByText } = render(<Component>hello world</Component>);
205+
206+
expect(getByText('hello world')).toHaveCompiledCss('animation-name', 'k1b0zjii');
207+
expect(getKeyframe('k1b0zjii')).toMatchInlineSnapshot(
208+
`"@keyframes k1b0zjii{0%{opacity:1}to{opacity:0}}"`
209+
);
210+
});
211+
212+
it('given a tagged template expression', () => {
213+
const fadeOut = keyframes`
214+
from { opacity: 1; }
215+
to { opacity: 0; }
216+
`;
217+
218+
const Component = styled.div({ animationName: fadeOut });
219+
const { getByText } = render(<Component>hello world</Component>);
220+
221+
expect(getByText('hello world')).toHaveCompiledCss('animation-name', 'k1vk0ha6');
222+
expect(getKeyframe('k1vk0ha6')).toMatchInlineSnapshot(
223+
`"@keyframes k1vk0ha6{0%{opacity:1}to{opacity:0}}"`
224+
);
225+
});
226+
227+
it('defined in a default export', () => {
228+
const Component = styled.div({ animationName: defaultFadeOut });
229+
const { getByText } = render(<Component>hello world</Component>);
230+
231+
expect(getByText('hello world')).toHaveCompiledCss('animation-name', 'k1m8j3od');
232+
expect(getKeyframe('k1m8j3od')).toMatchInlineSnapshot(
233+
`"@keyframes k1m8j3od{0%{opacity:1}to{opacity:0}}"`
234+
);
235+
});
236+
237+
it('defined in an imported named import', () => {
238+
const Component = styled.div({ animationName: namedFadeOut });
239+
const { getByText } = render(<Component>hello world</Component>);
240+
241+
expect(getByText('hello world')).toHaveCompiledCss('animation-name', 'k1m8j3od');
242+
expect(getKeyframe('k1m8j3od')).toMatchInlineSnapshot(
243+
`"@keyframes k1m8j3od{0%{opacity:1}to{opacity:0}}"`
244+
);
245+
});
246+
247+
it('defined in a local named import', () => {
248+
const Component = styled.div({ animationName: shadowedFadeOut });
249+
const { getByText } = render(<Component>hello world</Component>);
250+
251+
expect(getByText('hello world')).toHaveCompiledCss('animation-name', 'k1m8j3od');
252+
expect(getKeyframe('k1m8j3od')).toMatchInlineSnapshot(
253+
`"@keyframes k1m8j3od{0%{opacity:1}to{opacity:0}}"`
254+
);
255+
});
256+
257+
it('containing a call expression', () => {
258+
const from = 1;
259+
const to = 0;
260+
261+
const fadeOut = keyframes({
262+
from: {
263+
opacity: getOpacity(from),
264+
},
265+
to: {
266+
opacity: getOpacity(to),
267+
},
268+
});
269+
270+
const Component = styled.div({ animationName: fadeOut });
271+
const { getByText } = render(<Component>hello world</Component>);
272+
273+
expect(getByText('hello world')).toHaveCompiledCss('animation-name', 'kbrsk95');
274+
expect(getKeyframe('kbrsk95')).toMatchInlineSnapshot(
275+
`"@keyframes kbrsk95{0%{opacity:1}to{opacity:0}}"`
276+
);
277+
});
278+
279+
it('containing an identifier referencing a constant numeric literal', () => {
280+
const fromOpacity = 1;
281+
const toOpacity = 0;
282+
283+
const fadeOut = keyframes({
284+
from: {
285+
opacity: fromOpacity,
286+
},
287+
to: {
288+
opacity: toOpacity,
289+
},
290+
});
291+
292+
const Component = styled.div({ animationName: fadeOut });
293+
const { getByText } = render(<Component>hello world</Component>);
294+
295+
expect(getByText('hello world')).toHaveCompiledCss('animation-name', 'korwhog');
296+
expect(getKeyframe('korwhog')).toMatchInlineSnapshot(
297+
`"@keyframes korwhog{0%{opacity:1}to{opacity:0}}"`
298+
);
299+
});
300+
301+
it('containing an identifier referencing a call expression', () => {
302+
const fromOpacity = getOpacity(1);
303+
const toOpacity = getOpacity(0);
304+
305+
const fadeOut = keyframes({
306+
from: {
307+
opacity: fromOpacity,
308+
},
309+
to: {
310+
opacity: toOpacity,
311+
},
312+
});
313+
314+
const Component = styled.div({ animationName: fadeOut });
315+
const { getByText } = render(<Component>hello world</Component>);
316+
317+
expect(getByText('hello world')).toHaveCompiledCss('animation-name', 'korwhog');
318+
expect(getKeyframe('korwhog')).toMatchInlineSnapshot(
319+
`"@keyframes korwhog{0%{opacity:1}to{opacity:0}}"`
320+
);
321+
});
322+
});
323+
});
324+
});
+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import type { BasicTemplateInterpolations, CSSProps } from '../types';
2+
import { createSetupError } from '../utils/error';
3+
4+
export type KeyframeSteps = string | Record<string, CSSProps>;
5+
6+
/**
7+
* Create keyframes using a tagged template expression:
8+
*
9+
* ```
10+
* const fadeOut = keyframes`
11+
* from { opacity: 1; }
12+
* to { opacity: 0; }
13+
* `;
14+
* ```
15+
*
16+
* @param _strings The input string values
17+
* @param _interpolations The arguments used in the expression
18+
*/
19+
export function keyframes(
20+
_strings: TemplateStringsArray,
21+
..._interpolations: BasicTemplateInterpolations[]
22+
): string;
23+
24+
/**
25+
* Create keyframes using:
26+
*
27+
* 1. An object expression
28+
*
29+
* ```
30+
* const fadeOut = keyframes({
31+
* from: {
32+
* opacity: 1,
33+
* },
34+
* to: {
35+
* opacity: 0,
36+
* },
37+
* });
38+
* ```
39+
*
40+
* 2. A string
41+
*
42+
* ```
43+
* const fadeOut = keyframes('from { opacity: 1; } to { opacity: 0; }');
44+
* ```
45+
*
46+
* 3. A template literal
47+
*
48+
* ```
49+
* const fadeOut = keyframes(`from { opacity: 1; } to { opacity: 0; }`);
50+
* ```
51+
*
52+
* @param _steps The waypoints along the animation sequence
53+
*/
54+
export function keyframes(_steps: KeyframeSteps): string;
55+
56+
export function keyframes(
57+
_stringsOrSteps: TemplateStringsArray | KeyframeSteps,
58+
..._interpolations: BasicTemplateInterpolations[]
59+
): string {
60+
throw createSetupError();
61+
}

0 commit comments

Comments
 (0)
Please sign in to comment.