@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.
@@ -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
- * Joins classes together and ensures atomic declarations of a single group exist.
9
- * Atomic declarations take the form of `_{group}{value}` (always prefixed with an underscore),
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
- * Input:
15
- *
16
- * ```
17
- * ax(['_aaaabbbb', '_aaaacccc'])
10
+ * ```ts
11
+ * ax(['_aaaabbbb', '_aaaacccc']);
12
+ * // output
13
+ * '_aaaacccc'
18
14
  * ```
19
15
  *
20
- * Output:
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
- * @param classes
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
- if (classNames.length <= 1 && (!classNames[0] || classNames[0].indexOf(' ') === -1)) {
30
- // short circuit if there's no custom class names.
31
- return classNames[0] || undefined;
27
+ // Shortcut: nothing to do
28
+ if (!classNames.length) {
29
+ return;
32
30
  }
33
- const atomicGroups = {};
34
- for (let i = 0; i < classNames.length; i++) {
35
- const cls = classNames[i];
36
- if (!cls) {
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
- const groups = cls.split(' ');
40
- for (let x = 0; x < groups.length; x++) {
41
- const atomic = groups[x];
42
- const atomicGroupName = atomic.slice(0, atomic.charCodeAt(0) === UNDERSCORE_UNICODE ? ATOMIC_GROUP_LENGTH : undefined);
43
- atomicGroups[atomicGroupName] = atomic;
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
- let str = '';
47
- for (const key in atomicGroups) {
48
- const value = atomicGroups[key];
49
- str += value + ' ';
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
- return str.slice(0, -1);
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,MAAM,kBAAkB,GAAG,EAAE,CAAC;AAE9B;;;GAGG;AACH,MAAM,mBAAmB,GAAG,CAAC,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,OAAO,UAAU,EAAE,CAAC,UAAiD;IAC1E,IAAI,UAAU,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;QACnF,kDAAkD;QAClD,OAAO,UAAU,CAAC,CAAC,CAAC,IAAI,SAAS,CAAC;KACnC;IAED,MAAM,YAAY,GAA2B,EAAE,CAAC;IAEhD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,UAAU,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;QAC1C,MAAM,GAAG,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;QAC1B,IAAI,CAAC,GAAG,EAAE;YACR,SAAS;SACV;QAED,MAAM,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAE9B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;YACtC,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;YACzB,MAAM,eAAe,GAAG,MAAM,CAAC,KAAK,CAClC,CAAC,EACD,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,kBAAkB,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,SAAS,CAC9E,CAAC;YACF,YAAY,CAAC,eAAe,CAAC,GAAG,MAAM,CAAC;SACxC;KACF;IAED,IAAI,GAAG,GAAG,EAAE,CAAC;IAEb,KAAK,MAAM,GAAG,IAAI,YAAY,EAAE;QAC9B,MAAM,KAAK,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;QAChC,GAAG,IAAI,KAAK,GAAG,GAAG,CAAC;KACpB;IAED,OAAO,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AAC1B,CAAC"}
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"}
@@ -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 CSSPseudos = CSSPseudoElements | CSSPseudoClasses;
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, CSSPseudoClasses, CSSProperties, StrictCSSProperties } from '../types';
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 CSSPseudoClasses | ''> = {
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 CSSPseudoClasses ? Q : ''>, TRequiredProperties['requiredProperties']> : never;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compiled/react",
3
- "version": "0.18.3",
3
+ "version": "0.18.5",
4
4
  "description": "A familiar and performant compile time CSS-in-JS library for React.",
5
5
  "keywords": [
6
6
  "compiled",
@@ -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}._v0vw1x77:focus-visible, ._ysv71x77:link{color:white}</style>
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, ._6146twqo:hover{color:grey}._1cld11x8:active{color:black}}@supports (display:grid){._1df61gy6:focus{color:yellow}._7okp11x8:active{color:black}}</style>
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={{ '&:hover': {}, '&:active': {}, '&::before': {}, '&::after': {} }}
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 CSSPseudoClasses]?: StrictCSSProperties;
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 CSSPseudoClasses | ''
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 CSSPseudoClasses | '' = ''> = {
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 CSSPseudoClasses
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 arr = [
7
- '_19itglyw',
8
- '_2rko1l7b',
9
- '_ca0qftgi',
10
- '_u5f319bv',
11
- '_n3tdftgi',
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
- it('completes with ax() string as the fastest', async () => {
26
- // Remove undefined and join the strings
27
- const str = arr.slice(0, -1).join(' ');
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() array',
32
- fn: () => ax(arr),
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() string',
36
- fn: () => ax([str, undefined]),
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() string'],
60
+ fastest: ['ax() single'],
42
61
  });
43
- }, 30000);
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
- ])('%s', (_, params, result) => {
54
- expect(result).toEqual(ax(params));
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
- * Joins classes together and ensures atomic declarations of a single group exist.
11
- * Atomic declarations take the form of `_{group}{value}` (always prefixed with an underscore),
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
- * Output:
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
- * @param classes
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
- if (classNames.length <= 1 && (!classNames[0] || classNames[0].indexOf(' ') === -1)) {
32
- // short circuit if there's no custom class names.
33
- return classNames[0] || undefined;
28
+ // Shortcut: nothing to do
29
+ if (!classNames.length) {
30
+ return;
34
31
  }
35
32
 
36
- const atomicGroups: Record<string, string> = {};
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
- for (let i = 0; i < classNames.length; i++) {
39
- const cls = classNames[i];
40
- if (!cls) {
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
- const groups = cls.split(' ');
54
+ // a `value` can contain multiple classnames
55
+ const list = value.split(' ');
45
56
 
46
- for (let x = 0; x < groups.length; x++) {
47
- const atomic = groups[x];
48
- const atomicGroupName = atomic.slice(
49
- 0,
50
- atomic.charCodeAt(0) === UNDERSCORE_UNICODE ? ATOMIC_GROUP_LENGTH : undefined
51
- );
52
- atomicGroups[atomicGroupName] = atomic;
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
- let str = '';
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
- for (const key in atomicGroups) {
59
- const value = atomicGroups[key];
60
- str += value + ' ';
89
+ // If we have an empty string, then our `map` was empty.
90
+ if (!result) {
91
+ return;
61
92
  }
62
93
 
63
- return str.slice(0, -1);
94
+ // remove last " " from the result (we added " " at the end of every value)
95
+ return result.trimEnd();
64
96
  }