Skip to content

Commit 4caa678

Browse files
itsdougesAlex Hinds
andauthored
Component declared styles (#1533)
* chore: add example type test * feat: introduce stricter types part 1 * chore: update docs * feat: wire up xcss prop * feat: adds xcss inline object support * feat: add support for simple collection of pass styles * chore: move * feat: expose cx util * chore: add failing test * chore: remove strict types and replace with direct cx usage * chore: fix types * chore: code review * feat: block at rules from xcss prop * chore: add css prop test * fix: update jsx namespace * chore: add tests and jsdoc * chore: code review * chore: update changeset message * chore: update type from code review * chore: fix spelling * chore: resolve code review comments * chore: remove unused * chore: fix types * chore: ensure selectors isnt available * feat: add scope option --------- Co-authored-by: Alex Hinds <[email protected]>
1 parent 4a5449c commit 4caa678

File tree

15 files changed

+808
-94
lines changed

15 files changed

+808
-94
lines changed

.changeset/brown-students-share.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
---
2+
'@compiled/babel-plugin': patch
3+
'@compiled/react': patch
4+
---
5+
6+
The xcss prop is now available.
7+
Declare styles your component takes with all other styles marked as violations
8+
by the TypeScript compiler. There are two primary use cases for xcss prop:
9+
10+
- safe style overrides
11+
- inverting style declarations
12+
13+
Interverting style declarations is interesting for platform teams as
14+
it means products only pay for styles they use as they're now the ones who declare
15+
the styles!
16+
17+
The `XCSSProp` type has generics which must be defined — of which should be what you
18+
explicitly want to maintain as API. Use `XCSSAllProperties` and `XCSSAllPseudos` types
19+
to enable all properties and pseudos.
20+
21+
```tsx
22+
import { type XCSSProp } from '@compiled/react';
23+
24+
interface MyComponentProps {
25+
// Color is accepted, all other properties / pseudos are considered violations.
26+
xcss?: XCSSProp<'color', never>;
27+
28+
// Only backgrond color and hover pseudo is accepted.
29+
xcss?: XCSSProp<'backgroundColor', '&:hover'>;
30+
31+
// All properties are accepted, all pseudos are considered violations.
32+
xcss?: XCSSProp<XCSSAllProperties, never>;
33+
34+
// All properties are accepted, only the hover pseudo is accepted.
35+
xcss?: XCSSProp<XCSSAllProperties, '&:hover'>;
36+
}
37+
38+
function MyComponent({ xcss }: MyComponentProps) {
39+
return <div css={{ color: 'var(--ds-text-danger)' }} className={xcss} />;
40+
}
41+
```
42+
43+
The xcss prop works with static inline objects and the [cssMap](https://compiledcssinjs.com/docs/api-cssmap) API.
44+
45+
```tsx
46+
// Declared as an inline object
47+
<Component xcss={{ color: 'var(--ds-text)' }} />;
48+
49+
// Declared with the cssMap API
50+
const styles = cssMap({ text: { color: 'var(--ds-text)' } });
51+
<Component xcss={styles.text} />;
52+
```
53+
54+
To concatenate and conditonally apply styles use the `cssMap` and `cx` functions.

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
isCompiledCSSMapCallExpression,
2525
} from './utils/is-compiled';
2626
import { normalizePropsUsage } from './utils/normalize-props-usage';
27+
import { visitXcssPropPath } from './xcss-prop';
2728

2829
// eslint-disable-next-line @typescript-eslint/no-var-requires
2930
const packageJson = require('../package.json');
@@ -65,6 +66,8 @@ export default declare<State>((api) => {
6566
paths: [state.opts.root ?? this.cwd],
6667
}));
6768
}
69+
70+
this.transformCache = new WeakMap();
6871
},
6972
visitor: {
7073
Program: {
@@ -87,6 +90,15 @@ export default declare<State>((api) => {
8790
}
8891
}
8992
}
93+
94+
if (
95+
!state.opts.requireCompiledInScopeForXCSSProp &&
96+
!state.compiledImports &&
97+
/(x|X)css={/.exec(file.code)
98+
) {
99+
// xcss prop was found turn on Compiled
100+
state.compiledImports = {};
101+
}
90102
},
91103
exit(path, state) {
92104
if (!state.compiledImports) {
@@ -229,6 +241,7 @@ export default declare<State>((api) => {
229241
return;
230242
}
231243

244+
visitXcssPropPath(path, { context: 'root', state, parentPath: path });
232245
visitCssPropPath(path, { context: 'root', state, parentPath: path });
233246
},
234247
},

packages/babel-plugin/src/types.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,16 @@ export interface PluginOptions {
2727
*/
2828
importReact?: boolean;
2929

30+
/**
31+
* By default the `xcss` prop works by just using it. To aid repositories
32+
* migrating to Compiled `xcss` prop, you can use this config to have it
33+
* only be enabled when Compiled has been activated either by jsx pragma
34+
* or other Compiled APIs.
35+
*
36+
* Defaults to `false`.
37+
*/
38+
requireCompiledInScopeForXCSSProp?: boolean;
39+
3040
/**
3141
* Security nonce that will be applied to inline style elements if defined.
3242
*/
@@ -144,14 +154,19 @@ export interface State extends PluginPass {
144154
includedFiles: string[];
145155

146156
/**
147-
* Holds a record of currently evaluated CSS Map and its sheets in the module.
157+
* Holds a record of evaluated `cssMap()` calls with their compiled style sheets in the current pass.
148158
*/
149159
cssMap: Record<string, string[]>;
150160

151161
/**
152162
* A custom resolver used to statically evaluate import declarations
153163
*/
154164
resolver?: Resolver;
165+
166+
/**
167+
* Holds paths that have been transformed that we can ignore.
168+
*/
169+
transformCache: WeakMap<NodePath<any>, true>;
155170
}
156171

157172
interface CommonMetadata {
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import { transform } from '../../test-utils';
2+
3+
describe('xcss prop transformation', () => {
4+
it('should transform static inline object', () => {
5+
const result = transform(`
6+
<Component xcss={{ color: 'red' }} />
7+
`);
8+
9+
expect(result).toMatchInlineSnapshot(`
10+
"import * as React from "react";
11+
import { ax, ix, CC, CS } from "@compiled/react/runtime";
12+
const _ = "._syaz5scu{color:red}";
13+
<CC>
14+
<CS>{[_]}</CS>
15+
{<Component xcss={"_syaz5scu"} />}
16+
</CC>;
17+
"
18+
`);
19+
});
20+
21+
it('should throw when not static', () => {
22+
expect(() => {
23+
transform(
24+
`
25+
import { bar } from './foo';
26+
27+
<Component xcss={{ color: bar }} />
28+
`,
29+
{ highlightCode: false }
30+
);
31+
}).toThrowErrorMatchingInlineSnapshot(`
32+
"unknown file: Object given to the xcss prop must be static (4:23).
33+
2 | import { bar } from './foo';
34+
3 |
35+
> 4 | <Component xcss={{ color: bar }} />
36+
| ^^^^^^^^^^^^^^
37+
5 | "
38+
`);
39+
});
40+
41+
it('should transform named xcss prop usage', () => {
42+
const result = transform(`
43+
<Component innerXcss={{ color: 'red' }} />
44+
`);
45+
46+
expect(result).toMatchInlineSnapshot(`
47+
"import * as React from "react";
48+
import { ax, ix, CC, CS } from "@compiled/react/runtime";
49+
const _ = "._syaz5scu{color:red}";
50+
<CC>
51+
<CS>{[_]}</CS>
52+
{<Component innerXcss={"_syaz5scu"} />}
53+
</CC>;
54+
"
55+
`);
56+
});
57+
58+
it('should work with css map', () => {
59+
const result = transform(`
60+
import { cssMap } from '@compiled/react';
61+
62+
const styles = cssMap({
63+
primary: { color: 'red' },
64+
});
65+
66+
<Component xcss={styles.primary} />
67+
`);
68+
69+
expect(result).toMatchInlineSnapshot(`
70+
"import * as React from "react";
71+
import { ax, ix, CC, CS } from "@compiled/react/runtime";
72+
const _ = "._syaz5scu{color:red}";
73+
const styles = {
74+
primary: "_syaz5scu",
75+
};
76+
<CC>
77+
<CS>{[_]}</CS>
78+
{<Component xcss={styles.primary} />}
79+
</CC>;
80+
"
81+
`);
82+
});
83+
84+
it('should allow ternaries', () => {
85+
const result = transform(`
86+
import { cssMap } from '@compiled/react';
87+
88+
const styles = cssMap({
89+
primary: { color: 'red' },
90+
secondary: { color: 'blue' }
91+
});
92+
93+
<Component xcss={isPrimary ? styles.primary : styles.secondary} />
94+
`);
95+
96+
expect(result).toMatchInlineSnapshot(`
97+
"import * as React from "react";
98+
import { ax, ix, CC, CS } from "@compiled/react/runtime";
99+
const _2 = "._syaz13q2{color:blue}";
100+
const _ = "._syaz5scu{color:red}";
101+
const styles = {
102+
primary: "_syaz5scu",
103+
secondary: "_syaz13q2",
104+
};
105+
<CC>
106+
<CS>{[_, _2]}</CS>
107+
{<Component xcss={isPrimary ? styles.primary : styles.secondary} />}
108+
</CC>;
109+
"
110+
`);
111+
});
112+
113+
it('should allow concatenating styles', () => {
114+
const result = transform(`
115+
import { cssMap, j } from '@compiled/react';
116+
117+
const styles = cssMap({
118+
primary: { color: 'red' },
119+
secondary: { color: 'blue' }
120+
});
121+
122+
<Component xcss={j(isPrimary && styles.primary, !isPrimary && styles.secondary)} />
123+
`);
124+
125+
expect(result).toMatchInlineSnapshot(`
126+
"import * as React from "react";
127+
import { ax, ix, CC, CS } from "@compiled/react/runtime";
128+
import { j } from "@compiled/react";
129+
const _2 = "._syaz13q2{color:blue}";
130+
const _ = "._syaz5scu{color:red}";
131+
const styles = {
132+
primary: "_syaz5scu",
133+
secondary: "_syaz13q2",
134+
};
135+
<CC>
136+
<CS>{[_, _2]}</CS>
137+
{
138+
<Component
139+
xcss={j(isPrimary && styles.primary, !isPrimary && styles.secondary)}
140+
/>
141+
}
142+
</CC>;
143+
"
144+
`);
145+
});
146+
147+
it('should ignore xcss prop when compiled must be in scope', () => {
148+
const result = transform(
149+
`
150+
<Component xcss={{ color: 'red' }} />
151+
`,
152+
{ requireCompiledInScopeForXCSSProp: true }
153+
);
154+
155+
expect(result).toMatchInlineSnapshot(`
156+
"<Component
157+
xcss={{
158+
color: "red",
159+
}}
160+
/>;
161+
"
162+
`);
163+
});
164+
165+
it('should transform xcss prop when compiled is in scope', () => {
166+
const result = transform(
167+
`
168+
import { cssMap } from '@compiled/react';
169+
170+
const styles = cssMap({
171+
primary: { color: 'red' },
172+
});
173+
174+
<Component xcss={styles.primary} />
175+
`,
176+
{ requireCompiledInScopeForXCSSProp: true }
177+
);
178+
179+
expect(result).toMatchInlineSnapshot(`
180+
"import * as React from "react";
181+
import { ax, ix, CC, CS } from "@compiled/react/runtime";
182+
const _ = "._syaz5scu{color:red}";
183+
const styles = {
184+
primary: "_syaz5scu",
185+
};
186+
<CC>
187+
<CS>{[_]}</CS>
188+
{<Component xcss={styles.primary} />}
189+
</CC>;
190+
"
191+
`);
192+
});
193+
});

0 commit comments

Comments
 (0)