Skip to content

Commit f9c957e

Browse files
AtlassianRubberDuckrenovate[bot]JakeLaneatlas-dst-botgithub-actions[bot]
authoredFeb 20, 2023
Compress atomic class names (#1408)
* Update 'ax' to accept short class name * Update 'ax' flow type and up the size limit * Add benchmark test to test ax(compressed class names) * Change the format of compress class names * Restore Flow type * Compress class names * Update snapshot * chore(deps): update dependency @types/node to ^18.11.19 (#1407) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Turn off compressing class names if stylesheet extraction is off * parcel integration * Add changeset * Replace rather than insert class name in the sheet and make ClassNames to support conditional CSS * add 'generateCompressionMap' * Update changelog * Fix spelling mistake * Allow uppercase in class-name-generator * chore(deps): update dependency css-what to >=5.1.0 (#1409) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update webpack packages (#1411) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Fix transparent and currentcolor not being treated as a color (#1412) * chore(deps): update dependency css-what to >=6.1.0 (#1414) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Version Packages (#1413) Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> * chore(deps): update dependency nth-check to >=2.1.1 (#1415) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency prettier to ^2.8.4 (#1416) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency @types/node to ^18.13.0 (#1417) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Bump node version to v18 (#1392) * Bump node version to v18 * Bump import jsx * Bump nvmrc node version from 18.12 to 18.14 * Add changeset for PR #1392 --------- Co-authored-by: Grant Wong <gwong2@atlassian.com> * chore(deps): update parcel packages (#1390) * chore(deps): update parcel packages * Add dummy generic type so optimizer works with parcel v2.8.0+ Parcel v2.8.0 adds a BundleConfigType generic type to Optimizer (parcel-bundler/parcel#8370) as part of their `loadBundleConfig` method. We use a dummy type because we don't need this functionality currently. * Update snapshot tests for Parcel 2.8.3 --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Grant Wong <gwong2@atlassian.com> * chore(deps): update eslint packages (#1420) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update dependency jest to v29 (#1384) * chore(deps): update dependency jest to v29 * Update snapshots and node env for some tests --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Jake Lane <jlane2@atlassian.com> * Add Grant Wong as codeowner (#1421) Co-authored-by: Grant Wong <2908767+dddlr@users.noreply.github.com> * Replace rather than insert class name in the sheet and make ClassNames to support conditional CSS * Add prefix option and avoid 'ad' * Add comment to ax benchmark * Make classNameCompressionMap a separate file in parcel example app * Remove default reservedClassNames * Export generateCompressionMap * Update comment --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Jake Lane <jlane2@atlassian.com> Co-authored-by: atlas-dst-bot <81662413+atlas-dst-bot@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Grant Wong <gwong2@atlassian.com> Co-authored-by: Grant Wong <2908767+dddlr@users.noreply.github.com>
1 parent 32b54a1 commit f9c957e

30 files changed

+933
-147
lines changed
 

‎.changeset/grumpy-cows-look.md

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@compiled/parcel-transformer': minor
3+
'@compiled/webpack-loader': minor
4+
'@compiled/babel-plugin': minor
5+
'@compiled/react': minor
6+
'@compiled/css': minor
7+
---
8+
9+
Add an option to compress class names based on "classNameCompressionMap", which is provided by library consumers.
10+
Add a script to generate compressed class names.

‎.eslintrc.js

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ module.exports = {
22
root: true,
33
ignorePatterns: [
44
'dist',
5+
'build',
56
'flow-typed',
67
'*.d.ts',
78
'babel-cjs.js',

‎examples/parcel/.compiledcssrc

-24
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"1wyb12am": "a",
3+
"syaz5scu": "b",
4+
"syazruxl": "c",
5+
"k48pbfng": "d",
6+
"30l35scu": "e",
7+
"f8pj13q2": "f",
8+
"1e0c1o8l": "g",
9+
"ca0qftgi": "h",
10+
"u5f3ftgi": "i",
11+
"n3tdftgi": "j",
12+
"19bvftgi": "k",
13+
"19itak0l": "l",
14+
"2rko1l7b": "m",
15+
"syaz1aj3": "n",
16+
"1p1dangw": "o",
17+
"bfhkbf54": "p"
18+
}

‎examples/parcel/compiledcss.js

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
const classNameCompressionMap = require('./class-name-compression-map.json');
2+
3+
module.exports = {
4+
importReact: false,
5+
extensions: ['.js', '.jsx', '.ts', '.tsx', '.customjsx'],
6+
parserBabelPlugins: ['typescript', 'jsx'],
7+
transformerBabelPlugins: [
8+
[
9+
'@babel/plugin-proposal-decorators',
10+
{
11+
legacy: true,
12+
},
13+
],
14+
],
15+
extract: true,
16+
optimizeCss: false,
17+
classNameCompressionMap: classNameCompressionMap,
18+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"1wyb12am": "a",
3+
"syaz32ev": "b",
4+
"k48pbfng": "c",
5+
"30l35scu": "d",
6+
"f8pj13q2": "e",
7+
"19itptrx": "f",
8+
"1kt92a4o": "g",
9+
"171dak0l": "h",
10+
"1swkri7e": "i",
11+
"1tjq14ap": "j",
12+
"yzbc5scu": "k",
13+
"19pk1ul9": "l",
14+
"syaz13q2": "m",
15+
"1wyb1ul9": "n",
16+
"19itlf8h": "o",
17+
"ca0q1vi7": "p",
18+
"u5f31vi7": "q",
19+
"n3td1vi7": "r",
20+
"19bv1vi7": "s",
21+
"k48p1fw0": "t",
22+
"syaz1cnh": "u",
23+
"19it1srw": "v",
24+
"bfhk1j28": "w",
25+
"syazruxl": "x",
26+
"bfhkbf54": "y"
27+
}

‎examples/webpack/webpack.config.js

+3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ const HtmlWebpackPlugin = require('html-webpack-plugin');
77
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
88
const webpack = require('webpack');
99

10+
const classNameCompressionMap = require('./class-name-compression-map.json');
11+
1012
const extractCSS = process.env.EXTRACT_TO_CSS === 'true';
1113

1214
module.exports = {
@@ -40,6 +42,7 @@ module.exports = {
4042
parserBabelPlugins: ['typescript', 'jsx'],
4143
transformerBabelPlugins: [['@babel/plugin-proposal-decorators', { legacy: true }]],
4244
optimizeCss: false,
45+
classNameCompressionMap,
4346
},
4447
},
4548
],

‎packages/babel-plugin/src/__tests__/index.test.ts

+156
Original file line numberDiff line numberDiff line change
@@ -201,4 +201,160 @@ describe('babel plugin', () => {
201201

202202
expect(actual).toInclude('c_MyDiv');
203203
});
204+
205+
it('should compress class name for styled component', () => {
206+
const actual = transform(
207+
`
208+
import { styled } from '@compiled/react';
209+
210+
const MyDiv = styled.div\`
211+
font-size: 12px;
212+
\`;
213+
`,
214+
{
215+
classNameCompressionMap: {
216+
'1wyb1fwx': 'a',
217+
},
218+
}
219+
);
220+
221+
expect(actual).toIncludeMultiple(['.a{font-size:12px}', 'ax(["_1wyb_a", __cmplp.className])']);
222+
});
223+
224+
it('should compress class name for css props', () => {
225+
const actual = transform(
226+
`
227+
import '@compiled/react';
228+
229+
<div css={{ fontSize: 12 }} />
230+
`,
231+
{
232+
classNameCompressionMap: {
233+
'1wyb1fwx': 'a',
234+
},
235+
}
236+
);
237+
238+
expect(actual).toIncludeMultiple(['.a{font-size:12px}', 'ax(["_1wyb_a"])']);
239+
});
240+
241+
it('should compress class name for ClassNames', () => {
242+
const actual = transform(
243+
`
244+
import { ClassNames } from '@compiled/react';
245+
246+
<ClassNames>
247+
{({ css }) => (
248+
<div className={css({ fontSize: 12 })} />
249+
)}
250+
</ClassNames>
251+
`,
252+
{
253+
classNameCompressionMap: {
254+
'1wyb1fwx': 'a',
255+
},
256+
}
257+
);
258+
259+
expect(actual).toIncludeMultiple(['.a{font-size:12px}', 'className={ax(["_1wyb_a"])']);
260+
});
261+
262+
it('should compress class names with atrules', () => {
263+
const actual = transform(
264+
`
265+
import '@compiled/react';
266+
<div css={{ "@media (max-width: 1250px) ": { fontSize: 12 } }} />
267+
`,
268+
{
269+
classNameCompressionMap: {
270+
pz521fwx: 'a',
271+
},
272+
}
273+
);
274+
275+
expect(actual).toIncludeMultiple([
276+
'@media (max-width:1250px){.a{font-size:12px}}',
277+
'ax(["_pz52_a"])',
278+
]);
279+
});
280+
281+
it('should compress pseudo classes', () => {
282+
const actual = transform(
283+
`
284+
import '@compiled/react';
285+
<div css={{ "&:hover": { fontSize: 12 }, "&:active": { color: 'red' } }} />
286+
`,
287+
{
288+
classNameCompressionMap: {
289+
'9h8h5scu': 'a',
290+
e9151fwx: 'b',
291+
},
292+
}
293+
);
294+
295+
expect(actual).toIncludeMultiple([
296+
'.a:active{color:red}',
297+
'.b:hover{font-size:12px}',
298+
'ax(["_e915_b _9h8h_a"])',
299+
]);
300+
});
301+
302+
it('should compress nested selector', () => {
303+
const actual = transform(
304+
`
305+
import '@compiled/react';
306+
<div css={{ '>div': { 'div div:hover': { fontSize: 12 } } }} />
307+
`,
308+
{
309+
classNameCompressionMap: {
310+
'1jkf1fwx': 'a',
311+
},
312+
}
313+
);
314+
315+
expect(actual).toIncludeMultiple(['.a >div div div:hover{font-size:12px}', 'ax(["_1jkf_a"]']);
316+
});
317+
318+
it('should compress conditional class names', () => {
319+
const actual = transform(
320+
`
321+
import '@compiled/react';
322+
<div css={[{ fontSize: ({ bar }) => bar ? 14 : 16 }, () => foo ? { fontSize: 12 } : {}, baz && { fontSize: 20 }]} />
323+
`,
324+
{
325+
classNameCompressionMap: {
326+
'1wyb19ub': 'a',
327+
'1wyb1fwx': 'b',
328+
},
329+
}
330+
);
331+
332+
expect(actual).toIncludeMultiple([
333+
'.a{font-size:16}',
334+
'.b{font-size:12px}',
335+
'bar ? "_1wyb1o8a" : "_1wyb_a"',
336+
'foo && "_1wyb_b"',
337+
]);
338+
});
339+
340+
it('should compress class names according to the map', () => {
341+
const actual = transform(
342+
`
343+
import '@compiled/react';
344+
<div css={{ fontSize: 12, color: 'red', marginTop: 10 }} />
345+
`,
346+
{
347+
classNameCompressionMap: {
348+
syaz5scu: 'a',
349+
},
350+
}
351+
);
352+
353+
expect(actual).toIncludeMultiple([
354+
'._19pk19bv{margin-top:10px}',
355+
'.a{color:red}',
356+
'._1wyb1fwx{font-size:12px}',
357+
'ax(["_1wyb1fwx _syaz_a _19pk19bv"]',
358+
]);
359+
});
204360
});

