@compiled/react 0.18.3 → 0.18.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/create-strict-api/types.d.ts +5 -5
- package/dist/browser/runtime/ax.d.ts +13 -14
- package/dist/browser/runtime/ax.js +65 -32
- package/dist/browser/runtime/ax.js.map +1 -1
- package/dist/browser/types.d.ts +3 -1
- package/dist/browser/xcss-prop/index.d.ts +3 -3
- package/dist/cjs/create-strict-api/types.d.ts +5 -5
- package/dist/cjs/runtime/ax.d.ts +13 -14
- package/dist/cjs/runtime/ax.js +65 -32
- package/dist/cjs/runtime/ax.js.map +1 -1
- package/dist/cjs/types.d.ts +3 -1
- package/dist/cjs/xcss-prop/index.d.ts +3 -3
- package/dist/esm/create-strict-api/types.d.ts +5 -5
- package/dist/esm/runtime/ax.d.ts +13 -14
- package/dist/esm/runtime/ax.js +65 -32
- package/dist/esm/runtime/ax.js.map +1 -1
- package/dist/esm/types.d.ts +3 -1
- package/dist/esm/xcss-prop/index.d.ts +3 -3
- package/package.json +1 -1
- package/src/__tests__/browser.test.tsx +4 -3
- package/src/__tests__/jest-matcher.test.tsx +13 -0
- package/src/create-strict-api/__tests__/__fixtures__/strict-api-recursive.ts +6 -0
- package/src/create-strict-api/__tests__/generics.test.tsx +46 -4
- package/src/create-strict-api/types.ts +5 -5
- package/src/runtime/__perf__/ax.test.ts +45 -26
- package/src/runtime/__tests__/ax.test.ts +4 -2
- package/src/runtime/ax.ts +68 -36
- package/src/types.ts +14 -2
- package/src/xcss-prop/__tests__/xcss-prop.test.tsx +31 -0
- package/src/xcss-prop/index.ts +3 -3
package/dist/esm/runtime/ax.js
CHANGED
|
@@ -1,53 +1,86 @@
|
|
|
1
|
-
const UNDERSCORE_UNICODE = 95;
|
|
2
1
|
/**
|
|
3
2
|
* This length includes the underscore,
|
|
4
3
|
* e.g. `"_1s4A"` would be a valid atomic group hash.
|
|
5
4
|
*/
|
|
6
5
|
const ATOMIC_GROUP_LENGTH = 5;
|
|
7
6
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* where both `group` and `value` are hashes **four characters long**.
|
|
11
|
-
* Class names can be of any length,
|
|
12
|
-
* this function can take both atomic declarations and class names.
|
|
7
|
+
* Create a single string containing all the classnames provided, separated by a space (`" "`).
|
|
8
|
+
* The result will only contain the _last_ atomic style classname for each atomic `group`.
|
|
13
9
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* ax(['_aaaabbbb', '_aaaacccc']);
|
|
12
|
+
* // output
|
|
13
|
+
* '_aaaacccc'
|
|
18
14
|
* ```
|
|
19
15
|
*
|
|
20
|
-
*
|
|
16
|
+
* Format of Atomic style classnames: `_{group}{value}` (`_\w{4}\w{4}`)
|
|
21
17
|
*
|
|
22
|
-
*
|
|
23
|
-
* '_aaaacccc'
|
|
24
|
-
* ```
|
|
18
|
+
* `ax` will preserve any non atomic style classnames (eg `"border-red"`)
|
|
25
19
|
*
|
|
26
|
-
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* ax(['_aaaabbbb', '_aaaacccc', 'border-red']);
|
|
22
|
+
* // output
|
|
23
|
+
* '_aaaacccc border-red'
|
|
24
|
+
* ```
|
|
27
25
|
*/
|
|
28
26
|
export default function ax(classNames) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return
|
|
27
|
+
// Shortcut: nothing to do
|
|
28
|
+
if (!classNames.length) {
|
|
29
|
+
return;
|
|
32
30
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if
|
|
31
|
+
// Shortcut: don't need to do anything if we only have a single classname
|
|
32
|
+
if (classNames.length === 1 &&
|
|
33
|
+
classNames[0] &&
|
|
34
|
+
// checking to see if `classNames[0]` is a string that contains other classnames
|
|
35
|
+
!classNames[0].includes(' ')) {
|
|
36
|
+
return classNames[0];
|
|
37
|
+
}
|
|
38
|
+
// Using an object rather than a `Map` as it performed better in our benchmarks.
|
|
39
|
+
// Would be happy to move to `Map` if it proved to be better under real conditions.
|
|
40
|
+
const map = {};
|
|
41
|
+
// Note: using loops to minimize iterations over the collection
|
|
42
|
+
for (const value of classNames) {
|
|
43
|
+
// Exclude all falsy values, which leaves us with populated strings
|
|
44
|
+
if (!value) {
|
|
37
45
|
continue;
|
|
38
46
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
// a `value` can contain multiple classnames
|
|
48
|
+
const list = value.split(' ');
|
|
49
|
+
for (const className of list) {
|
|
50
|
+
/**
|
|
51
|
+
* For atomic style classnames: the `key` is the `group`
|
|
52
|
+
*
|
|
53
|
+
* - Later atomic classnames with the same `group` will override earlier ones
|
|
54
|
+
* (which is what we want).
|
|
55
|
+
* - Assumes atomic classnames are the only things that start with `_`
|
|
56
|
+
* - Could use a regex to ensure that atomic classnames are structured how we expect,
|
|
57
|
+
* but did not add that for now as it did slow things down a bit.
|
|
58
|
+
*
|
|
59
|
+
* For other classnames: the `key` is the whole classname
|
|
60
|
+
* - Okay to remove duplicates as doing so does not impact specificity
|
|
61
|
+
*
|
|
62
|
+
* */
|
|
63
|
+
const key = className.startsWith('_') ? className.slice(0, ATOMIC_GROUP_LENGTH) : className;
|
|
64
|
+
map[key] = className;
|
|
44
65
|
}
|
|
45
66
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
67
|
+
/**
|
|
68
|
+
* We are converting the `map` into a string.
|
|
69
|
+
*
|
|
70
|
+
* The simple way to do this would be `Object.values(map).join(' ')`.
|
|
71
|
+
* However, the approach below performs 10%-20% better in benchmarks.
|
|
72
|
+
*
|
|
73
|
+
* For `ax()` it feels right to squeeze as much runtime performance out as we can.
|
|
74
|
+
*/
|
|
75
|
+
let result = '';
|
|
76
|
+
for (const key in map) {
|
|
77
|
+
result += map[key] + ' ';
|
|
78
|
+
}
|
|
79
|
+
// If we have an empty string, then our `map` was empty.
|
|
80
|
+
if (!result) {
|
|
81
|
+
return;
|
|
50
82
|
}
|
|
51
|
-
|
|
83
|
+
// remove last " " from the result (we added " " at the end of every value)
|
|
84
|
+
return result.trimEnd();
|
|
52
85
|
}
|
|
53
86
|
//# sourceMappingURL=ax.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ax.js","sourceRoot":"","sources":["../../../src/runtime/ax.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"ax.js","sourceRoot":"","sources":["../../../src/runtime/ax.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,mBAAmB,GAAG,CAAC,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,OAAO,UAAU,EAAE,CAAC,UAAiD;IAC1E,0BAA0B;IAC1B,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE;QACtB,OAAO;KACR;IAED,yEAAyE;IACzE,IACE,UAAU,CAAC,MAAM,KAAK,CAAC;QACvB,UAAU,CAAC,CAAC,CAAC;QACb,gFAAgF;QAChF,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,EAC5B;QACA,OAAO,UAAU,CAAC,CAAC,CAAC,CAAC;KACtB;IAED,gFAAgF;IAChF,mFAAmF;IACnF,MAAM,GAAG,GAA2B,EAAE,CAAC;IAEvC,+DAA+D;IAC/D,KAAK,MAAM,KAAK,IAAI,UAAU,EAAE;QAC9B,mEAAmE;QACnE,IAAI,CAAC,KAAK,EAAE;YACV,SAAS;SACV;QAED,4CAA4C;QAC5C,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAE9B,KAAK,MAAM,SAAS,IAAI,IAAI,EAAE;YAC5B;;;;;;;;;;;;iBAYK;YACL,MAAM,GAAG,GAAG,SAAS,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,mBAAmB,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;YAC5F,GAAG,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;SACtB;KACF;IAED;;;;;;;OAOG;IACH,IAAI,MAAM,GAAW,EAAE,CAAC;IACxB,KAAK,MAAM,GAAG,IAAI,GAAG,EAAE;QACrB,MAAM,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;KAC1B;IAED,wDAAwD;IACxD,IAAI,CAAC,MAAM,EAAE;QACX,OAAO;KACR;IAED,2EAA2E;IAC3E,OAAO,MAAM,CAAC,OAAO,EAAE,CAAC;AAC1B,CAAC"}
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -19,8 +19,10 @@ export type CssObject<TProps> = Readonly<{
|
|
|
19
19
|
}>;
|
|
20
20
|
export type CssFunction<TProps = unknown> = CssType<TProps> | BasicTemplateInterpolations | null | boolean | undefined;
|
|
21
21
|
export type CSSPseudoElements = '&::after' | '&::backdrop' | '&::before' | '&::cue' | '&::cue-region' | '&::first-letter' | '&::first-line' | '&::grammar-error' | '&::marker' | '&::placeholder' | '&::selection' | '&::spelling-error' | '&::target-text' | '&::view-transition';
|
|
22
|
+
export type FlattenedChainedCSSPseudosClasses = '&:visited:active' | '&:visited:hover' | '&:active:visited' | '&:hover::before' | '&:hover::after' | '&:focus-visible::before' | '&:focus-visible::after' | '&:focus:not(:focus-visible)';
|
|
22
23
|
export type CSSPseudoClasses = '&:active' | '&:autofill' | '&:blank' | '&:checked' | '&:default' | '&:defined' | '&:disabled' | '&:empty' | '&:enabled' | '&:first' | '&:focus' | '&:focus-visible' | '&:focus-within' | '&:fullscreen' | '&:hover' | '&:in-range' | '&:indeterminate' | '&:invalid' | '&:left' | '&:link' | '&:local-link' | '&:optional' | '&:out-of-range' | '&:paused' | '&:picture-in-picture' | '&:placeholder-shown' | '&:playing' | '&:read-only' | '&:read-write' | '&:required' | '&:right' | '&:target' | '&:user-invalid' | '&:user-valid' | '&:valid' | '&:visited';
|
|
23
|
-
export type
|
|
24
|
+
export type AllCSSPseudoClasses = CSSPseudoClasses | FlattenedChainedCSSPseudosClasses;
|
|
25
|
+
export type CSSPseudos = CSSPseudoElements | AllCSSPseudoClasses;
|
|
24
26
|
/**
|
|
25
27
|
* The XCSSProp must be given all known available properties even
|
|
26
28
|
* if it takes a subset of them. This ensures the (lack-of an)
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import type * as CSS from 'csstype';
|
|
2
2
|
import type { ApplySchemaValue } from '../create-strict-api/types';
|
|
3
|
-
import type { CSSPseudos,
|
|
3
|
+
import type { CSSPseudos, CSSProperties, StrictCSSProperties, AllCSSPseudoClasses } from '../types';
|
|
4
4
|
type MarkAsRequired<T, K extends keyof T> = T & {
|
|
5
5
|
[P in K]-?: T[P];
|
|
6
6
|
};
|
|
7
|
-
type XCSSValue<TStyleDecl extends keyof CSSProperties, TSchema, TPseudoKey extends
|
|
7
|
+
type XCSSValue<TStyleDecl extends keyof CSSProperties, TSchema, TPseudoKey extends AllCSSPseudoClasses | ''> = {
|
|
8
8
|
[Q in keyof StrictCSSProperties]: Q extends TStyleDecl ? ApplySchemaValue<TSchema, Q, TPseudoKey> : never;
|
|
9
9
|
};
|
|
10
10
|
type XCSSPseudo<TAllowedProperties extends keyof StrictCSSProperties, TAllowedPseudos extends CSSPseudos, TRequiredProperties extends {
|
|
11
11
|
requiredProperties: TAllowedProperties;
|
|
12
12
|
}, TSchema> = {
|
|
13
|
-
[Q in CSSPseudos]?: Q extends TAllowedPseudos ? MarkAsRequired<XCSSValue<TAllowedProperties, TSchema, Q extends
|
|
13
|
+
[Q in CSSPseudos]?: Q extends TAllowedPseudos ? MarkAsRequired<XCSSValue<TAllowedProperties, TSchema, Q extends AllCSSPseudoClasses ? Q : ''>, TRequiredProperties['requiredProperties']> : never;
|
|
14
14
|
};
|
|
15
15
|
type XCSSMediaQuery<TAllowedProperties extends keyof StrictCSSProperties, TAllowedPseudos extends CSSPseudos, TAllowedMediaQueries extends string, TSchema> = {
|
|
16
16
|
[Q in `@media ${TAllowedMediaQueries}`]?: XCSSValue<TAllowedProperties, TSchema, ''> | XCSSPseudo<TAllowedProperties, TAllowedPseudos, never, TSchema>;
|
package/package.json
CHANGED
|
@@ -94,13 +94,14 @@ describe('browser', () => {
|
|
|
94
94
|
render(<StyledLink href="https://atlassian.design">Atlassian Design System</StyledLink>);
|
|
95
95
|
|
|
96
96
|
expect(document.head.innerHTML.split('</style>').join('</style>\n')).toMatchInlineSnapshot(`
|
|
97
|
-
"<style nonce="k0Mp1lEd">._1e0c1txw{display:flex}._1wyb12am{font-size:50px}._syaz1cnh{color:purple}
|
|
98
|
-
<style nonce="k0Mp1lEd">._ysv75scu:link{color:red}</style>
|
|
97
|
+
"<style nonce="k0Mp1lEd">._1e0c1txw{display:flex}._1wyb12am{font-size:50px}._syaz1cnh{color:purple}</style>
|
|
98
|
+
<style nonce="k0Mp1lEd">._ysv75scu:link{color:red}._ysv71x77:link{color:white}</style>
|
|
99
99
|
<style nonce="k0Mp1lEd">._105332ev:visited{color:pink}</style>
|
|
100
100
|
<style nonce="k0Mp1lEd">._f8pjbf54:focus{color:green}</style>
|
|
101
|
+
<style nonce="k0Mp1lEd">._v0vw1x77:focus-visible{color:white}</style>
|
|
101
102
|
<style nonce="k0Mp1lEd">._30l31gy6:hover{color:yellow}</style>
|
|
102
103
|
<style nonce="k0Mp1lEd">._9h8h13q2:active{color:blue}</style>
|
|
103
|
-
<style nonce="k0Mp1lEd">@media (max-width:800px){._1o8z1gy6:focus{color:yellow}._jbabtwqo:focus-visible
|
|
104
|
+
<style nonce="k0Mp1lEd">@media (max-width:800px){._1o8z1gy6:focus{color:yellow}._jbabtwqo:focus-visible{color:grey}._6146twqo:hover{color:grey}._1cld11x8:active{color:black}}@supports (display:grid){._1df61gy6:focus{color:yellow}._7okp11x8:active{color:black}}</style>
|
|
104
105
|
"
|
|
105
106
|
`);
|
|
106
107
|
});
|
|
@@ -18,6 +18,19 @@ describe('toHaveCompliedCss', () => {
|
|
|
18
18
|
expect(getByText('hello world')).toHaveCompiledCss('font-size', '12px');
|
|
19
19
|
});
|
|
20
20
|
|
|
21
|
+
it('should detect styles (SVG)', () => {
|
|
22
|
+
const { getByText } = render(
|
|
23
|
+
<svg
|
|
24
|
+
css={{
|
|
25
|
+
fontSize: '12px',
|
|
26
|
+
}}>
|
|
27
|
+
hello world
|
|
28
|
+
</svg>
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
expect(getByText('hello world')).toHaveCompiledCss('font-size', '12px');
|
|
32
|
+
});
|
|
33
|
+
|
|
21
34
|
it('should detect missing styles', () => {
|
|
22
35
|
const { getByText } = render(<div css={{ fontSize: '12px' }}>hello world</div>);
|
|
23
36
|
|
|
@@ -24,9 +24,15 @@ interface PressedProperties {
|
|
|
24
24
|
backgroundColor: BackgroundPressed;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
interface ChainedProperties {
|
|
28
|
+
color: ColorPressed;
|
|
29
|
+
backgroundColor: BackgroundPressed;
|
|
30
|
+
}
|
|
31
|
+
|
|
27
32
|
interface CSSPropertiesSchema extends Properties {
|
|
28
33
|
'&:hover': HoveredProperties;
|
|
29
34
|
'&:active': PressedProperties;
|
|
35
|
+
'&:hover::after': ChainedProperties;
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
const { css, cssMap, cx, XCSSProp } = createStrictAPI<CSSPropertiesSchema>();
|
|
@@ -9,6 +9,7 @@ describe('createStrictAPI()', () => {
|
|
|
9
9
|
const styles = css({
|
|
10
10
|
'&:hover': {},
|
|
11
11
|
'&:active': {},
|
|
12
|
+
'&:hover::after': {},
|
|
12
13
|
'&::before': {},
|
|
13
14
|
'&::after': {},
|
|
14
15
|
});
|
|
@@ -23,6 +24,7 @@ describe('createStrictAPI()', () => {
|
|
|
23
24
|
nested: {
|
|
24
25
|
'&:hover': {},
|
|
25
26
|
'&:active': {},
|
|
27
|
+
'&:hover::after': {},
|
|
26
28
|
'&::before': {},
|
|
27
29
|
'&::after': {},
|
|
28
30
|
},
|
|
@@ -40,7 +42,7 @@ describe('createStrictAPI()', () => {
|
|
|
40
42
|
xcss: ReturnType<
|
|
41
43
|
typeof XCSSProp<
|
|
42
44
|
'backgroundColor' | 'color',
|
|
43
|
-
'&:hover' | '&:active' | '&::before' | '&::after'
|
|
45
|
+
'&:hover' | '&:active' | '&::before' | '&::after' | '&:hover::after'
|
|
44
46
|
>
|
|
45
47
|
>;
|
|
46
48
|
}) {
|
|
@@ -49,7 +51,13 @@ describe('createStrictAPI()', () => {
|
|
|
49
51
|
|
|
50
52
|
const { getByTestId } = render(
|
|
51
53
|
<Component
|
|
52
|
-
xcss={{
|
|
54
|
+
xcss={{
|
|
55
|
+
'&:hover': {},
|
|
56
|
+
'&:active': {},
|
|
57
|
+
'&::before': {},
|
|
58
|
+
'&::after': {},
|
|
59
|
+
'&:hover::after': {},
|
|
60
|
+
}}
|
|
53
61
|
data-testid="div"
|
|
54
62
|
/>
|
|
55
63
|
);
|
|
@@ -76,6 +84,12 @@ describe('createStrictAPI()', () => {
|
|
|
76
84
|
// @ts-expect-error — Type '""' is not assignable to type ...
|
|
77
85
|
backgroundColor: '',
|
|
78
86
|
},
|
|
87
|
+
'&:hover::after': {
|
|
88
|
+
// @ts-expect-error — Type '""' is not assignable to type ...
|
|
89
|
+
color: '',
|
|
90
|
+
// @ts-expect-error — Type '""' is not assignable to type ...
|
|
91
|
+
backgroundColor: '',
|
|
92
|
+
},
|
|
79
93
|
'&::before': {
|
|
80
94
|
// @ts-expect-error — Type '""' is not assignable to type ...
|
|
81
95
|
color: '',
|
|
@@ -114,6 +128,12 @@ describe('createStrictAPI()', () => {
|
|
|
114
128
|
// @ts-expect-error — Type '""' is not assignable to type ...
|
|
115
129
|
backgroundColor: '',
|
|
116
130
|
},
|
|
131
|
+
'&:hover::after': {
|
|
132
|
+
// @ts-expect-error — Type '""' is not assignable to type ...
|
|
133
|
+
color: '',
|
|
134
|
+
// @ts-expect-error — Type '""' is not assignable to type ...
|
|
135
|
+
backgroundColor: '',
|
|
136
|
+
},
|
|
117
137
|
'&::before': {
|
|
118
138
|
// @ts-expect-error — Type '""' is not assignable to type ...
|
|
119
139
|
color: '',
|
|
@@ -139,7 +159,7 @@ describe('createStrictAPI()', () => {
|
|
|
139
159
|
xcss: ReturnType<
|
|
140
160
|
typeof XCSSProp<
|
|
141
161
|
'backgroundColor' | 'color',
|
|
142
|
-
'&:hover' | '&:active' | '&::before' | '&::after'
|
|
162
|
+
'&:hover' | '&:active' | '&::before' | '&::after' | '&:hover::after'
|
|
143
163
|
>
|
|
144
164
|
>;
|
|
145
165
|
}) {
|
|
@@ -165,6 +185,12 @@ describe('createStrictAPI()', () => {
|
|
|
165
185
|
// @ts-expect-error — Type '""' is not assignable to type ...
|
|
166
186
|
backgroundColor: 'var(--ds-success)',
|
|
167
187
|
},
|
|
188
|
+
'&:hover::after': {
|
|
189
|
+
// @ts-expect-error — Type '""' is not assignable to type ...
|
|
190
|
+
color: 'var(--ds-text)',
|
|
191
|
+
// @ts-expect-error — Type '""' is not assignable to type ...
|
|
192
|
+
backgroundColor: 'var(--ds-success)',
|
|
193
|
+
},
|
|
168
194
|
'&::before': {
|
|
169
195
|
// @ts-expect-error — Type '""' is not assignable to type ...
|
|
170
196
|
color: '',
|
|
@@ -198,6 +224,12 @@ describe('createStrictAPI()', () => {
|
|
|
198
224
|
color: 'var(--ds-text-hovered)',
|
|
199
225
|
backgroundColor: 'var(--ds-bold-hovered)',
|
|
200
226
|
},
|
|
227
|
+
'&:hover::after': {
|
|
228
|
+
// @ts-expect-error — should be a value from the schema
|
|
229
|
+
padding: '10px',
|
|
230
|
+
color: 'var(--ds-text-pressed)',
|
|
231
|
+
backgroundColor: 'var(--ds-bold-pressed)',
|
|
232
|
+
},
|
|
201
233
|
'&:active': {
|
|
202
234
|
// @ts-expect-error — should be a value from the schema
|
|
203
235
|
padding: '10px',
|
|
@@ -243,6 +275,12 @@ describe('createStrictAPI()', () => {
|
|
|
243
275
|
color: 'var(--ds-text-pressed)',
|
|
244
276
|
backgroundColor: 'var(--ds-bold-pressed)',
|
|
245
277
|
},
|
|
278
|
+
'&:hover::after': {
|
|
279
|
+
// @ts-expect-error — should be a value from the schema
|
|
280
|
+
padding: '10px',
|
|
281
|
+
color: 'var(--ds-text-pressed)',
|
|
282
|
+
backgroundColor: 'var(--ds-bold-pressed)',
|
|
283
|
+
},
|
|
246
284
|
'&::before': {
|
|
247
285
|
// @ts-expect-error — should be a value from the schema
|
|
248
286
|
padding: '10px',
|
|
@@ -270,7 +308,7 @@ describe('createStrictAPI()', () => {
|
|
|
270
308
|
xcss: ReturnType<
|
|
271
309
|
typeof XCSSProp<
|
|
272
310
|
'backgroundColor' | 'color',
|
|
273
|
-
'&:hover' | '&:active' | '&::before' | '&::after'
|
|
311
|
+
'&:hover' | '&:active' | '&::before' | '&::after' | '&:hover::after'
|
|
274
312
|
>
|
|
275
313
|
>;
|
|
276
314
|
}) {
|
|
@@ -290,6 +328,10 @@ describe('createStrictAPI()', () => {
|
|
|
290
328
|
color: 'var(--ds-text-pressed)',
|
|
291
329
|
backgroundColor: 'var(--ds-bold-pressed)',
|
|
292
330
|
},
|
|
331
|
+
'&:hover::after': {
|
|
332
|
+
color: 'var(--ds-text-pressed)',
|
|
333
|
+
backgroundColor: 'var(--ds-bold-pressed)',
|
|
334
|
+
},
|
|
293
335
|
'&::before': {
|
|
294
336
|
color: 'var(--ds-text)',
|
|
295
337
|
backgroundColor: 'var(--ds-bold)',
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
StrictCSSProperties,
|
|
3
|
-
CSSPseudoClasses,
|
|
4
3
|
CSSPseudoElements,
|
|
5
4
|
CSSPseudos,
|
|
5
|
+
AllCSSPseudoClasses,
|
|
6
6
|
} from '../types';
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
* and pseudo elements.
|
|
12
12
|
*/
|
|
13
13
|
export type CompiledSchemaShape = StrictCSSProperties & {
|
|
14
|
-
[Q in
|
|
14
|
+
[Q in AllCSSPseudoClasses]?: StrictCSSProperties;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
export type PseudosDeclarations = { [Q in CSSPseudos]?: StrictCSSProperties };
|
|
@@ -27,7 +27,7 @@ export type AllowedStyles<TMediaQuery extends string> = StrictCSSProperties &
|
|
|
27
27
|
export type ApplySchemaValue<
|
|
28
28
|
TSchema,
|
|
29
29
|
TKey extends keyof StrictCSSProperties,
|
|
30
|
-
TPseudoKey extends
|
|
30
|
+
TPseudoKey extends AllCSSPseudoClasses | ''
|
|
31
31
|
> = TKey extends keyof TSchema
|
|
32
32
|
? // TKey is a valid property on the schema
|
|
33
33
|
TPseudoKey extends keyof TSchema
|
|
@@ -46,11 +46,11 @@ export type ApplySchemaValue<
|
|
|
46
46
|
* value if present, else fallback to its value from {@link StrictCSSProperties}. If
|
|
47
47
|
* the property isn't a known property its value will be resolved to `never`.
|
|
48
48
|
*/
|
|
49
|
-
export type ApplySchema<TObject, TSchema, TPseudoKey extends
|
|
49
|
+
export type ApplySchema<TObject, TSchema, TPseudoKey extends AllCSSPseudoClasses | '' = ''> = {
|
|
50
50
|
[TKey in keyof TObject]?: TKey extends keyof StrictCSSProperties
|
|
51
51
|
? // TKey is a valid CSS property, try to resolve its value.
|
|
52
52
|
ApplySchemaValue<TSchema, TKey, TPseudoKey>
|
|
53
|
-
: TKey extends
|
|
53
|
+
: TKey extends AllCSSPseudoClasses
|
|
54
54
|
? // TKey is a valid pseudo class, recursively resolve its child properties
|
|
55
55
|
// while passing down the parent pseudo key to resolve any specific schema types.
|
|
56
56
|
ApplySchema<TObject[TKey], TSchema, TKey>
|
|
@@ -3,42 +3,61 @@ import { runBenchmark } from '@compiled/benchmark';
|
|
|
3
3
|
import { ax } from '../index';
|
|
4
4
|
|
|
5
5
|
describe('ax benchmark', () => {
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
'_19bv19bv',
|
|
13
|
-
'_bfhk1mzw',
|
|
14
|
-
'_syazu67f',
|
|
15
|
-
'_k48p1nn1',
|
|
16
|
-
'_ect41kw7',
|
|
17
|
-
'_1wybdlk8',
|
|
18
|
-
'_irr3mlcl',
|
|
19
|
-
'_1di6vctu',
|
|
20
|
-
// `undefined` is an acceptable parameter so we want to include it in the test case.
|
|
21
|
-
// Example: ax(['aaaabbbb', foo() && "aaaacccc"])
|
|
22
|
-
undefined,
|
|
6
|
+
const chunks: string[] = ['aaaa', 'bbbb', 'cccc', 'dddd', 'eeee', 'ffff', 'gggg'];
|
|
7
|
+
const uniques: string[] = chunks.map((chunk) => `_${chunk}${chunk}`);
|
|
8
|
+
const withClashes: string[] = [
|
|
9
|
+
...Array.from({ length: 4 }, () => `_${chunks[0]}${chunks[0]}`),
|
|
10
|
+
...Array.from({ length: 6 }, () => `_${chunks[0]}${chunks[1]}`),
|
|
11
|
+
...Array.from({ length: 8 }, () => `_${chunks[0]}${chunks[2]}`),
|
|
23
12
|
];
|
|
24
13
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
14
|
+
const getRandomRules = (() => {
|
|
15
|
+
function randomChunk() {
|
|
16
|
+
return chunks[Math.floor(Math.random() * chunks.length)];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return function create(): string[] {
|
|
20
|
+
return Array.from({ length: 20 }, () => `_${randomChunk()}${randomChunk()}`);
|
|
21
|
+
};
|
|
22
|
+
})();
|
|
28
23
|
|
|
24
|
+
it('completes with ax() string as the fastest', async () => {
|
|
29
25
|
const benchmark = await runBenchmark('ax', [
|
|
30
26
|
{
|
|
31
|
-
name: 'ax()
|
|
32
|
-
fn: () => ax(
|
|
27
|
+
name: 'ax() single',
|
|
28
|
+
fn: () => ax(['_aaaabbbb']),
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'ax() uniques (array)',
|
|
32
|
+
fn: () => ax(uniques),
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'ax() uniques (as a string)',
|
|
36
|
+
fn: () => ax([uniques.join(' ')]),
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'ax() clashes',
|
|
40
|
+
fn: () => ax(withClashes),
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'ax() clashes (as a string)',
|
|
44
|
+
fn: () => ax([withClashes.join(' ')]),
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'ax() random keys (no clashes)',
|
|
48
|
+
fn: () => ax(getRandomRules()),
|
|
33
49
|
},
|
|
34
50
|
{
|
|
35
|
-
name: 'ax()
|
|
36
|
-
fn: () =>
|
|
51
|
+
name: 'ax() random keys (with clashes)',
|
|
52
|
+
fn: () => {
|
|
53
|
+
const random = getRandomRules();
|
|
54
|
+
ax([...random, ...random, ...random]);
|
|
55
|
+
},
|
|
37
56
|
},
|
|
38
57
|
]);
|
|
39
58
|
|
|
40
59
|
expect(benchmark).toMatchObject({
|
|
41
|
-
fastest: ['ax()
|
|
60
|
+
fastest: ['ax() single'],
|
|
42
61
|
});
|
|
43
|
-
},
|
|
62
|
+
}, 90000);
|
|
44
63
|
});
|
|
@@ -6,6 +6,7 @@ describe('ax', () => {
|
|
|
6
6
|
it.each([
|
|
7
7
|
['should handle empty array', [], undefined],
|
|
8
8
|
['should handle array with undefined', [undefined], undefined],
|
|
9
|
+
['should handle array with falsy values', [undefined, null, false as const, ''], undefined],
|
|
9
10
|
['should join single classes together', ['foo', 'bar'], 'foo bar'],
|
|
10
11
|
['should join multi classes together', ['foo baz', 'bar'], 'foo baz bar'],
|
|
11
12
|
['should remove undefined', ['foo', 'bar', undefined], 'foo bar'],
|
|
@@ -50,7 +51,8 @@ describe('ax', () => {
|
|
|
50
51
|
['hello_there', 'hello_world', '_aaaabbbb'],
|
|
51
52
|
'hello_there hello_world _aaaabbbb',
|
|
52
53
|
],
|
|
53
|
-
|
|
54
|
-
|
|
54
|
+
['should remove duplicate custom class names', ['a', 'a'], 'a'],
|
|
55
|
+
])('%s', (_, params, expected) => {
|
|
56
|
+
expect(ax(params)).toEqual(expected);
|
|
55
57
|
});
|
|
56
58
|
});
|
package/src/runtime/ax.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
const UNDERSCORE_UNICODE = 95;
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* This length includes the underscore,
|
|
5
3
|
* e.g. `"_1s4A"` would be a valid atomic group hash.
|
|
@@ -7,58 +5,92 @@ const UNDERSCORE_UNICODE = 95;
|
|
|
7
5
|
const ATOMIC_GROUP_LENGTH = 5;
|
|
8
6
|
|
|
9
7
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* where both `group` and `value` are hashes **four characters long**.
|
|
13
|
-
* Class names can be of any length,
|
|
14
|
-
* this function can take both atomic declarations and class names.
|
|
15
|
-
*
|
|
16
|
-
* Input:
|
|
8
|
+
* Create a single string containing all the classnames provided, separated by a space (`" "`).
|
|
9
|
+
* The result will only contain the _last_ atomic style classname for each atomic `group`.
|
|
17
10
|
*
|
|
18
|
-
* ```
|
|
19
|
-
* ax(['_aaaabbbb', '_aaaacccc'])
|
|
11
|
+
* ```ts
|
|
12
|
+
* ax(['_aaaabbbb', '_aaaacccc']);
|
|
13
|
+
* // output
|
|
14
|
+
* '_aaaacccc'
|
|
20
15
|
* ```
|
|
21
16
|
*
|
|
22
|
-
*
|
|
17
|
+
* Format of Atomic style classnames: `_{group}{value}` (`_\w{4}\w{4}`)
|
|
23
18
|
*
|
|
24
|
-
*
|
|
25
|
-
* '_aaaacccc'
|
|
26
|
-
* ```
|
|
19
|
+
* `ax` will preserve any non atomic style classnames (eg `"border-red"`)
|
|
27
20
|
*
|
|
28
|
-
*
|
|
21
|
+
* ```ts
|
|
22
|
+
* ax(['_aaaabbbb', '_aaaacccc', 'border-red']);
|
|
23
|
+
* // output
|
|
24
|
+
* '_aaaacccc border-red'
|
|
25
|
+
* ```
|
|
29
26
|
*/
|
|
30
27
|
export default function ax(classNames: (string | undefined | null | false)[]): string | undefined {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return
|
|
28
|
+
// Shortcut: nothing to do
|
|
29
|
+
if (!classNames.length) {
|
|
30
|
+
return;
|
|
34
31
|
}
|
|
35
32
|
|
|
36
|
-
|
|
33
|
+
// Shortcut: don't need to do anything if we only have a single classname
|
|
34
|
+
if (
|
|
35
|
+
classNames.length === 1 &&
|
|
36
|
+
classNames[0] &&
|
|
37
|
+
// checking to see if `classNames[0]` is a string that contains other classnames
|
|
38
|
+
!classNames[0].includes(' ')
|
|
39
|
+
) {
|
|
40
|
+
return classNames[0];
|
|
41
|
+
}
|
|
37
42
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
// Using an object rather than a `Map` as it performed better in our benchmarks.
|
|
44
|
+
// Would be happy to move to `Map` if it proved to be better under real conditions.
|
|
45
|
+
const map: Record<string, string> = {};
|
|
46
|
+
|
|
47
|
+
// Note: using loops to minimize iterations over the collection
|
|
48
|
+
for (const value of classNames) {
|
|
49
|
+
// Exclude all falsy values, which leaves us with populated strings
|
|
50
|
+
if (!value) {
|
|
41
51
|
continue;
|
|
42
52
|
}
|
|
43
53
|
|
|
44
|
-
|
|
54
|
+
// a `value` can contain multiple classnames
|
|
55
|
+
const list = value.split(' ');
|
|
45
56
|
|
|
46
|
-
for (
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
57
|
+
for (const className of list) {
|
|
58
|
+
/**
|
|
59
|
+
* For atomic style classnames: the `key` is the `group`
|
|
60
|
+
*
|
|
61
|
+
* - Later atomic classnames with the same `group` will override earlier ones
|
|
62
|
+
* (which is what we want).
|
|
63
|
+
* - Assumes atomic classnames are the only things that start with `_`
|
|
64
|
+
* - Could use a regex to ensure that atomic classnames are structured how we expect,
|
|
65
|
+
* but did not add that for now as it did slow things down a bit.
|
|
66
|
+
*
|
|
67
|
+
* For other classnames: the `key` is the whole classname
|
|
68
|
+
* - Okay to remove duplicates as doing so does not impact specificity
|
|
69
|
+
*
|
|
70
|
+
* */
|
|
71
|
+
const key = className.startsWith('_') ? className.slice(0, ATOMIC_GROUP_LENGTH) : className;
|
|
72
|
+
map[key] = className;
|
|
53
73
|
}
|
|
54
74
|
}
|
|
55
75
|
|
|
56
|
-
|
|
76
|
+
/**
|
|
77
|
+
* We are converting the `map` into a string.
|
|
78
|
+
*
|
|
79
|
+
* The simple way to do this would be `Object.values(map).join(' ')`.
|
|
80
|
+
* However, the approach below performs 10%-20% better in benchmarks.
|
|
81
|
+
*
|
|
82
|
+
* For `ax()` it feels right to squeeze as much runtime performance out as we can.
|
|
83
|
+
*/
|
|
84
|
+
let result: string = '';
|
|
85
|
+
for (const key in map) {
|
|
86
|
+
result += map[key] + ' ';
|
|
87
|
+
}
|
|
57
88
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
89
|
+
// If we have an empty string, then our `map` was empty.
|
|
90
|
+
if (!result) {
|
|
91
|
+
return;
|
|
61
92
|
}
|
|
62
93
|
|
|
63
|
-
|
|
94
|
+
// remove last " " from the result (we added " " at the end of every value)
|
|
95
|
+
return result.trimEnd();
|
|
64
96
|
}
|