Skip to content

Commit 34e5339

Browse files
Fix @compiled/babel-plugin to not silently skip cssMap() extraction in a few common edge-cases. (#1773)
* Fix `@compiled/babel-plugin` to not require `cssMap()` to be called prior to use. Example: this code failed before for no reason other than the fact that our `state.cssMap` was generated _after_ `JSXElement` and `JSXOpeningElement` were ran. ```tsx import { cssMap } from "@compiled/react"; export default () => <div css={styles.root} />; const styles = cssMap({ root: { padding: 8 } }); ``` * Attempt to resolve 'cssMap' calls when we hit a MemberExpression that does not have this defined. * Throw an error when compiling a `cssMap` object where we expect a `css` or nested `cssMap` object. (#1774) * Throw an error when compiling a `cssMap` object where we expect a `css` or nested `cssMap` object. Fixes #1642 Example of code that silently fails today, using `styles` directly: ```tsx import { cssMap } from '@compiled/react'; const styles = cssMap({ root: { padding: 8 } }); export default () => <div css={styles} />; ``` What we expect to see instead, using `styles.root` instead: ```tsx import { cssMap } from '@compiled/react'; const styles = cssMap({ root: { padding: 8 } }); export default () => <div css={styles.root} />; ``` * Handle the scenario where we use `css={styles}` before `cssMap()` is called Also, just update the tests which muddy `cssMap` vs. `css` scope — technically a missed scenario, but one that we shouldn't hit as this throws an error if compiled in one go.
1 parent b000220 commit 34e5339

File tree

8 files changed

+155
-12
lines changed

8 files changed

+155
-12
lines changed

.changeset/dirty-carrots-sit.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@compiled/babel-plugin': minor
3+
---
4+
5+
Fix `@compiled/babel-plugin` to not require `cssMap()` to be called prior to use.
6+
7+
Example, this failed before for no reason other than the fact that our `state.cssMap` was generated _after_ `JSXElement` and `JSXOpeningElement` were ran.
8+
9+
```tsx
10+
import { cssMap } from '@compiled/react';
11+
export default () => <div css={styles.root} />;
12+
const styles = cssMap({ root: { padding: 8 } });
13+
```

.changeset/silent-geese-wonder.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@compiled/babel-plugin': minor
3+
---
4+
5+
Throw an error when compiling a `cssMap` object where we expect a `css` or nested `cssMap` object.
6+
7+
Example of code that silently fails today, using `styles` directly:
8+
9+
```tsx
10+
import { cssMap } from '@compiled/react';
11+
const styles = cssMap({ root: { padding: 8 } });
12+
export default () => <div css={styles} />;
13+
```
14+
15+
What we expect to see instead, using `styles.root` instead:
16+
17+
```tsx
18+
import { cssMap } from '@compiled/react';
19+
const styles = cssMap({ root: { padding: 8 } });
20+
export default () => <div css={styles.root} />;
21+
```

packages/babel-plugin/src/babel-plugin.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export default declare<State>((api) => {
7676

7777
this.sheets = {};
7878
this.cssMap = {};
79+
this.ignoreMemberExpressions = {};
7980
let cache: Cache;
8081

8182
if (this.opts.cache === true) {
@@ -239,7 +240,6 @@ export default declare<State>((api) => {
239240
},
240241
ImportDeclaration(path, state) {
241242
const userLandModule = path.node.source.value;
242-
243243
const isCompiledModule = this.importSources.some((compiledModuleOrigin) => {
244244
if (compiledModuleOrigin === userLandModule) {
245245
return true;
@@ -282,6 +282,7 @@ export default declare<State>((api) => {
282282
const apiArray = state.compiledImports[apiName] || [];
283283
apiArray.push(specifier.node.local.name);
284284
state.compiledImports[apiName] = apiArray;
285+
285286
specifier.remove();
286287
}
287288
});
@@ -311,6 +312,7 @@ Reasons this might happen:
311312
path.parentPath
312313
);
313314
}
315+
314316
if (isCompiledCSSMapCallExpression(path.node, state)) {
315317
visitCssMapPath(path, { context: 'root', state, parentPath: path });
316318
return;

packages/babel-plugin/src/css-map/__tests__/index.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,25 @@ describe('css map basic functionality', () => {
3636
]);
3737
});
3838

39+
it('should transform css map even when the styles are defined below the component', () => {
40+
const actual = transform(`
41+
import { cssMap } from '@compiled/react';
42+
43+
const Component = () => <div>
44+
<span css={styles.danger} />
45+
<span css={styles.success} />
46+
</div>
47+
48+
const styles = cssMap(${styles});
49+
`);
50+
51+
expect(actual).toIncludeMultiple([
52+
'const styles={danger:"_syaz5scu _bfhk5scu",success:"_syazbf54 _bfhkbf54"};',
53+
'<span className={ax([styles.danger])}/>',
54+
'<span className={ax([styles.success])}/>',
55+
]);
56+
});
57+
3958
it('should transform css map even with an empty object', () => {
4059
const actual = transform(`
4160
import { css, cssMap } from '@compiled/react';
@@ -88,6 +107,30 @@ describe('css map basic functionality', () => {
88107
]);
89108
});
90109

110+
it('should error out if the root cssMap object is being directly called', () => {
111+
expect(() => {
112+
transform(`
113+
import { cssMap } from '@compiled/react';
114+
115+
const styles = cssMap({ root: { color: 'red' } });
116+
117+
// Eg. we expect 'styles.root' here instead of 'styles'
118+
<div css={styles} />
119+
`);
120+
}).toThrow(ErrorMessages.USE_VARIANT_OF_CSS_MAP);
121+
122+
expect(() => {
123+
transform(`
124+
import { cssMap } from '@compiled/react';
125+
126+
// Eg. we expect 'styles.root' here instead of 'styles'
127+
<div css={styles} />
128+
129+
const styles = cssMap({ root: { color: 'red' } });
130+
`);
131+
}).toThrow(ErrorMessages.USE_VARIANT_OF_CSS_MAP);
132+
});
133+
91134
it('should error out if variants are not defined at the top-most scope of the module.', () => {
92135
expect(() => {
93136
transform(`

packages/babel-plugin/src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,11 @@ export interface State extends PluginPass {
194194
*/
195195
cssMap: Record<string, string[]>;
196196

197+
/**
198+
* Holdings a record of member expression names to ignore
199+
*/
200+
ignoreMemberExpressions: Record<string, true>;
201+
197202
/**
198203
* A custom resolver used to statically evaluate import declarations
199204
*/

packages/babel-plugin/src/utils/css-builders.ts

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
import generate from '@babel/generator';
2+
import type { NodePath } from '@babel/traverse';
23
import * as t from '@babel/types';
34
import { addUnitIfNeeded, cssAffixInterpolation } from '@compiled/css';
45
import { hash, kebabCase } from '@compiled/utils';
56

7+
import { visitCssMapPath } from '../css-map';
68
import type { Metadata } from '../types';
79

810
import { buildCodeFrameError } from './ast';
911
import { CONDITIONAL_PATHS } from './constants';
12+
import { createErrorMessage, ErrorMessages } from './css-map';
1013
import { evaluateExpression } from './evaluate-expression';
1114
import {
1215
isCompiledCSSCallExpression,
16+
isCompiledCSSMapCallExpression,
1317
isCompiledCSSTaggedTemplateExpression,
1418
isCompiledKeyframesCallExpression,
1519
isCompiledKeyframesTaggedTemplateExpression,
@@ -665,6 +669,45 @@ const extractObjectExpression = (node: t.ObjectExpression, meta: Metadata): CSSO
665669
return { css: mergeSubsequentUnconditionalCssItems(css), variables };
666670
};
667671

672+
/**
673+
* If we don't yet have a `meta.state.cssMap[node.name]` built yet, try to build and cache it, eg. in this scenario:
674+
* ```tsx
675+
* const Component = () => <div css={styles.root} />
676+
* const styles = cssMap({ root: { padding: 0 } });
677+
* ```
678+
*
679+
* If we don't find this is a `cssMap()` call, we put it into `ignoreMemberExpressions` to ignore on future runs.
680+
*
681+
* @returns {Boolean} Whether the cache was generated
682+
*/
683+
const generateCacheForCSSMap = (node: t.Identifier, meta: Metadata): void => {
684+
if (meta.state.cssMap[node.name] || meta.state.ignoreMemberExpressions[node.name]) {
685+
return;
686+
}
687+
688+
const resolved = resolveBinding(node.name, meta, evaluateExpression);
689+
if (resolved && isCompiledCSSMapCallExpression(resolved.node, meta.state)) {
690+
let resolvedCallPath = resolved.path.get('init');
691+
if (Array.isArray(resolvedCallPath)) {
692+
resolvedCallPath = resolvedCallPath[0];
693+
}
694+
695+
if (t.isCallExpression(resolvedCallPath.node)) {
696+
// This visits the cssMap path and caches the styles
697+
visitCssMapPath(resolvedCallPath as NodePath<t.CallExpression>, {
698+
context: 'root',
699+
state: meta.state,
700+
parentPath: resolved.path,
701+
});
702+
}
703+
}
704+
705+
if (!meta.state.cssMap[node.name]) {
706+
// If this cannot be found, it's likely not a `cssMap` identifier and we shouldn't parse it again on future runs…
707+
meta.state.ignoreMemberExpressions[node.name] = true;
708+
}
709+
};
710+
668711
/**
669712
* Extracts CSS data from a member expression node (eg. `styles.primary`)
670713
*
@@ -688,11 +731,16 @@ function extractMemberExpression(
688731
fallbackToEvaluate = true
689732
): CSSOutput | undefined {
690733
const bindingIdentifier = findBindingIdentifier(node);
691-
if (bindingIdentifier && meta.state.cssMap[bindingIdentifier.name]) {
692-
return {
693-
css: [{ type: 'map', expression: node, name: bindingIdentifier.name, css: '' }],
694-
variables: [],
695-
};
734+
735+
if (bindingIdentifier) {
736+
// In some cases, the `state.cssMap` is not warmed yet, so run it:
737+
generateCacheForCSSMap(bindingIdentifier, meta);
738+
if (meta.state.cssMap[bindingIdentifier.name]) {
739+
return {
740+
css: [{ type: 'map', expression: node, name: bindingIdentifier.name, css: '' }],
741+
variables: [],
742+
};
743+
}
696744
}
697745

698746
if (fallbackToEvaluate) {
@@ -956,6 +1004,16 @@ export const buildCss = (node: t.Expression | t.Expression[], meta: Metadata): C
9561004
);
9571005
}
9581006

1007+
// In some cases, the `state.cssMap` is not warmed yet, so run it:
1008+
generateCacheForCSSMap(node, meta);
1009+
if (meta.state.cssMap[node.name]) {
1010+
throw buildCodeFrameError(
1011+
createErrorMessage(ErrorMessages.USE_VARIANT_OF_CSS_MAP),
1012+
node,
1013+
meta.parentPath
1014+
);
1015+
}
1016+
9591017
const result = buildCss(resolvedBinding.node, resolvedBinding.meta);
9601018

9611019
assertNoImportedCssVariables(node, meta, resolvedBinding, result);

packages/babel-plugin/src/utils/css-map.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ export enum ErrorMessages {
8686
STATIC_PROPERTY_KEY = 'Property key may only be a static string.',
8787
SELECTOR_BLOCK_WRONG_PLACE = '`selector` key was defined in the wrong place.',
8888
USE_SELECTORS_WITH_AMPERSAND = 'This selector is applied to the parent element, and so you need to specify the ampersand symbol (&) directly before it. For example, `:hover` should be written as `&:hover`.',
89+
USE_VARIANT_OF_CSS_MAP = 'You must use the variant of a CSS Map object (eg. `styles.root`), not the root object itself, eg. `styles`.',
8990
}
9091

9192
export const createErrorMessage = (message: string): string => {

packages/react/src/create-strict-api/__tests__/generics.test.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ describe('createStrictAPI()', () => {
1919
});
2020

2121
it('should mark all styles as optional in cssMap()', () => {
22-
const styles = cssMap({
22+
const stylesMap = cssMap({
2323
nested: {
2424
'&:hover': {},
2525
'&:active': {},
@@ -28,7 +28,7 @@ describe('createStrictAPI()', () => {
2828
},
2929
});
3030

31-
const { getByTestId } = render(<div css={styles.nested} data-testid="div" />);
31+
const { getByTestId } = render(<div css={stylesMap.nested} data-testid="div" />);
3232

3333
expect(getByTestId('div')).toBeDefined();
3434
});
@@ -96,7 +96,7 @@ describe('createStrictAPI()', () => {
9696
});
9797

9898
it('should violate types for cssMap()', () => {
99-
const styles = cssMap({
99+
const stylesMap = cssMap({
100100
primary: {
101101
// @ts-expect-error — Type '""' is not assignable to type ...
102102
color: '',
@@ -129,7 +129,7 @@ describe('createStrictAPI()', () => {
129129
},
130130
});
131131

132-
const { getByTestId } = render(<div css={styles.primary} data-testid="div" />);
132+
const { getByTestId } = render(<div css={stylesMap.primary} data-testid="div" />);
133133

134134
expect(getByTestId('div')).toBeDefined();
135135
});
@@ -224,7 +224,7 @@ describe('createStrictAPI()', () => {
224224
});
225225

226226
it('should pass type check for cssMap()', () => {
227-
const styles = cssMap({
227+
const stylesMap = cssMap({
228228
primary: {
229229
color: 'var(--ds-text)',
230230
backgroundColor: 'var(--ds-bold)',
@@ -258,7 +258,7 @@ describe('createStrictAPI()', () => {
258258
},
259259
});
260260

261-
const { getByTestId } = render(<div css={styles.primary} data-testid="div" />);
261+
const { getByTestId } = render(<div css={stylesMap.primary} data-testid="div" />);
262262

263263
expect(getByTestId('div')).toHaveCompiledCss('color', 'var(--ds-text)');
264264
});

0 commit comments

Comments
 (0)