‎packages/babel-plugin/src/__tests__/jsx-automatic.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ describe('jsx automatic', () => {
4949
children: [_],
5050
}),
5151
_jsx("div", {
52-
className: "_syaz13q2",
52+
className: ax(["_syaz13q2"]),
5353
}),
5454
],
5555
});

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

+86-15
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ describe('class names behaviour', () => {
2929
const ListItem = () => (
3030
<CC>
3131
<CS>{[_]}</CS>
32-
{<div className={"_1wybgktf"}>hello, world!</div>}
32+
{<div className={ax(["_1wybgktf"])}>hello, world!</div>}
3333
</CC>
3434
);
3535
"
@@ -55,7 +55,7 @@ describe('class names behaviour', () => {
5555
<CC>
5656
<CS>{[_]}</CS>
5757
{(() => {
58-
return <div className={"_1wybgktf"}>hello, world!</div>;
58+
return <div className={ax(["_1wybgktf"])}>hello, world!</div>;
5959
})()}
6060
</CC>
6161
);
@@ -101,7 +101,7 @@ describe('class names behaviour', () => {
101101
const ListItem = () => (
102102
<CC>
103103
<CS>{[_]}</CS>
104-
{<div className={"_1wybgktf"}>hello, world!</div>}
104+
{<div className={ax(["_1wybgktf"])}>hello, world!</div>}
105105
</CC>
106106
);
107107
"
@@ -165,14 +165,18 @@ describe('class names behaviour', () => {
165165
<CS>{[_, _2, _3, _4, _5]}</CS>
166166
{
167167
<>
168-
<div className={"_5sagymdr _j7hq1sbx _1pgl1ytf"}>
168+
<div className={ax(["_5sagymdr _j7hq1sbx _1pgl1ytf"])}>
169169
longhand object call expression
170170
</div>
171-
<div className={"_y44vonb9"}>shorthand object call expression</div>
172-
<div className={"_5sagymdr _j7hq1sbx _1pgl1ytf"}>
171+
<div className={ax(["_y44vonb9"])}>
172+
shorthand object call expression
173+
</div>
174+
<div className={ax(["_5sagymdr _j7hq1sbx _1pgl1ytf"])}>
173175
longhand tagged template expression
174176
</div>
175-
<div className={"_y44vonb9"}>shorthand tagged template expression</div>
177+
<div className={ax(["_y44vonb9"])}>
178+
shorthand tagged template expression
179+
</div>
176180
</>
177181
}
178182
</CC>
@@ -241,7 +245,7 @@ describe('class names behaviour', () => {
241245
const ListItem = () => (
242246
<CC>
243247
<CS>{[_]}</CS>
244-
{<div className={"_1wybgktf"}>hello, world!</div>}
248+
{<div className={ax(["_1wybgktf"])}>hello, world!</div>}
245249
</CC>
246250
);
247251
"
@@ -275,8 +279,8 @@ describe('class names behaviour', () => {
275279
{
276280
<div
277281
className={{
278-
button: "_syaz5scu _1wybgktf",
279-
container: "_syaz13q2 _1wybgktf",
282+
button: ax(["_syaz5scu _1wybgktf"]),
283+
container: ax(["_syaz13q2 _1wybgktf"]),
280284
}}
281285
>
282286
hello, world!
@@ -304,7 +308,7 @@ describe('class names behaviour', () => {
304308
const ListItem = () => (
305309
<CC>
306310
<CS>{[_]}</CS>
307-
{<div className={"_1wybgktf"}>hello, world!</div>}
311+
{<div className={ax(["_1wybgktf"])}>hello, world!</div>}
308312
</CC>
309313
);
310314
"
@@ -343,7 +347,7 @@ describe('class names behaviour', () => {
343347
const ListItem = ({ children }) => (
344348
<CC>
345349
<CS>{[_]}</CS>
346-
{children("_1wybgktf")}
350+
{children(ax(["_1wybgktf"]))}
347351
</CC>
348352
);
349353
"
@@ -361,7 +365,7 @@ describe('class names behaviour', () => {
361365
);
362366
`);
363367

364-
expect(actual).toInclude(`<div className={\"_1wyb1fwx\"} /`);
368+
expect(actual).toInclude(`<div className={ax([\"_1wyb1fwx\"])} /`);
365369
});
366370

367371
it('should replace style identifier with undefined', () => {
@@ -399,7 +403,7 @@ describe('class names behaviour', () => {
399403
style={{
400404
"--_1ylxx6h": ix(color),
401405
}}
402-
className={"_syaz1aj3"}
406+
className={ax(["_syaz1aj3"])}
403407
/>
404408
}
405409
</CC>
@@ -466,7 +470,7 @@ describe('class names behaviour', () => {
466470
{(() => {
467471
const { css: c, style: styl } = arg;
468472
return (
469-
<div style={undefined} className={"_1wyb19bv _syaz5scu"}>
473+
<div style={undefined} className={ax(["_1wyb19bv _syaz5scu"])}>
470474
hello world
471475
</div>
472476
);
@@ -476,4 +480,71 @@ describe('class names behaviour', () => {
476480
"
477481
`);
478482
});
483+
484+
it('should apply conditional logical expression object spread styles', () => {
485+
const actual = transform(`
486+
import { ClassNames } from '@compiled/react';
487+
488+
const ListItem = (props) => (
489+
<ClassNames>
490+
{({ css }) => (<div className={css({
491+
...props.isPrimary && {
492+
color: 'blue',
493+
fontSize: 20
494+
}})}>hello, world!</div>)}
495+
</ClassNames>
496+
);
497+
`);
498+
499+
expect(actual).toInclude('className={ax([props.isPrimary && "_syaz13q2 _1wybgktf"])}');
500+
});
501+
502+
it('should apply array logical-based conditional css', () => {
503+
const actual = transform(
504+
`
505+
import { ClassNames } from '@compiled/react';
506+
507+
const ListItem = (props) => (
508+
<ClassNames>
509+
{({ css }) => (<div className={css([
510+
{ fontSize: 40, },
511+
(props.isPrimary || props.isMaybe) && {
512+
color: 'blue',
513+
fontSize: 20,
514+
},
515+
])}>hello, world!</div>)}
516+
</ClassNames>
517+
);
518+
`,
519+
{ pretty: false }
520+
);
521+
522+
expect(actual).toInclude(
523+
'className={ax(["_1wyb1ylp",(props.isPrimary||props.isMaybe)&&"_syaz13q2 _1wybgktf"])}'
524+
);
525+
});
526+
527+
it('should apply array prop ternary-based inline conditional css', () => {
528+
const actual = transform(
529+
`
530+
import { ClassNames } from '@compiled/react';
531+
532+
const ListItem = (props) => (
533+
<ClassNames>
534+
{({ css }) => (<div className={css([
535+
props.isPrimary
536+
? { background: 'white', color: 'black' }
537+
: { background: 'green', color: 'red' },
538+
{ 'font-size': '12px' }
539+
])}>hello, world!</div>)}
540+
</ClassNames>
541+
);
542+
`,
543+
{ pretty: false }
544+
);
545+
546+
expect(actual).toInclude(
547+
'className={ax([props.isPrimary?"_bfhk1x77 _syaz11x8":"_bfhkbf54 _syaz5scu","_1wyb1fwx"])}'
548+
);
549+
});
479550
});

‎packages/babel-plugin/src/class-names/__tests__/tagged-template-expression.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ describe('ClassNames used with a css tagged template expression', () => {
1515

1616
expect(actual).toIncludeMultiple([
1717
'const _ = "._1wybgktf{font-size:20px}"',
18-
'className={"_1wybgktf"}',
18+
'className={ax(["_1wybgktf"])}',
1919
]);
2020
});
2121

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

+4-7
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import type { NodePath } from '@babel/core';
22
import * as t from '@babel/types';
3-
import { transformCss } from '@compiled/css';
43

54
import type { Metadata } from '../types';
65
import { buildCodeFrameError, pickFunctionBody } from '../utils/ast';
76
import { compiledTemplate } from '../utils/build-compiled-component';
87
import { buildCssVariables } from '../utils/build-css-variables';
9-
import { buildCss, getItemCss } from '../utils/css-builders';
8+
import { buildCss } from '../utils/css-builders';
109
import { resolveIdentifierComingFromDestructuring } from '../utils/resolve-binding';
10+
import { transformCssItems } from '../utils/transform-css-items';
1111
import type { CSSOutput } from '../utils/types';
1212

1313
/**
@@ -132,15 +132,12 @@ export const visitClassNamesPath = (path: NodePath<t.JSXElement>, meta: Metadata
132132
}
133133

134134
const builtCss = buildCss(styles, meta);
135-
const { sheets, classNames } = transformCss(
136-
builtCss.css.map((x) => getItemCss(x)).join(''),
137-
meta.state.opts
138-
);
135+
const { sheets, classNames } = transformCssItems(builtCss.css, meta);
139136

140137
collectedVariables.push(...builtCss.variables);
141138
collectedSheets.push(...sheets);
142139

143-
path.replaceWith(t.stringLiteral(classNames.join(' ')));
140+
path.replaceWith(t.callExpression(t.identifier('ax'), [t.arrayExpression(classNames)]));
144141
},
145142
});
146143

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

+8
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,14 @@ export interface PluginOptions {
6767
* Default to `false`
6868
*/
6969
addComponentName?: boolean;
70+
71+
/**
72+
* A map holds the key-value pairs between full Atomic class names and the compressed ones
73+
* i.e. { '_aaaabbbb': 'a' }
74+
*
75+
* Default to `undefined`
76+
*/
77+
classNameCompressionMap?: { [index: string]: string };
7078
}
7179

7280
export interface State extends PluginPass {

‎packages/babel-plugin/src/utils/build-styled-component.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type { Metadata, Tag } from '../types';
1616

1717
import { pickFunctionBody } from './ast';
1818
import { buildCssVariables } from './build-css-variables';
19+
import { compressClassNamesForAx } from './compress-class-names-for-ax';
1920
import { getItemCss } from './css-builders';
2021
import { hoistSheet } from './hoist-sheet';
2122
import { applySelectors, transformCssItems } from './transform-css-items';
@@ -237,7 +238,14 @@ export const buildStyledComponent = (tag: Tag, cssOutput: CSSOutput, meta: Metad
237238

238239
const sheets = [...uniqueUnconditionalCssOutput.sheets, ...conditionalCssOutput.sheets];
239240
const classNames = [
240-
...[t.stringLiteral(uniqueUnconditionalCssOutput.classNames.join(' '))],
241+
...[
242+
t.stringLiteral(
243+
compressClassNamesForAx(
244+
uniqueUnconditionalCssOutput.classNames,
245+
meta.state.opts.classNameCompressionMap
246+
).join(' ')
247+
),
248+
],
241249
...conditionalCssOutput.classNames,
242250
];
243251

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* Compress class names based on `classNameCompressionMap`.
3+
* The compressed class name has a format of `_aaaa_a`, which is expected by `ax`.
4+
* `aaaa` is the atomic group and `a` is the compressed name.
5+
*/
6+
export const compressClassNamesForAx = (
7+
classNames: string[],
8+
classNameCompressionMap?: { [index: string]: string }
9+
): string[] => {
10+
if (!classNameCompressionMap) return classNames;
11+
return classNames.map((className) => {
12+
const compressedClassName =
13+
classNameCompressionMap && classNameCompressionMap[className.slice(1)];
14+
return compressedClassName ? `_${className.slice(1, 5)}_${compressedClassName}` : className;
15+
});
16+
};

‎packages/babel-plugin/src/utils/transform-css-items.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { transformCss } from '@compiled/css';
33

44
import type { Metadata } from '../types';
55

6+
import { compressClassNamesForAx } from './compress-class-names-for-ax';
67
import { getItemCss } from './css-builders';
78
import type { CssItem } from './types';
89

@@ -64,13 +65,21 @@ const transformCssItem = (
6465
classExpression: t.logicalExpression(
6566
item.operator,
6667
item.expression,
67-
t.stringLiteral(logicalCss.classNames.join(' '))
68+
t.stringLiteral(
69+
compressClassNamesForAx(
70+
logicalCss.classNames,
71+
meta.state.opts.classNameCompressionMap
72+
).join(' ')
73+
)
6874
),
6975
};
7076

7177
default:
7278
const css = transformCss(getItemCss(item), meta.state.opts);
73-
const className = css.classNames.join(' ');
79+
const className = compressClassNamesForAx(
80+
css.classNames,
81+
meta.state.opts.classNameCompressionMap
82+
).join(' ');
7483

7584
return {
7685
sheets: css.sheets,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
import { generateCompressionMap as generate } from '../generate-compression-map';
2+
3+
describe('generate compression map', () => {
4+
const baseCSS = `
5+
._154i14e6{top:33px}
6+
._14tk72c6>div:not([role=group])>a{padding-left:18.6px}
7+
._14n4stnw._14n4stnw{position:absolute}
8+
._13h81y44 span[role=button]{padding-top:4px}
9+
._1di6k6hx:active, ._irr3k6hx:hover{background-color:var(--ds-background-neutral-subtle-hovered,#091e420a)}
10+
._1di6k6hx:active, ._jomrk6hx:focus, ._10j7k6hx:focus-within, ._irr3k6hx:hover{background-color:var(--ds-background-neutral-subtle-hovered,#091e420a)}
11+
._1gg2glyw>a:active, ._1o3iglyw>a[aria-current=page]{-webkit-text-decoration-line:none;text-decoration-line:none}
12+
._1iohnqa1:active, ._5goinqa1:focus, ._jf4cnqa1:hover{-webkit-text-decoration-style:solid;text-decoration-style:solid}
13+
._1iohnqa1:active, ._jf4cnqa1:hover, ._xatrnqa1:link, ._1726nqa1:visited{-webkit-text-decoration-style:solid;text-decoration-style:solid}
14+
._1iqunqa1._1iqunqa1:active, ._1ejunqa1._1ejunqa1:hover, ._1lwpnqa1._1lwpnqa1:visited{-webkit-text-decoration-style:solid;text-decoration-style:solid}
15+
._1iqunqa1._1iqunqa1:active, ._6xf7nqa1._6xf7nqa1:focus, ._1ejunqa1._1ejunqa1:hover, ._1lwpnqa1._1lwpnqa1:visited{-webkit-text-decoration-style:solid;text-decoration-style:solid}
16+
._1mb818uv>a:active, ._oga118uv>a[aria-current=page]{-webkit-text-decoration-color:initial;text-decoration-color:initial}
17+
._1n2onqa1>a:active, ._1k4fnqa1>a[aria-current=page]{-webkit-text-decoration-style:solid;text-decoration-style:solid}
18+
._1nrm18uv:active, ._1a3b18uv:focus, ._9oik18uv:hover{-webkit-text-decoration-color:initial;text-decoration-color:initial}
19+
._1nrm18uv:active, ._9oik18uv:hover, ._5bd618uv:link, ._1ydc18uv:visited{-webkit-text-decoration-color:initial;text-decoration-color:initial}
20+
._1ohyglyw:active, ._49pcglyw:focus, ._ra3xglyw:focus-visible, ._ksodglyw:hover{outline-style:none}
21+
._1ohyglyw:active, ._ksodglyw:hover, ._q4asglyw:link, ._tpgfglyw:visited{outline-style:none}
22+
._1oxgru3m:active{transition-duration:0s}
23+
._9h8h1e9r:active, ._f8pj1e9r:focus, ._30l31e9r:hover, ._10531e9r:visited{color:var(--ds-text-subtlest,#7a869a)}
24+
@media screen and (min-width:1300px){._1jhpoyl8{max-width:10vw}}
25+
@media (max-width:1199px){._11usglyw{display:none}}
26+
@media (min--moz-device-pixel-ratio:2){._11y7oza4{max-width:510px}._11y7uu9g{max-width:840px}._l82t7vkz{border-left-width:1pc}._j7o07vkz{border-right-width:1pc}._1od57vkz._1od57vkz{border-left-width:1pc}._l82tgktf{border-left-width:20px}._j7o0gktf{border-right-width:20px}._yksp1ssb{width:50%}._1b421ssb{height:50%}._s8ks18ws{transform:scale(2)}._u1wz1nty{transform-origin:0 0}}
27+
@media (min-width:1000px) and (max-width:1439px){._hnu8tcjq{display:block!important}}
28+
@media (min-width:1200px){._jvpg11p5{display:grid}._1nwdwxkt{grid-template-columns:1fr 1fr}._1vlxckbl{grid-gap:3pc}._kz8c16xz{padding-top:6pc}._1jyu16xz{padding-right:6pc}._11et16xz{padding-bottom:6pc}._fgkv16xz{padding-left:6pc}._szna1wug{margin-top:auto}._13on1wug{margin-right:auto}._1f3k1wug{margin-bottom:auto}._inid1wug{margin-left:auto}._12wp9ac1{max-width:1400px}._jvpgglyw{display:none}}
29+
@media (min-width:1440px) and (max-width:1919px){._pbi4tcjq{display:block!important}}
30+
@media (min-width:1920px) and (max-width:2559px){._16b9tcjq{display:block!important}}
31+
@media (min-width:2560px) and (max-width:2999px){._jmaqtcjq{display:block!important}}
32+
@media (min-width:3000px){._1q5htcjq{display:block!important}}
33+
@media (min-width:800px) and (max-width:999px){._11x1tcjq{display:block!important}}
34+
@media (min-width:800px){._121jagmp{display:none!important}}
35+
@media (prefers-reduced-motion:reduce){._1bumglyw{animation:none}._sedtglyw{transition:none}}
36+
@media screen and (-webkit-min-device-pixel-ratio:0){._14kw1hna >textarea{word-break:break-word}._mc2h1hna{word-break:break-word}}
37+
@media screen and (-webkit-transition){._14fy1hna{word-break:break-word}._1vdp1hna >textarea{word-break:break-word}}
38+
@media screen and (max-height:400px){._17gjpfqs{position:static}}
39+
@media screen and (min-width:1300px){._1jhpoyl8{max-width:10vw}}
40+
`;
41+
42+
const baseResult = {
43+
'154i14e6': 'a',
44+
'14tk72c6': 'b',
45+
'14n4stnw': 'c',
46+
'13h81y44': 'd',
47+
'1di6k6hx': 'e',
48+
irr3k6hx: 'f',
49+
jomrk6hx: 'g',
50+
'10j7k6hx': 'h',
51+
'1gg2glyw': 'i',
52+
'1o3iglyw': 'j',
53+
'1iohnqa1': 'k',
54+
'5goinqa1': 'l',
55+
jf4cnqa1: 'm',
56+
xatrnqa1: 'n',
57+
'1726nqa1': 'o',
58+
'1iqunqa1': 'p',
59+
'1ejunqa1': 'q',
60+
'1lwpnqa1': 'r',
61+
'6xf7nqa1': 's',
62+
'1mb818uv': 't',
63+
oga118uv: 'u',
64+
'1n2onqa1': 'v',
65+
'1k4fnqa1': 'w',
66+
'1nrm18uv': 'x',
67+
'1a3b18uv': 'y',
68+
'9oik18uv': 'z',
69+
'5bd618uv': 'A',
70+
'1ydc18uv': 'B',
71+
'1ohyglyw': 'C',
72+
'49pcglyw': 'D',
73+
ra3xglyw: 'E',
74+
ksodglyw: 'F',
75+
q4asglyw: 'G',
76+
tpgfglyw: 'H',
77+
'1oxgru3m': 'I',
78+
'9h8h1e9r': 'J',
79+
f8pj1e9r: 'K',
80+
'30l31e9r': 'L',
81+
'10531e9r': 'M',
82+
'1jhpoyl8': 'N',
83+
'11usglyw': 'O',
84+
'11y7oza4': 'P',
85+
'11y7uu9g': 'Q',
86+
l82t7vkz: 'R',
87+
j7o07vkz: 'S',
88+
'1od57vkz': 'T',
89+
l82tgktf: 'U',
90+
j7o0gktf: 'V',
91+
yksp1ssb: 'W',
92+
'1b421ssb': 'X',
93+
s8ks18ws: 'Y',
94+
u1wz1nty: 'Z',
95+
hnu8tcjq: '_',
96+
jvpg11p5: 'aa',
97+
'1nwdwxkt': 'ba',
98+
'1vlxckbl': 'ca',
99+
kz8c16xz: 'da',
100+
'1jyu16xz': 'ea',
101+
'11et16xz': 'fa',
102+
fgkv16xz: 'ga',
103+
szna1wug: 'ha',
104+
'13on1wug': 'ia',
105+
'1f3k1wug': 'ja',
106+
inid1wug: 'ka',
107+
'12wp9ac1': 'la',
108+
jvpgglyw: 'ma',
109+
pbi4tcjq: 'na',
110+
'16b9tcjq': 'oa',
111+
jmaqtcjq: 'pa',
112+
'1q5htcjq': 'qa',
113+
'11x1tcjq': 'ra',
114+
'121jagmp': 'sa',
115+
'1bumglyw': 'ta',
116+
sedtglyw: 'ua',
117+
'14kw1hna': 'va',
118+
mc2h1hna: 'wa',
119+
'14fy1hna': 'xa',
120+
'1vdp1hna': 'ya',
121+
'17gjpfqs': 'za',
122+
};
123+
it('should generate class names as expected', () => {
124+
const result = generate(baseCSS);
125+
expect(result).toStrictEqual(baseResult);
126+
});
127+
128+
it('should generate class names with the old compression map', () => {
129+
const oldCompressionMap: { [index: string]: string } = {
130+
'17gjpfqs': 'a',
131+
'1vdp1hna': 'b',
132+
'14fy1hna': 'c',
133+
};
134+
const result = generate(baseCSS, { oldClassNameCompressionMap: oldCompressionMap });
135+
for (const property in oldCompressionMap) {
136+
expect(result).toHaveProperty(property, oldCompressionMap[property]);
137+
}
138+
});
139+
140+
it('should generate class names with prefix', () => {
141+
const result = generate(baseCSS, { prefix: '_' });
142+
expect(result).toStrictEqual({
143+
'154i14e6': '_a',
144+
'14tk72c6': '_b',
145+
'14n4stnw': '_c',
146+
'13h81y44': '_d',
147+
'1di6k6hx': '_e',
148+
irr3k6hx: '_f',
149+
jomrk6hx: '_g',
150+
'10j7k6hx': '_h',
151+
'1gg2glyw': '_i',
152+
'1o3iglyw': '_j',
153+
'1iohnqa1': '_k',
154+
'5goinqa1': '_l',
155+
jf4cnqa1: '_m',
156+
xatrnqa1: '_n',
157+
'1726nqa1': '_o',
158+
'1iqunqa1': '_p',
159+
'1ejunqa1': '_q',
160+
'1lwpnqa1': '_r',
161+
'6xf7nqa1': '_s',
162+
'1mb818uv': '_t',
163+
oga118uv: '_u',
164+
'1n2onqa1': '_v',
165+
'1k4fnqa1': '_w',
166+
'1nrm18uv': '_x',
167+
'1a3b18uv': '_y',
168+
'9oik18uv': '_z',
169+
'5bd618uv': '_A',
170+
'1ydc18uv': '_B',
171+
'1ohyglyw': '_C',
172+
'49pcglyw': '_D',
173+
ra3xglyw: '_E',
174+
ksodglyw: '_F',
175+
q4asglyw: '_G',
176+
tpgfglyw: '_H',
177+
'1oxgru3m': '_I',
178+
'9h8h1e9r': '_J',
179+
f8pj1e9r: '_K',
180+
'30l31e9r': '_L',
181+
'10531e9r': '_M',
182+
'1jhpoyl8': '_N',
183+
'11usglyw': '_O',
184+
'11y7oza4': '_P',
185+
'11y7uu9g': '_Q',
186+
l82t7vkz: '_R',
187+
j7o07vkz: '_S',
188+
'1od57vkz': '_T',
189+
l82tgktf: '_U',
190+
j7o0gktf: '_V',
191+
yksp1ssb: '_W',
192+
'1b421ssb': '_X',
193+
s8ks18ws: '_Y',
194+
u1wz1nty: '_Z',
195+
hnu8tcjq: '__',
196+
jvpg11p5: '_-',
197+
'1nwdwxkt': '_0',
198+
'1vlxckbl': '_1',
199+
kz8c16xz: '_2',
200+
'1jyu16xz': '_3',
201+
'11et16xz': '_4',
202+
fgkv16xz: '_5',
203+
szna1wug: '_6',
204+
'13on1wug': '_7',
205+
'1f3k1wug': '_8',
206+
inid1wug: '_9',
207+
'12wp9ac1': '_aa',
208+
jvpgglyw: '_ba',
209+
pbi4tcjq: '_ca',
210+
'16b9tcjq': '_da',
211+
jmaqtcjq: '_ea',
212+
'1q5htcjq': '_fa',
213+
'11x1tcjq': '_ga',
214+
'121jagmp': '_ha',
215+
'1bumglyw': '_ia',
216+
sedtglyw: '_ja',
217+
'14kw1hna': '_ka',
218+
mc2h1hna: '_la',
219+
'14fy1hna': '_ma',
220+
'1vdp1hna': '_na',
221+
'17gjpfqs': '_oa',
222+
});
223+
});
224+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import postcss from 'postcss';
2+
import selectorParser from 'postcss-selector-parser';
3+
4+
import { ClassNameGenerator } from './utils/class-name-generator';
5+
6+
const UNDERSCORE_UNICODE = 95;
7+
8+
/**
9+
* Generate a compression map, which is used by @compiled/babel-plugin to compress class names.
10+
* The compression map looks like { 'aaaabbbb': 'a', 'bbbbcccc': 'b' }
11+
*
12+
* @param stylesheet css content i.e. `.aaaabbbb{font-size: 10px}`
13+
* @param oldClassNameCompressionMap the previous compression map, which ensures the compression is deterministic.
14+
* @returns newClassNameCompressionMap
15+
*/
16+
export const generateCompressionMap = (
17+
css: string,
18+
opts?: { oldClassNameCompressionMap?: { [index: string]: string }; prefix?: string }
19+
): undefined | { [index: string]: string } => {
20+
const { oldClassNameCompressionMap, prefix } = opts || {};
21+
22+
let classNamesToCompress: string[] = [];
23+
const classNameCompressionMap: { [index: string]: string } = {};
24+
const reservedClassNames: string[] = [];
25+
26+
const selectorProcessor = selectorParser((selectors) => {
27+
selectors.walkClasses((node: selectorParser.ClassName | selectorParser.Identifier) => {
28+
// Only compress Atomic class names, which has the format of `_aaaabbbb`.
29+
if (node.value.charCodeAt(0) === UNDERSCORE_UNICODE && node.value.length === 9) {
30+
classNamesToCompress.push(node.value.slice(1));
31+
}
32+
});
33+
});
34+
35+
const result = postcss([
36+
{
37+
postcssPlugin: 'postcss-find-atomic-class-names',
38+
Rule(ruleNode) {
39+
selectorProcessor.process(ruleNode);
40+
},
41+
},
42+
]).process(css, { from: undefined });
43+
44+
// We need to access something to make the transformation happen.
45+
result.css;
46+
47+
// Remove duplicates
48+
classNamesToCompress = Array.from(new Set(classNamesToCompress));
49+
50+
// Check if class name to compress already exists in oldClassNameCompressionMap
51+
// If yes, re-use the compressed class name
52+
if (oldClassNameCompressionMap) {
53+
classNamesToCompress = classNamesToCompress.filter((className) => {
54+
if (oldClassNameCompressionMap[className]) {
55+
reservedClassNames.push(oldClassNameCompressionMap[className]);
56+
classNameCompressionMap[className] = oldClassNameCompressionMap[className];
57+
return false;
58+
}
59+
return true;
60+
});
61+
}
62+
63+
const classNameGenerator = new ClassNameGenerator({ reservedClassNames, prefix });
64+
classNamesToCompress.forEach((className) => {
65+
const newClassName = classNameGenerator.generateClassName();
66+
classNameCompressionMap[className] = newClassName;
67+
});
68+
69+
return classNameCompressionMap;
70+
};

‎packages/css/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ export {
66
BeforeInterpolation,
77
} from './utils/css-affix-interpolation';
88
export { sort } from './sort';
9+
export { generateCompressionMap } from './generate-compression-map';

‎packages/css/src/plugins/atomicify-rules.ts

+13-4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { Plugin, ChildNode, Declaration, Container, Rule, AtRule } from 'po
33
import { rule } from 'postcss';
44

55
interface PluginOpts {
6+
classNameCompressionMap?: { [index: string]: string };
67
callback?: (className: string) => void;
78
}
89

@@ -76,20 +77,28 @@ const replaceNestingSelector = (selector: string, parentClassName: string) => {
7677
* @param node
7778
*/
7879
const buildAtomicSelector = (node: Declaration, opts: AtomicifyOpts) => {
80+
const { classNameCompressionMap } = opts;
7981
const selectors: string[] = [];
8082

8183
(opts.selectors || ['']).forEach((selector) => {
8284
const normalizedSelector = normalizeSelector(selector);
83-
const className = atomicClassName(node, {
85+
const fullClassName = atomicClassName(node, {
8486
...opts,
8587
selectors: [normalizedSelector],
8688
});
87-
const replacedSelector = replaceNestingSelector(normalizedSelector, className);
8889

89-
selectors.push(replacedSelector);
90+
const compressedClassName =
91+
classNameCompressionMap && classNameCompressionMap[fullClassName.slice(1)];
92+
93+
if (compressedClassName) {
94+
// Use compressed class name if compressedClassName is available
95+
selectors.push(replaceNestingSelector(normalizedSelector, compressedClassName));
96+
} else {
97+
selectors.push(replaceNestingSelector(normalizedSelector, fullClassName));
98+
}
9099

91100
if (opts.callback) {
92-
opts.callback(className);
101+
opts.callback(fullClassName);
93102
}
94103
});
95104

‎packages/css/src/transform.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { sortAtRulePseudos } from './plugins/sort-at-rule-pseudos';
1515

1616
interface TransformOpts {
1717
optimizeCss?: boolean;
18+
classNameCompressionMap?: object;
1819
}
1920

2021
/**
@@ -38,7 +39,10 @@ export const transformCss = (
3839
nested(),
3940
...normalizeCSS(opts),
4041
expandShorthands(),
41-
atomicifyRules({ callback: (className: string) => classNames.push(className) }),
42+
atomicifyRules({
43+
classNameCompressionMap: opts.classNameCompressionMap,
44+
callback: (className: string) => classNames.push(className),
45+
}),
4246
sortAtRulePseudos(),
4347
...(process.env.AUTOPREFIXER === 'off' ? [] : [autoprefixer()]),
4448
whitespace(),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { ClassNameGenerator } from '../class-name-generator';
2+
3+
describe('ClassNameGenerator', () => {
4+
it('should generate class names with minimal length', () => {
5+
const generator = new ClassNameGenerator();
6+
Array.from(Array(27).keys()).forEach(() => {
7+
const className = generator.generateClassName();
8+
expect(className.length).toBe(1);
9+
});
10+
});
11+
12+
it('should skip reservedClassNames', () => {
13+
const generator = new ClassNameGenerator({ reservedClassNames: ['a', 'b', 'c'] });
14+
const className = generator.generateClassName();
15+
expect(className).toBe('d');
16+
});
17+
18+
it('should not generate class names starting with a number if prefix is not given', () => {
19+
const generator = new ClassNameGenerator();
20+
Array.from(Array(30).keys()).forEach(() => {
21+
const className = generator.generateClassName();
22+
expect(className.charAt(0)).toMatch(/[^1-9]/);
23+
});
24+
});
25+
26+
it('should prefix class names', () => {
27+
const prefix = '_';
28+
const generator = new ClassNameGenerator({ prefix });
29+
expect(generator.generateClassName().startsWith(prefix)).toBeTrue();
30+
});
31+
32+
it('should throw an error if invalid prefix is given', () => {
33+
expect(() => {
34+
new ClassNameGenerator({ prefix: '-' });
35+
}).toThrowErrorMatchingInlineSnapshot(
36+
`"'-' is an invalid prefix. The allowed prefix is [a-zA-Z_]"`
37+
);
38+
});
39+
40+
it('should not generate class name which includes the word "ad"', () => {
41+
const generator = new ClassNameGenerator({ prefix: 'a' });
42+
Array.from(Array(10).keys()).forEach(() => {
43+
const className = generator.generateClassName();
44+
expect(className.toLocaleLowerCase().includes('ad')).toBeFalse();
45+
});
46+
});
47+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// CSS classes are case sensitive in non-quirk mode
2+
// Spec: https://html.spec.whatwg.org/multipage/semantics-other.html#case-sensitivity-of-selectors
3+
// CSS classes can contain only the characters [a-zA-Z0-9] and ISO 10646 characters U+00A0 and higher, plus the hyphen (-) and the underscore (_); they cannot start with a digit, two hyphens, or a hyphen followed by a digit.
4+
// Spec: https://www.w3.org/TR/CSS21/syndata.html#characters
5+
const acceptPrefixBase = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_';
6+
const acceptPrefix = acceptPrefixBase.split('');
7+
const acceptChars = `${acceptPrefixBase}-0123456789`.split('');
8+
9+
export class ClassNameGenerator {
10+
newClassSize: number;
11+
reservedClassNames: string[];
12+
prefix?: string;
13+
constructor(opts: { reservedClassNames?: string[]; prefix?: string } = {}) {
14+
this.newClassSize = 0;
15+
this.reservedClassNames = opts.reservedClassNames || [];
16+
this.prefix = opts.prefix;
17+
18+
if (this.prefix && !acceptPrefix.includes(this.prefix)) {
19+
throw new Error(`'${this.prefix}' is an invalid prefix. The allowed prefix is [a-zA-Z_]`);
20+
}
21+
}
22+
generateClassName(): string {
23+
const chars = [];
24+
let rest = this.prefix
25+
? this.newClassSize + 1
26+
: (this.newClassSize - (this.newClassSize % acceptPrefix.length)) / acceptPrefix.length;
27+
if (rest > 0) {
28+
while (true) {
29+
rest -= 1;
30+
const m = rest % acceptChars.length;
31+
const c = acceptChars[m];
32+
chars.push(c);
33+
rest -= m;
34+
if (rest === 0) {
35+
break;
36+
}
37+
rest /= acceptChars.length;
38+
}
39+
}
40+
const newClassName = `${
41+
this.prefix ? this.prefix : acceptPrefix[this.newClassSize % acceptPrefix.length]
42+
}${chars.join('')}`;
43+
44+
if (this.reservedClassNames && this.reservedClassNames.includes(newClassName)) {
45+
this.newClassSize++;
46+
return this.generateClassName();
47+
}
48+
49+
// Avoid any class name which includes the word 'ad' to prevent adblocker from blocking the HTML element
50+
if (newClassName.toLowerCase().includes('ad')) {
51+
this.newClassSize++;
52+
return this.generateClassName();
53+
}
54+
55+
this.newClassSize++;
56+
return newClassName;
57+
}
58+
}

‎packages/parcel-transformer/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ export default new Transformer<ParcelTransformerOpts>({
121121
'@compiled/babel-plugin',
122122
{
123123
...config,
124+
classNameCompressionMap: config.extract && config.classNameCompressionMap,
124125
onIncludedFiles: (files: string[]) => includedFiles.push(...files),
125126
resolver: {
126127
// The resolver needs to be synchronous, as babel plugins must be synchronous

‎packages/parcel-transformer/src/types.ts

+8
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,12 @@ export interface ParcelTransformerOpts extends BabelPluginOpts {
3939
* Default to `false`
4040
*/
4141
addComponentName?: boolean;
42+
43+
/**
44+
* A map holds the key-value pairs between full Atomic class names and the compressed ones
45+
* i.e. { '_aaaabbbb': 'a' }
46+
*
47+
* Default to `undefined`
48+
*/
49+
classNameCompressionMap?: { [index: string]: string };
4250
}

‎packages/react/src/runtime/__perf__/ax.test.ts

+39-17
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,26 @@ import { runBenchmark } from '@compiled/benchmark';
33
import { ax } from '../index';
44

55
describe('ax benchmark', () => {
6-
it('completes with ax() string as the fastest', async () => {
7-
const arr = [
8-
'_19itglyw',
9-
'_2rko1l7b',
10-
'_ca0qftgi',
11-
'_u5f319bv',
12-
'_n3tdftgi',
13-
'_19bv19bv',
14-
'_bfhk1mzw',
15-
'_syazu67f',
16-
'_k48p1nn1',
17-
'_ect41kw7',
18-
'_1wybdlk8',
19-
'_irr3mlcl',
20-
'_1di6vctu',
21-
undefined,
22-
];
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,
23+
];
2324

25+
it('completes with ax() string as the fastest', async () => {
2426
// Remove undefined and join the strings
2527
const str = arr.slice(0, -1).join(' ');
2628

@@ -39,4 +41,24 @@ describe('ax benchmark', () => {
3941
fastest: ['ax() string'],
4042
});
4143
}, 30000);
44+
45+
it('completes with ax() non-compressed class names as the fastest', async () => {
46+
const arrWithCompressedClassNames = arr.map((item) =>
47+
item ? `${item.slice(0, 4)}_${item.slice(8)}` : item
48+
);
49+
const benchmark = await runBenchmark('ax', [
50+
{
51+
name: 'ax() array',
52+
fn: () => ax(arr),
53+
},
54+
{
55+
name: 'ax() array with compressed class names',
56+
fn: () => ax(arrWithCompressedClassNames),
57+
},
58+
]);
59+
60+
expect(benchmark).toMatchObject({
61+
fastest: ['ax() array'],
62+
});
63+
}, 30000);
4264
});
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,81 @@
11
import ax from '../ax';
22

33
describe('ax', () => {
4-
it('should join single classes together', () => {
5-
const result = ax(['foo', 'bar']);
6-
7-
expect(result).toEqual('foo bar');
8-
});
9-
10-
it('should join multi classes together', () => {
11-
const result = ax(['foo baz', 'bar']);
12-
13-
expect(result).toEqual('foo baz bar');
14-
});
15-
16-
it('should remove undefined', () => {
17-
const result = ax(['foo', 'bar', undefined]);
18-
19-
expect(result).toEqual('foo bar');
20-
});
21-
22-
it('should ensure the last atomic declaration of a single group wins', () => {
23-
const result = ax(['_aaaabbbb', '_aaaacccc']);
24-
25-
expect(result).toEqual('_aaaacccc');
26-
});
27-
28-
it('should ensure the last atomic declaration of many single groups wins', () => {
29-
const result = ax(['_aaaabbbb', '_aaaacccc', '_aaaadddd', '_aaaaeeee']);
30-
31-
expect(result).toEqual('_aaaaeeee');
32-
});
33-
34-
it('should ensure the last atomic declaration of a multi group wins', () => {
35-
const result = ax(['_aaaabbbb _aaaacccc']);
36-
37-
expect(result).toEqual('_aaaacccc');
38-
});
39-
40-
it('should ensure the last atomic declaration of many multi groups wins', () => {
41-
const result = ax(['_aaaabbbb _aaaacccc _aaaadddd _aaaaeeee']);
42-
43-
expect(result).toEqual('_aaaaeeee');
44-
});
45-
46-
it('should not remove any atomic declarations if there are no duplicate groups', () => {
47-
const result = ax(['_aaaabbbb', '_bbbbcccc']);
48-
49-
expect(result).toEqual('_aaaabbbb _bbbbcccc');
50-
});
51-
52-
it('should not apply conditional class', () => {
53-
const isEnabled: boolean = (() => false)();
54-
const result = ax([isEnabled && 'foo', 'bar']);
55-
56-
expect(result).toEqual('bar');
57-
});
58-
59-
it('should ignore non atomic declarations', () => {
60-
const result = ax(['hello_there', 'hello_world']);
61-
62-
expect(result).toEqual('hello_there hello_world');
63-
});
64-
65-
it('should ignore non atomic declarations when atomic declarations exist', () => {
66-
const result = ax(['hello_there', 'hello_world', '_aaaabbbb']);
67-
68-
expect(result).toEqual('hello_there hello_world _aaaabbbb');
4+
const isEnabled: boolean = (() => false)();
5+
6+
it.each([
7+
['should handle empty array', [], undefined],
8+
['should handle array with undefined', [undefined], undefined],
9+
['should join single classes together', ['foo', 'bar'], 'foo bar'],
10+
['should join multi classes together', ['foo baz', 'bar'], 'foo baz bar'],
11+
['should remove undefined', ['foo', 'bar', undefined], 'foo bar'],
12+
[
13+
'should ensure the last atomic declaration of a single group wins',
14+
['_aaaabbbb', '_aaaacccc'],
15+
'_aaaacccc',
16+
],
17+
[
18+
'should ensure the last atomic declaration of a single group with short class name wins',
19+
['_aaaabbbb', '_aaaacccc', '_aaaa_a'],
20+
'a',
21+
],
22+
[
23+
'should ensure the last atomic declaration of many single groups wins',
24+
['_aaaabbbb', '_aaaacccc', '_aaaadddd', '_aaaaeeee'],
25+
'_aaaaeeee',
26+
],
27+
[
28+
'should ensure the last atomic declaration of many single groups with short class name wins',
29+
['_aaaabbbb', '_aaaacccc', '_aaaa_a', '_aaaa_b'],
30+
'b',
31+
],
32+
[
33+
'should ensure the last atomic declaration of a multi group wins',
34+
['_aaaabbbb _aaaacccc'],
35+
'_aaaacccc',
36+
],
37+
[
38+
'should ensure the last atomic declaration of a multi group with short class name wins',
39+
['_aaaa_e', '_aaaabbbb _aaaacccc'],
40+
'_aaaacccc',
41+
],
42+
[
43+
'should ensure the last atomic declaration of many multi groups wins',
44+
['_aaaabbbb _aaaacccc _aaaadddd _aaaaeeee'],
45+
'_aaaaeeee',
46+
],
47+
[
48+
'should ensure the last atomic declaration of many multi groups with short class name wins',
49+
['_aaaabbbb', '_aaaa_a', '_bbbb_b', '_ddddcccc'],
50+
'a b _ddddcccc',
51+
],
52+
[
53+
'should not remove any atomic declarations if there are no duplicate groups',
54+
['_aaaabbbb', '_bbbbcccc'],
55+
'_aaaabbbb _bbbbcccc',
56+
],
57+
[
58+
'should not remove any atomic declarations if there are short class name and no duplicate groups',
59+
['_eeee_e', '_aaaabbbb', '_bbbbcccc'],
60+
'e _aaaabbbb _bbbbcccc',
61+
],
62+
['should not apply conditional class', [isEnabled && 'foo', 'bar'], 'bar'],
63+
[
64+
'should ignore non atomic declarations',
65+
['hello_there', 'hello_world'],
66+
'hello_there hello_world',
67+
],
68+
[
69+
'should ignore non atomic declarations when atomic declarations exist',
70+
['hello_there', 'hello_world', '_aaaabbbb'],
71+
'hello_there hello_world _aaaabbbb',
72+
],
73+
[
74+
'should ignore non atomic declarations when atomic declarations with short class name exist',
75+
['hello_there', 'hello_world', '_aaaa_a'],
76+
'hello_there hello_world a',
77+
],
78+
])('%s', (_, params, result) => {
79+
expect(result).toEqual(ax(params));
6980
});
7081
});

‎packages/react/src/runtime/ax.ts

+7-9
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,8 @@ const ATOMIC_GROUP_LENGTH = 5;
2828
* @param classes
2929
*/
3030
export default function ax(classNames: (string | undefined | 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;
34-
}
31+
// short circuit if there's no class names.
32+
if (classNames.length <= 1 && !classNames[0]) return undefined;
3533

3634
const atomicGroups: Record<string, string> = {};
3735

@@ -45,11 +43,11 @@ export default function ax(classNames: (string | undefined | false)[]): string |
4543

4644
for (let x = 0; x < groups.length; x++) {
4745
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;
46+
const isAtomic = atomic.charCodeAt(0) === UNDERSCORE_UNICODE;
47+
const isCompressed = isAtomic && atomic.charCodeAt(5) === UNDERSCORE_UNICODE;
48+
49+
const atomicGroupName = isAtomic ? atomic.slice(0, ATOMIC_GROUP_LENGTH) : atomic;
50+
atomicGroups[atomicGroupName] = isCompressed ? atomic.slice(ATOMIC_GROUP_LENGTH + 1) : atomic;
5351
}
5452
}
5553

‎packages/webpack-loader/src/compiled-loader.ts

+7
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ function getLoaderOptions(context: LoaderContext<CompiledLoaderOptions>) {
3232
ssr = false,
3333
optimizeCss = true,
3434
addComponentName = false,
35+
classNameCompressionMap = undefined,
3536
}: CompiledLoaderOptions = typeof context.getOptions === 'undefined'
3637
? // Webpack v4 flow
3738
getOptions(context)
@@ -75,6 +76,9 @@ function getLoaderOptions(context: LoaderContext<CompiledLoaderOptions>) {
7576
addComponentName: {
7677
type: 'boolean',
7778
},
79+
classNameCompressionMap: {
80+
type: 'object',
81+
},
7882
},
7983
});
8084

@@ -91,6 +95,7 @@ function getLoaderOptions(context: LoaderContext<CompiledLoaderOptions>) {
9195
ssr,
9296
optimizeCss,
9397
addComponentName,
98+
classNameCompressionMap,
9499
};
95100
}
96101

@@ -163,6 +168,8 @@ export default async function compiledLoader(
163168
'@compiled/babel-plugin',
164169
{
165170
...options,
171+
// Turn off compressing class names if stylesheet extraction is off
172+
classNameCompressionMap: options.extract && options.classNameCompressionMap,
166173
onIncludedFiles: (files: string[]) => includedFiles.push(...files),
167174
resolver: {
168175
// The resolver needs to be synchronous, as babel plugins must be synchronous

‎packages/webpack-loader/src/types.ts

+8
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,14 @@ export interface CompiledLoaderOptions {
7878
* Default to `false`
7979
*/
8080
addComponentName?: boolean;
81+
82+
/**
83+
* A map holds the key-value pairs between full Atomic class names and the compressed ones
84+
* i.e. { '_aaaabbbb': 'a' }
85+
*
86+
* Default to `undefined`
87+
*/
88+
classNameCompressionMap?: object;
8189
}
8290

8391
export interface CompiledExtractPluginOptions {

0 commit comments

Comments
 (0)
Please sign in to comment.