diff --git a/.changeset/grumpy-cows-look.md b/.changeset/grumpy-cows-look.md
new file mode 100644
index 000000000..c4bb0580e
--- /dev/null
+++ b/.changeset/grumpy-cows-look.md
@@ -0,0 +1,10 @@
+---
+'@compiled/parcel-transformer': minor
+'@compiled/webpack-loader': minor
+'@compiled/babel-plugin': minor
+'@compiled/react': minor
+'@compiled/css': minor
+---
+
+Add an option to compress class names based on "classNameCompressionMap", which is provided by library consumers.
+Add a script to generate compressed class names.
diff --git a/.eslintrc.js b/.eslintrc.js
index 2f84ef972..f5e73914f 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -2,6 +2,7 @@ module.exports = {
root: true,
ignorePatterns: [
'dist',
+ 'build',
'flow-typed',
'*.d.ts',
'babel-cjs.js',
diff --git a/examples/parcel/.compiledcssrc b/examples/parcel/.compiledcssrc
deleted file mode 100644
index d49ad0af9..000000000
--- a/examples/parcel/.compiledcssrc
+++ /dev/null
@@ -1,24 +0,0 @@
-{
- "importReact": false,
- "extensions": [
- ".js",
- ".jsx",
- ".ts",
- ".tsx",
- ".customjsx"
- ],
- "parserBabelPlugins": [
- "typescript",
- "jsx"
- ],
- "transformerBabelPlugins": [
- [
- "@babel/plugin-proposal-decorators",
- {
- "legacy": true
- }
- ]
- ],
- "extract": true,
- "optimizeCss": false
-}
diff --git a/examples/parcel/class-name-compression-map.json b/examples/parcel/class-name-compression-map.json
new file mode 100644
index 000000000..540145edd
--- /dev/null
+++ b/examples/parcel/class-name-compression-map.json
@@ -0,0 +1,18 @@
+{
+ "1wyb12am": "a",
+ "syaz5scu": "b",
+ "syazruxl": "c",
+ "k48pbfng": "d",
+ "30l35scu": "e",
+ "f8pj13q2": "f",
+ "1e0c1o8l": "g",
+ "ca0qftgi": "h",
+ "u5f3ftgi": "i",
+ "n3tdftgi": "j",
+ "19bvftgi": "k",
+ "19itak0l": "l",
+ "2rko1l7b": "m",
+ "syaz1aj3": "n",
+ "1p1dangw": "o",
+ "bfhkbf54": "p"
+}
diff --git a/examples/parcel/compiledcss.js b/examples/parcel/compiledcss.js
new file mode 100644
index 000000000..d4a9209a1
--- /dev/null
+++ b/examples/parcel/compiledcss.js
@@ -0,0 +1,18 @@
+const classNameCompressionMap = require('./class-name-compression-map.json');
+
+module.exports = {
+ importReact: false,
+ extensions: ['.js', '.jsx', '.ts', '.tsx', '.customjsx'],
+ parserBabelPlugins: ['typescript', 'jsx'],
+ transformerBabelPlugins: [
+ [
+ '@babel/plugin-proposal-decorators',
+ {
+ legacy: true,
+ },
+ ],
+ ],
+ extract: true,
+ optimizeCss: false,
+ classNameCompressionMap: classNameCompressionMap,
+};
diff --git a/examples/webpack/class-name-compression-map.json b/examples/webpack/class-name-compression-map.json
new file mode 100644
index 000000000..4cda334b2
--- /dev/null
+++ b/examples/webpack/class-name-compression-map.json
@@ -0,0 +1,27 @@
+{
+ "1wyb12am": "a",
+ "syaz32ev": "b",
+ "k48pbfng": "c",
+ "30l35scu": "d",
+ "f8pj13q2": "e",
+ "19itptrx": "f",
+ "1kt92a4o": "g",
+ "171dak0l": "h",
+ "1swkri7e": "i",
+ "1tjq14ap": "j",
+ "yzbc5scu": "k",
+ "19pk1ul9": "l",
+ "syaz13q2": "m",
+ "1wyb1ul9": "n",
+ "19itlf8h": "o",
+ "ca0q1vi7": "p",
+ "u5f31vi7": "q",
+ "n3td1vi7": "r",
+ "19bv1vi7": "s",
+ "k48p1fw0": "t",
+ "syaz1cnh": "u",
+ "19it1srw": "v",
+ "bfhk1j28": "w",
+ "syazruxl": "x",
+ "bfhkbf54": "y"
+}
diff --git a/examples/webpack/webpack.config.js b/examples/webpack/webpack.config.js
index 1b01af03f..9e8edef48 100644
--- a/examples/webpack/webpack.config.js
+++ b/examples/webpack/webpack.config.js
@@ -7,6 +7,8 @@ const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const webpack = require('webpack');
+const classNameCompressionMap = require('./class-name-compression-map.json');
+
const extractCSS = process.env.EXTRACT_TO_CSS === 'true';
module.exports = {
@@ -40,6 +42,7 @@ module.exports = {
parserBabelPlugins: ['typescript', 'jsx'],
transformerBabelPlugins: [['@babel/plugin-proposal-decorators', { legacy: true }]],
optimizeCss: false,
+ classNameCompressionMap,
},
},
],
diff --git a/packages/babel-plugin/src/__tests__/index.test.ts b/packages/babel-plugin/src/__tests__/index.test.ts
index 9a594baa1..e0878dbf5 100644
--- a/packages/babel-plugin/src/__tests__/index.test.ts
+++ b/packages/babel-plugin/src/__tests__/index.test.ts
@@ -201,4 +201,160 @@ describe('babel plugin', () => {
expect(actual).toInclude('c_MyDiv');
});
+
+ it('should compress class name for styled component', () => {
+ const actual = transform(
+ `
+ import { styled } from '@compiled/react';
+
+ const MyDiv = styled.div\`
+ font-size: 12px;
+ \`;
+ `,
+ {
+ classNameCompressionMap: {
+ '1wyb1fwx': 'a',
+ },
+ }
+ );
+
+ expect(actual).toIncludeMultiple(['.a{font-size:12px}', 'ax(["_1wyb_a", __cmplp.className])']);
+ });
+
+ it('should compress class name for css props', () => {
+ const actual = transform(
+ `
+ import '@compiled/react';
+
+
+ `,
+ {
+ classNameCompressionMap: {
+ '1wyb1fwx': 'a',
+ },
+ }
+ );
+
+ expect(actual).toIncludeMultiple(['.a{font-size:12px}', 'ax(["_1wyb_a"])']);
+ });
+
+ it('should compress class name for ClassNames', () => {
+ const actual = transform(
+ `
+ import { ClassNames } from '@compiled/react';
+
+
+ {({ css }) => (
+
+ )}
+
+ `,
+ {
+ classNameCompressionMap: {
+ '1wyb1fwx': 'a',
+ },
+ }
+ );
+
+ expect(actual).toIncludeMultiple(['.a{font-size:12px}', 'className={ax(["_1wyb_a"])']);
+ });
+
+ it('should compress class names with atrules', () => {
+ const actual = transform(
+ `
+ import '@compiled/react';
+
+ `,
+ {
+ classNameCompressionMap: {
+ pz521fwx: 'a',
+ },
+ }
+ );
+
+ expect(actual).toIncludeMultiple([
+ '@media (max-width:1250px){.a{font-size:12px}}',
+ 'ax(["_pz52_a"])',
+ ]);
+ });
+
+ it('should compress pseudo classes', () => {
+ const actual = transform(
+ `
+ import '@compiled/react';
+
+ `,
+ {
+ classNameCompressionMap: {
+ '9h8h5scu': 'a',
+ e9151fwx: 'b',
+ },
+ }
+ );
+
+ expect(actual).toIncludeMultiple([
+ '.a:active{color:red}',
+ '.b:hover{font-size:12px}',
+ 'ax(["_e915_b _9h8h_a"])',
+ ]);
+ });
+
+ it('should compress nested selector', () => {
+ const actual = transform(
+ `
+ import '@compiled/react';
+ div': { 'div div:hover': { fontSize: 12 } } }} />
+ `,
+ {
+ classNameCompressionMap: {
+ '1jkf1fwx': 'a',
+ },
+ }
+ );
+
+ expect(actual).toIncludeMultiple(['.a >div div div:hover{font-size:12px}', 'ax(["_1jkf_a"]']);
+ });
+
+ it('should compress conditional class names', () => {
+ const actual = transform(
+ `
+ import '@compiled/react';
+
bar ? 14 : 16 }, () => foo ? { fontSize: 12 } : {}, baz && { fontSize: 20 }]} />
+ `,
+ {
+ classNameCompressionMap: {
+ '1wyb19ub': 'a',
+ '1wyb1fwx': 'b',
+ },
+ }
+ );
+
+ expect(actual).toIncludeMultiple([
+ '.a{font-size:16}',
+ '.b{font-size:12px}',
+ 'bar ? "_1wyb1o8a" : "_1wyb_a"',
+ 'foo && "_1wyb_b"',
+ ]);
+ });
+
+ it('should compress class names according to the map', () => {
+ const actual = transform(
+ `
+ import '@compiled/react';
+
+ `,
+ {
+ classNameCompressionMap: {
+ syaz5scu: 'a',
+ },
+ }
+ );
+
+ expect(actual).toIncludeMultiple([
+ '._19pk19bv{margin-top:10px}',
+ '.a{color:red}',
+ '._1wyb1fwx{font-size:12px}',
+ 'ax(["_1wyb1fwx _syaz_a _19pk19bv"]',
+ ]);
+ });
});
diff --git a/packages/babel-plugin/src/__tests__/jsx-automatic.test.ts b/packages/babel-plugin/src/__tests__/jsx-automatic.test.ts
index 3fc90bb26..e594c86d5 100644
--- a/packages/babel-plugin/src/__tests__/jsx-automatic.test.ts
+++ b/packages/babel-plugin/src/__tests__/jsx-automatic.test.ts
@@ -49,7 +49,7 @@ describe('jsx automatic', () => {
children: [_],
}),
_jsx("div", {
- className: "_syaz13q2",
+ className: ax(["_syaz13q2"]),
}),
],
});
diff --git a/packages/babel-plugin/src/class-names/__tests__/behaviour.test.ts b/packages/babel-plugin/src/class-names/__tests__/behaviour.test.ts
index 74469aa9d..27554cb07 100644
--- a/packages/babel-plugin/src/class-names/__tests__/behaviour.test.ts
+++ b/packages/babel-plugin/src/class-names/__tests__/behaviour.test.ts
@@ -29,7 +29,7 @@ describe('class names behaviour', () => {
const ListItem = () => (
{[_]}
- {hello, world!
}
+ {hello, world!
}
);
"
@@ -55,7 +55,7 @@ describe('class names behaviour', () => {
{[_]}
{(() => {
- return hello, world!
;
+ return hello, world!
;
})()}
);
@@ -101,7 +101,7 @@ describe('class names behaviour', () => {
const ListItem = () => (
{[_]}
- {hello, world!
}
+ {hello, world!
}
);
"
@@ -165,14 +165,18 @@ describe('class names behaviour', () => {
{[_, _2, _3, _4, _5]}
{
<>
-
+
longhand object call expression
-
shorthand object call expression
-
+
+ shorthand object call expression
+
+
longhand tagged template expression
-
shorthand tagged template expression
+
+ shorthand tagged template expression
+
>
}
@@ -241,7 +245,7 @@ describe('class names behaviour', () => {
const ListItem = () => (
{[_]}
- {hello, world!
}
+ {hello, world!
}
);
"
@@ -275,8 +279,8 @@ describe('class names behaviour', () => {
{
hello, world!
@@ -304,7 +308,7 @@ describe('class names behaviour', () => {
const ListItem = () => (
{[_]}
- {hello, world!
}
+ {hello, world!
}
);
"
@@ -343,7 +347,7 @@ describe('class names behaviour', () => {
const ListItem = ({ children }) => (
{[_]}
- {children("_1wybgktf")}
+ {children(ax(["_1wybgktf"]))}
);
"
@@ -361,7 +365,7 @@ describe('class names behaviour', () => {
);
`);
- expect(actual).toInclude(`
{
@@ -399,7 +403,7 @@ describe('class names behaviour', () => {
style={{
"--_1ylxx6h": ix(color),
}}
- className={"_syaz1aj3"}
+ className={ax(["_syaz1aj3"])}
/>
}
@@ -466,7 +470,7 @@ describe('class names behaviour', () => {
{(() => {
const { css: c, style: styl } = arg;
return (
-
+
hello world
);
@@ -476,4 +480,71 @@ describe('class names behaviour', () => {
"
`);
});
+
+ it('should apply conditional logical expression object spread styles', () => {
+ const actual = transform(`
+ import { ClassNames } from '@compiled/react';
+
+ const ListItem = (props) => (
+
+ {({ css }) => (hello, world!
)}
+
+ );
+ `);
+
+ expect(actual).toInclude('className={ax([props.isPrimary && "_syaz13q2 _1wybgktf"])}');
+ });
+
+ it('should apply array logical-based conditional css', () => {
+ const actual = transform(
+ `
+ import { ClassNames } from '@compiled/react';
+
+ const ListItem = (props) => (
+
+ {({ css }) => (hello, world!
)}
+
+ );
+ `,
+ { pretty: false }
+ );
+
+ expect(actual).toInclude(
+ 'className={ax(["_1wyb1ylp",(props.isPrimary||props.isMaybe)&&"_syaz13q2 _1wybgktf"])}'
+ );
+ });
+
+ it('should apply array prop ternary-based inline conditional css', () => {
+ const actual = transform(
+ `
+ import { ClassNames } from '@compiled/react';
+
+ const ListItem = (props) => (
+
+ {({ css }) => (hello, world!
)}
+
+ );
+ `,
+ { pretty: false }
+ );
+
+ expect(actual).toInclude(
+ 'className={ax([props.isPrimary?"_bfhk1x77 _syaz11x8":"_bfhkbf54 _syaz5scu","_1wyb1fwx"])}'
+ );
+ });
});
diff --git a/packages/babel-plugin/src/class-names/__tests__/tagged-template-expression.test.ts b/packages/babel-plugin/src/class-names/__tests__/tagged-template-expression.test.ts
index 95b7d5fa4..525a5936f 100644
--- a/packages/babel-plugin/src/class-names/__tests__/tagged-template-expression.test.ts
+++ b/packages/babel-plugin/src/class-names/__tests__/tagged-template-expression.test.ts
@@ -15,7 +15,7 @@ describe('ClassNames used with a css tagged template expression', () => {
expect(actual).toIncludeMultiple([
'const _ = "._1wybgktf{font-size:20px}"',
- 'className={"_1wybgktf"}',
+ 'className={ax(["_1wybgktf"])}',
]);
});
diff --git a/packages/babel-plugin/src/class-names/index.ts b/packages/babel-plugin/src/class-names/index.ts
index 1b989841f..74d4b6d59 100644
--- a/packages/babel-plugin/src/class-names/index.ts
+++ b/packages/babel-plugin/src/class-names/index.ts
@@ -1,13 +1,13 @@
import type { NodePath } from '@babel/core';
import * as t from '@babel/types';
-import { transformCss } from '@compiled/css';
import type { Metadata } from '../types';
import { buildCodeFrameError, pickFunctionBody } from '../utils/ast';
import { compiledTemplate } from '../utils/build-compiled-component';
import { buildCssVariables } from '../utils/build-css-variables';
-import { buildCss, getItemCss } from '../utils/css-builders';
+import { buildCss } from '../utils/css-builders';
import { resolveIdentifierComingFromDestructuring } from '../utils/resolve-binding';
+import { transformCssItems } from '../utils/transform-css-items';
import type { CSSOutput } from '../utils/types';
/**
@@ -132,15 +132,12 @@ export const visitClassNamesPath = (path: NodePath
, meta: Metadata
}
const builtCss = buildCss(styles, meta);
- const { sheets, classNames } = transformCss(
- builtCss.css.map((x) => getItemCss(x)).join(''),
- meta.state.opts
- );
+ const { sheets, classNames } = transformCssItems(builtCss.css, meta);
collectedVariables.push(...builtCss.variables);
collectedSheets.push(...sheets);
- path.replaceWith(t.stringLiteral(classNames.join(' ')));
+ path.replaceWith(t.callExpression(t.identifier('ax'), [t.arrayExpression(classNames)]));
},
});
diff --git a/packages/babel-plugin/src/types.ts b/packages/babel-plugin/src/types.ts
index 7216f9d40..fc72ed4a8 100644
--- a/packages/babel-plugin/src/types.ts
+++ b/packages/babel-plugin/src/types.ts
@@ -67,6 +67,14 @@ export interface PluginOptions {
* Default to `false`
*/
addComponentName?: boolean;
+
+ /**
+ * A map holds the key-value pairs between full Atomic class names and the compressed ones
+ * i.e. { '_aaaabbbb': 'a' }
+ *
+ * Default to `undefined`
+ */
+ classNameCompressionMap?: { [index: string]: string };
}
export interface State extends PluginPass {
diff --git a/packages/babel-plugin/src/utils/build-styled-component.ts b/packages/babel-plugin/src/utils/build-styled-component.ts
index 3faa4a8af..a0f38e2ce 100644
--- a/packages/babel-plugin/src/utils/build-styled-component.ts
+++ b/packages/babel-plugin/src/utils/build-styled-component.ts
@@ -16,6 +16,7 @@ import type { Metadata, Tag } from '../types';
import { pickFunctionBody } from './ast';
import { buildCssVariables } from './build-css-variables';
+import { compressClassNamesForAx } from './compress-class-names-for-ax';
import { getItemCss } from './css-builders';
import { hoistSheet } from './hoist-sheet';
import { applySelectors, transformCssItems } from './transform-css-items';
@@ -237,7 +238,14 @@ export const buildStyledComponent = (tag: Tag, cssOutput: CSSOutput, meta: Metad
const sheets = [...uniqueUnconditionalCssOutput.sheets, ...conditionalCssOutput.sheets];
const classNames = [
- ...[t.stringLiteral(uniqueUnconditionalCssOutput.classNames.join(' '))],
+ ...[
+ t.stringLiteral(
+ compressClassNamesForAx(
+ uniqueUnconditionalCssOutput.classNames,
+ meta.state.opts.classNameCompressionMap
+ ).join(' ')
+ ),
+ ],
...conditionalCssOutput.classNames,
];
diff --git a/packages/babel-plugin/src/utils/compress-class-names-for-ax.ts b/packages/babel-plugin/src/utils/compress-class-names-for-ax.ts
new file mode 100644
index 000000000..c069374c0
--- /dev/null
+++ b/packages/babel-plugin/src/utils/compress-class-names-for-ax.ts
@@ -0,0 +1,16 @@
+/**
+ * Compress class names based on `classNameCompressionMap`.
+ * The compressed class name has a format of `_aaaa_a`, which is expected by `ax`.
+ * `aaaa` is the atomic group and `a` is the compressed name.
+ */
+export const compressClassNamesForAx = (
+ classNames: string[],
+ classNameCompressionMap?: { [index: string]: string }
+): string[] => {
+ if (!classNameCompressionMap) return classNames;
+ return classNames.map((className) => {
+ const compressedClassName =
+ classNameCompressionMap && classNameCompressionMap[className.slice(1)];
+ return compressedClassName ? `_${className.slice(1, 5)}_${compressedClassName}` : className;
+ });
+};
diff --git a/packages/babel-plugin/src/utils/transform-css-items.ts b/packages/babel-plugin/src/utils/transform-css-items.ts
index 2f0bce1ae..0134cc850 100644
--- a/packages/babel-plugin/src/utils/transform-css-items.ts
+++ b/packages/babel-plugin/src/utils/transform-css-items.ts
@@ -3,6 +3,7 @@ import { transformCss } from '@compiled/css';
import type { Metadata } from '../types';
+import { compressClassNamesForAx } from './compress-class-names-for-ax';
import { getItemCss } from './css-builders';
import type { CssItem } from './types';
@@ -64,13 +65,21 @@ const transformCssItem = (
classExpression: t.logicalExpression(
item.operator,
item.expression,
- t.stringLiteral(logicalCss.classNames.join(' '))
+ t.stringLiteral(
+ compressClassNamesForAx(
+ logicalCss.classNames,
+ meta.state.opts.classNameCompressionMap
+ ).join(' ')
+ )
),
};
default:
const css = transformCss(getItemCss(item), meta.state.opts);
- const className = css.classNames.join(' ');
+ const className = compressClassNamesForAx(
+ css.classNames,
+ meta.state.opts.classNameCompressionMap
+ ).join(' ');
return {
sheets: css.sheets,
diff --git a/packages/css/src/__tests__/generate-compression-map.test.ts b/packages/css/src/__tests__/generate-compression-map.test.ts
new file mode 100644
index 000000000..b03736db3
--- /dev/null
+++ b/packages/css/src/__tests__/generate-compression-map.test.ts
@@ -0,0 +1,224 @@
+import { generateCompressionMap as generate } from '../generate-compression-map';
+
+describe('generate compression map', () => {
+ const baseCSS = `
+ ._154i14e6{top:33px}
+ ._14tk72c6>div:not([role=group])>a{padding-left:18.6px}
+ ._14n4stnw._14n4stnw{position:absolute}
+ ._13h81y44 span[role=button]{padding-top:4px}
+ ._1di6k6hx:active, ._irr3k6hx:hover{background-color:var(--ds-background-neutral-subtle-hovered,#091e420a)}
+ ._1di6k6hx:active, ._jomrk6hx:focus, ._10j7k6hx:focus-within, ._irr3k6hx:hover{background-color:var(--ds-background-neutral-subtle-hovered,#091e420a)}
+ ._1gg2glyw>a:active, ._1o3iglyw>a[aria-current=page]{-webkit-text-decoration-line:none;text-decoration-line:none}
+ ._1iohnqa1:active, ._5goinqa1:focus, ._jf4cnqa1:hover{-webkit-text-decoration-style:solid;text-decoration-style:solid}
+ ._1iohnqa1:active, ._jf4cnqa1:hover, ._xatrnqa1:link, ._1726nqa1:visited{-webkit-text-decoration-style:solid;text-decoration-style:solid}
+ ._1iqunqa1._1iqunqa1:active, ._1ejunqa1._1ejunqa1:hover, ._1lwpnqa1._1lwpnqa1:visited{-webkit-text-decoration-style:solid;text-decoration-style:solid}
+ ._1iqunqa1._1iqunqa1:active, ._6xf7nqa1._6xf7nqa1:focus, ._1ejunqa1._1ejunqa1:hover, ._1lwpnqa1._1lwpnqa1:visited{-webkit-text-decoration-style:solid;text-decoration-style:solid}
+ ._1mb818uv>a:active, ._oga118uv>a[aria-current=page]{-webkit-text-decoration-color:initial;text-decoration-color:initial}
+ ._1n2onqa1>a:active, ._1k4fnqa1>a[aria-current=page]{-webkit-text-decoration-style:solid;text-decoration-style:solid}
+ ._1nrm18uv:active, ._1a3b18uv:focus, ._9oik18uv:hover{-webkit-text-decoration-color:initial;text-decoration-color:initial}
+ ._1nrm18uv:active, ._9oik18uv:hover, ._5bd618uv:link, ._1ydc18uv:visited{-webkit-text-decoration-color:initial;text-decoration-color:initial}
+ ._1ohyglyw:active, ._49pcglyw:focus, ._ra3xglyw:focus-visible, ._ksodglyw:hover{outline-style:none}
+ ._1ohyglyw:active, ._ksodglyw:hover, ._q4asglyw:link, ._tpgfglyw:visited{outline-style:none}
+ ._1oxgru3m:active{transition-duration:0s}
+ ._9h8h1e9r:active, ._f8pj1e9r:focus, ._30l31e9r:hover, ._10531e9r:visited{color:var(--ds-text-subtlest,#7a869a)}
+ @media screen and (min-width:1300px){._1jhpoyl8{max-width:10vw}}
+ @media (max-width:1199px){._11usglyw{display:none}}
+ @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}}
+ @media (min-width:1000px) and (max-width:1439px){._hnu8tcjq{display:block!important}}
+ @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}}
+ @media (min-width:1440px) and (max-width:1919px){._pbi4tcjq{display:block!important}}
+ @media (min-width:1920px) and (max-width:2559px){._16b9tcjq{display:block!important}}
+ @media (min-width:2560px) and (max-width:2999px){._jmaqtcjq{display:block!important}}
+ @media (min-width:3000px){._1q5htcjq{display:block!important}}
+ @media (min-width:800px) and (max-width:999px){._11x1tcjq{display:block!important}}
+ @media (min-width:800px){._121jagmp{display:none!important}}
+ @media (prefers-reduced-motion:reduce){._1bumglyw{animation:none}._sedtglyw{transition:none}}
+ @media screen and (-webkit-min-device-pixel-ratio:0){._14kw1hna >textarea{word-break:break-word}._mc2h1hna{word-break:break-word}}
+ @media screen and (-webkit-transition){._14fy1hna{word-break:break-word}._1vdp1hna >textarea{word-break:break-word}}
+ @media screen and (max-height:400px){._17gjpfqs{position:static}}
+ @media screen and (min-width:1300px){._1jhpoyl8{max-width:10vw}}
+`;
+
+ const baseResult = {
+ '154i14e6': 'a',
+ '14tk72c6': 'b',
+ '14n4stnw': 'c',
+ '13h81y44': 'd',
+ '1di6k6hx': 'e',
+ irr3k6hx: 'f',
+ jomrk6hx: 'g',
+ '10j7k6hx': 'h',
+ '1gg2glyw': 'i',
+ '1o3iglyw': 'j',
+ '1iohnqa1': 'k',
+ '5goinqa1': 'l',
+ jf4cnqa1: 'm',
+ xatrnqa1: 'n',
+ '1726nqa1': 'o',
+ '1iqunqa1': 'p',
+ '1ejunqa1': 'q',
+ '1lwpnqa1': 'r',
+ '6xf7nqa1': 's',
+ '1mb818uv': 't',
+ oga118uv: 'u',
+ '1n2onqa1': 'v',
+ '1k4fnqa1': 'w',
+ '1nrm18uv': 'x',
+ '1a3b18uv': 'y',
+ '9oik18uv': 'z',
+ '5bd618uv': 'A',
+ '1ydc18uv': 'B',
+ '1ohyglyw': 'C',
+ '49pcglyw': 'D',
+ ra3xglyw: 'E',
+ ksodglyw: 'F',
+ q4asglyw: 'G',
+ tpgfglyw: 'H',
+ '1oxgru3m': 'I',
+ '9h8h1e9r': 'J',
+ f8pj1e9r: 'K',
+ '30l31e9r': 'L',
+ '10531e9r': 'M',
+ '1jhpoyl8': 'N',
+ '11usglyw': 'O',
+ '11y7oza4': 'P',
+ '11y7uu9g': 'Q',
+ l82t7vkz: 'R',
+ j7o07vkz: 'S',
+ '1od57vkz': 'T',
+ l82tgktf: 'U',
+ j7o0gktf: 'V',
+ yksp1ssb: 'W',
+ '1b421ssb': 'X',
+ s8ks18ws: 'Y',
+ u1wz1nty: 'Z',
+ hnu8tcjq: '_',
+ jvpg11p5: 'aa',
+ '1nwdwxkt': 'ba',
+ '1vlxckbl': 'ca',
+ kz8c16xz: 'da',
+ '1jyu16xz': 'ea',
+ '11et16xz': 'fa',
+ fgkv16xz: 'ga',
+ szna1wug: 'ha',
+ '13on1wug': 'ia',
+ '1f3k1wug': 'ja',
+ inid1wug: 'ka',
+ '12wp9ac1': 'la',
+ jvpgglyw: 'ma',
+ pbi4tcjq: 'na',
+ '16b9tcjq': 'oa',
+ jmaqtcjq: 'pa',
+ '1q5htcjq': 'qa',
+ '11x1tcjq': 'ra',
+ '121jagmp': 'sa',
+ '1bumglyw': 'ta',
+ sedtglyw: 'ua',
+ '14kw1hna': 'va',
+ mc2h1hna: 'wa',
+ '14fy1hna': 'xa',
+ '1vdp1hna': 'ya',
+ '17gjpfqs': 'za',
+ };
+ it('should generate class names as expected', () => {
+ const result = generate(baseCSS);
+ expect(result).toStrictEqual(baseResult);
+ });
+
+ it('should generate class names with the old compression map', () => {
+ const oldCompressionMap: { [index: string]: string } = {
+ '17gjpfqs': 'a',
+ '1vdp1hna': 'b',
+ '14fy1hna': 'c',
+ };
+ const result = generate(baseCSS, { oldClassNameCompressionMap: oldCompressionMap });
+ for (const property in oldCompressionMap) {
+ expect(result).toHaveProperty(property, oldCompressionMap[property]);
+ }
+ });
+
+ it('should generate class names with prefix', () => {
+ const result = generate(baseCSS, { prefix: '_' });
+ expect(result).toStrictEqual({
+ '154i14e6': '_a',
+ '14tk72c6': '_b',
+ '14n4stnw': '_c',
+ '13h81y44': '_d',
+ '1di6k6hx': '_e',
+ irr3k6hx: '_f',
+ jomrk6hx: '_g',
+ '10j7k6hx': '_h',
+ '1gg2glyw': '_i',
+ '1o3iglyw': '_j',
+ '1iohnqa1': '_k',
+ '5goinqa1': '_l',
+ jf4cnqa1: '_m',
+ xatrnqa1: '_n',
+ '1726nqa1': '_o',
+ '1iqunqa1': '_p',
+ '1ejunqa1': '_q',
+ '1lwpnqa1': '_r',
+ '6xf7nqa1': '_s',
+ '1mb818uv': '_t',
+ oga118uv: '_u',
+ '1n2onqa1': '_v',
+ '1k4fnqa1': '_w',
+ '1nrm18uv': '_x',
+ '1a3b18uv': '_y',
+ '9oik18uv': '_z',
+ '5bd618uv': '_A',
+ '1ydc18uv': '_B',
+ '1ohyglyw': '_C',
+ '49pcglyw': '_D',
+ ra3xglyw: '_E',
+ ksodglyw: '_F',
+ q4asglyw: '_G',
+ tpgfglyw: '_H',
+ '1oxgru3m': '_I',
+ '9h8h1e9r': '_J',
+ f8pj1e9r: '_K',
+ '30l31e9r': '_L',
+ '10531e9r': '_M',
+ '1jhpoyl8': '_N',
+ '11usglyw': '_O',
+ '11y7oza4': '_P',
+ '11y7uu9g': '_Q',
+ l82t7vkz: '_R',
+ j7o07vkz: '_S',
+ '1od57vkz': '_T',
+ l82tgktf: '_U',
+ j7o0gktf: '_V',
+ yksp1ssb: '_W',
+ '1b421ssb': '_X',
+ s8ks18ws: '_Y',
+ u1wz1nty: '_Z',
+ hnu8tcjq: '__',
+ jvpg11p5: '_-',
+ '1nwdwxkt': '_0',
+ '1vlxckbl': '_1',
+ kz8c16xz: '_2',
+ '1jyu16xz': '_3',
+ '11et16xz': '_4',
+ fgkv16xz: '_5',
+ szna1wug: '_6',
+ '13on1wug': '_7',
+ '1f3k1wug': '_8',
+ inid1wug: '_9',
+ '12wp9ac1': '_aa',
+ jvpgglyw: '_ba',
+ pbi4tcjq: '_ca',
+ '16b9tcjq': '_da',
+ jmaqtcjq: '_ea',
+ '1q5htcjq': '_fa',
+ '11x1tcjq': '_ga',
+ '121jagmp': '_ha',
+ '1bumglyw': '_ia',
+ sedtglyw: '_ja',
+ '14kw1hna': '_ka',
+ mc2h1hna: '_la',
+ '14fy1hna': '_ma',
+ '1vdp1hna': '_na',
+ '17gjpfqs': '_oa',
+ });
+ });
+});
diff --git a/packages/css/src/generate-compression-map.ts b/packages/css/src/generate-compression-map.ts
new file mode 100644
index 000000000..4b56e20d4
--- /dev/null
+++ b/packages/css/src/generate-compression-map.ts
@@ -0,0 +1,70 @@
+import postcss from 'postcss';
+import selectorParser from 'postcss-selector-parser';
+
+import { ClassNameGenerator } from './utils/class-name-generator';
+
+const UNDERSCORE_UNICODE = 95;
+
+/**
+ * Generate a compression map, which is used by @compiled/babel-plugin to compress class names.
+ * The compression map looks like { 'aaaabbbb': 'a', 'bbbbcccc': 'b' }
+ *
+ * @param stylesheet css content i.e. `.aaaabbbb{font-size: 10px}`
+ * @param oldClassNameCompressionMap the previous compression map, which ensures the compression is deterministic.
+ * @returns newClassNameCompressionMap
+ */
+export const generateCompressionMap = (
+ css: string,
+ opts?: { oldClassNameCompressionMap?: { [index: string]: string }; prefix?: string }
+): undefined | { [index: string]: string } => {
+ const { oldClassNameCompressionMap, prefix } = opts || {};
+
+ let classNamesToCompress: string[] = [];
+ const classNameCompressionMap: { [index: string]: string } = {};
+ const reservedClassNames: string[] = [];
+
+ const selectorProcessor = selectorParser((selectors) => {
+ selectors.walkClasses((node: selectorParser.ClassName | selectorParser.Identifier) => {
+ // Only compress Atomic class names, which has the format of `_aaaabbbb`.
+ if (node.value.charCodeAt(0) === UNDERSCORE_UNICODE && node.value.length === 9) {
+ classNamesToCompress.push(node.value.slice(1));
+ }
+ });
+ });
+
+ const result = postcss([
+ {
+ postcssPlugin: 'postcss-find-atomic-class-names',
+ Rule(ruleNode) {
+ selectorProcessor.process(ruleNode);
+ },
+ },
+ ]).process(css, { from: undefined });
+
+ // We need to access something to make the transformation happen.
+ result.css;
+
+ // Remove duplicates
+ classNamesToCompress = Array.from(new Set(classNamesToCompress));
+
+ // Check if class name to compress already exists in oldClassNameCompressionMap
+ // If yes, re-use the compressed class name
+ if (oldClassNameCompressionMap) {
+ classNamesToCompress = classNamesToCompress.filter((className) => {
+ if (oldClassNameCompressionMap[className]) {
+ reservedClassNames.push(oldClassNameCompressionMap[className]);
+ classNameCompressionMap[className] = oldClassNameCompressionMap[className];
+ return false;
+ }
+ return true;
+ });
+ }
+
+ const classNameGenerator = new ClassNameGenerator({ reservedClassNames, prefix });
+ classNamesToCompress.forEach((className) => {
+ const newClassName = classNameGenerator.generateClassName();
+ classNameCompressionMap[className] = newClassName;
+ });
+
+ return classNameCompressionMap;
+};
diff --git a/packages/css/src/index.ts b/packages/css/src/index.ts
index 7ffc2a5f2..69e3fcc41 100644
--- a/packages/css/src/index.ts
+++ b/packages/css/src/index.ts
@@ -6,3 +6,4 @@ export {
BeforeInterpolation,
} from './utils/css-affix-interpolation';
export { sort } from './sort';
+export { generateCompressionMap } from './generate-compression-map';
diff --git a/packages/css/src/plugins/atomicify-rules.ts b/packages/css/src/plugins/atomicify-rules.ts
index cc4521367..9f3c423d7 100644
--- a/packages/css/src/plugins/atomicify-rules.ts
+++ b/packages/css/src/plugins/atomicify-rules.ts
@@ -3,6 +3,7 @@ import type { Plugin, ChildNode, Declaration, Container, Rule, AtRule } from 'po
import { rule } from 'postcss';
interface PluginOpts {
+ classNameCompressionMap?: { [index: string]: string };
callback?: (className: string) => void;
}
@@ -76,20 +77,28 @@ const replaceNestingSelector = (selector: string, parentClassName: string) => {
* @param node
*/
const buildAtomicSelector = (node: Declaration, opts: AtomicifyOpts) => {
+ const { classNameCompressionMap } = opts;
const selectors: string[] = [];
(opts.selectors || ['']).forEach((selector) => {
const normalizedSelector = normalizeSelector(selector);
- const className = atomicClassName(node, {
+ const fullClassName = atomicClassName(node, {
...opts,
selectors: [normalizedSelector],
});
- const replacedSelector = replaceNestingSelector(normalizedSelector, className);
- selectors.push(replacedSelector);
+ const compressedClassName =
+ classNameCompressionMap && classNameCompressionMap[fullClassName.slice(1)];
+
+ if (compressedClassName) {
+ // Use compressed class name if compressedClassName is available
+ selectors.push(replaceNestingSelector(normalizedSelector, compressedClassName));
+ } else {
+ selectors.push(replaceNestingSelector(normalizedSelector, fullClassName));
+ }
if (opts.callback) {
- opts.callback(className);
+ opts.callback(fullClassName);
}
});
diff --git a/packages/css/src/transform.ts b/packages/css/src/transform.ts
index 0502878d8..462245d4f 100644
--- a/packages/css/src/transform.ts
+++ b/packages/css/src/transform.ts
@@ -15,6 +15,7 @@ import { sortAtRulePseudos } from './plugins/sort-at-rule-pseudos';
interface TransformOpts {
optimizeCss?: boolean;
+ classNameCompressionMap?: object;
}
/**
@@ -38,7 +39,10 @@ export const transformCss = (
nested(),
...normalizeCSS(opts),
expandShorthands(),
- atomicifyRules({ callback: (className: string) => classNames.push(className) }),
+ atomicifyRules({
+ classNameCompressionMap: opts.classNameCompressionMap,
+ callback: (className: string) => classNames.push(className),
+ }),
sortAtRulePseudos(),
...(process.env.AUTOPREFIXER === 'off' ? [] : [autoprefixer()]),
whitespace(),
diff --git a/packages/css/src/utils/__tests__/class-name-generator.test.ts b/packages/css/src/utils/__tests__/class-name-generator.test.ts
new file mode 100644
index 000000000..ddd729ef9
--- /dev/null
+++ b/packages/css/src/utils/__tests__/class-name-generator.test.ts
@@ -0,0 +1,47 @@
+import { ClassNameGenerator } from '../class-name-generator';
+
+describe('ClassNameGenerator', () => {
+ it('should generate class names with minimal length', () => {
+ const generator = new ClassNameGenerator();
+ Array.from(Array(27).keys()).forEach(() => {
+ const className = generator.generateClassName();
+ expect(className.length).toBe(1);
+ });
+ });
+
+ it('should skip reservedClassNames', () => {
+ const generator = new ClassNameGenerator({ reservedClassNames: ['a', 'b', 'c'] });
+ const className = generator.generateClassName();
+ expect(className).toBe('d');
+ });
+
+ it('should not generate class names starting with a number if prefix is not given', () => {
+ const generator = new ClassNameGenerator();
+ Array.from(Array(30).keys()).forEach(() => {
+ const className = generator.generateClassName();
+ expect(className.charAt(0)).toMatch(/[^1-9]/);
+ });
+ });
+
+ it('should prefix class names', () => {
+ const prefix = '_';
+ const generator = new ClassNameGenerator({ prefix });
+ expect(generator.generateClassName().startsWith(prefix)).toBeTrue();
+ });
+
+ it('should throw an error if invalid prefix is given', () => {
+ expect(() => {
+ new ClassNameGenerator({ prefix: '-' });
+ }).toThrowErrorMatchingInlineSnapshot(
+ `"'-' is an invalid prefix. The allowed prefix is [a-zA-Z_]"`
+ );
+ });
+
+ it('should not generate class name which includes the word "ad"', () => {
+ const generator = new ClassNameGenerator({ prefix: 'a' });
+ Array.from(Array(10).keys()).forEach(() => {
+ const className = generator.generateClassName();
+ expect(className.toLocaleLowerCase().includes('ad')).toBeFalse();
+ });
+ });
+});
diff --git a/packages/css/src/utils/class-name-generator.ts b/packages/css/src/utils/class-name-generator.ts
new file mode 100644
index 000000000..1dcf577a3
--- /dev/null
+++ b/packages/css/src/utils/class-name-generator.ts
@@ -0,0 +1,58 @@
+// CSS classes are case sensitive in non-quirk mode
+// Spec: https://html.spec.whatwg.org/multipage/semantics-other.html#case-sensitivity-of-selectors
+// 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.
+// Spec: https://www.w3.org/TR/CSS21/syndata.html#characters
+const acceptPrefixBase = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_';
+const acceptPrefix = acceptPrefixBase.split('');
+const acceptChars = `${acceptPrefixBase}-0123456789`.split('');
+
+export class ClassNameGenerator {
+ newClassSize: number;
+ reservedClassNames: string[];
+ prefix?: string;
+ constructor(opts: { reservedClassNames?: string[]; prefix?: string } = {}) {
+ this.newClassSize = 0;
+ this.reservedClassNames = opts.reservedClassNames || [];
+ this.prefix = opts.prefix;
+
+ if (this.prefix && !acceptPrefix.includes(this.prefix)) {
+ throw new Error(`'${this.prefix}' is an invalid prefix. The allowed prefix is [a-zA-Z_]`);
+ }
+ }
+ generateClassName(): string {
+ const chars = [];
+ let rest = this.prefix
+ ? this.newClassSize + 1
+ : (this.newClassSize - (this.newClassSize % acceptPrefix.length)) / acceptPrefix.length;
+ if (rest > 0) {
+ while (true) {
+ rest -= 1;
+ const m = rest % acceptChars.length;
+ const c = acceptChars[m];
+ chars.push(c);
+ rest -= m;
+ if (rest === 0) {
+ break;
+ }
+ rest /= acceptChars.length;
+ }
+ }
+ const newClassName = `${
+ this.prefix ? this.prefix : acceptPrefix[this.newClassSize % acceptPrefix.length]
+ }${chars.join('')}`;
+
+ if (this.reservedClassNames && this.reservedClassNames.includes(newClassName)) {
+ this.newClassSize++;
+ return this.generateClassName();
+ }
+
+ // Avoid any class name which includes the word 'ad' to prevent adblocker from blocking the HTML element
+ if (newClassName.toLowerCase().includes('ad')) {
+ this.newClassSize++;
+ return this.generateClassName();
+ }
+
+ this.newClassSize++;
+ return newClassName;
+ }
+}
diff --git a/packages/parcel-transformer/src/index.ts b/packages/parcel-transformer/src/index.ts
index 474012f15..f9b66bafa 100644
--- a/packages/parcel-transformer/src/index.ts
+++ b/packages/parcel-transformer/src/index.ts
@@ -121,6 +121,7 @@ export default new Transformer({
'@compiled/babel-plugin',
{
...config,
+ classNameCompressionMap: config.extract && config.classNameCompressionMap,
onIncludedFiles: (files: string[]) => includedFiles.push(...files),
resolver: {
// The resolver needs to be synchronous, as babel plugins must be synchronous
diff --git a/packages/parcel-transformer/src/types.ts b/packages/parcel-transformer/src/types.ts
index f518ac2cd..7bd764cd2 100644
--- a/packages/parcel-transformer/src/types.ts
+++ b/packages/parcel-transformer/src/types.ts
@@ -39,4 +39,12 @@ export interface ParcelTransformerOpts extends BabelPluginOpts {
* Default to `false`
*/
addComponentName?: boolean;
+
+ /**
+ * A map holds the key-value pairs between full Atomic class names and the compressed ones
+ * i.e. { '_aaaabbbb': 'a' }
+ *
+ * Default to `undefined`
+ */
+ classNameCompressionMap?: { [index: string]: string };
}
diff --git a/packages/react/src/runtime/__perf__/ax.test.ts b/packages/react/src/runtime/__perf__/ax.test.ts
index d8b398ade..40e0117da 100644
--- a/packages/react/src/runtime/__perf__/ax.test.ts
+++ b/packages/react/src/runtime/__perf__/ax.test.ts
@@ -3,24 +3,26 @@ import { runBenchmark } from '@compiled/benchmark';
import { ax } from '../index';
describe('ax benchmark', () => {
- it('completes with ax() string as the fastest', async () => {
- const arr = [
- '_19itglyw',
- '_2rko1l7b',
- '_ca0qftgi',
- '_u5f319bv',
- '_n3tdftgi',
- '_19bv19bv',
- '_bfhk1mzw',
- '_syazu67f',
- '_k48p1nn1',
- '_ect41kw7',
- '_1wybdlk8',
- '_irr3mlcl',
- '_1di6vctu',
- undefined,
- ];
+ const arr = [
+ '_19itglyw',
+ '_2rko1l7b',
+ '_ca0qftgi',
+ '_u5f319bv',
+ '_n3tdftgi',
+ '_19bv19bv',
+ '_bfhk1mzw',
+ '_syazu67f',
+ '_k48p1nn1',
+ '_ect41kw7',
+ '_1wybdlk8',
+ '_irr3mlcl',
+ '_1di6vctu',
+ // `undefined` is an acceptable parameter so we want to include it in the test case.
+ // Example: ax(['aaaabbbb', foo() && "aaaacccc"])
+ undefined,
+ ];
+ it('completes with ax() string as the fastest', async () => {
// Remove undefined and join the strings
const str = arr.slice(0, -1).join(' ');
@@ -39,4 +41,24 @@ describe('ax benchmark', () => {
fastest: ['ax() string'],
});
}, 30000);
+
+ it('completes with ax() non-compressed class names as the fastest', async () => {
+ const arrWithCompressedClassNames = arr.map((item) =>
+ item ? `${item.slice(0, 4)}_${item.slice(8)}` : item
+ );
+ const benchmark = await runBenchmark('ax', [
+ {
+ name: 'ax() array',
+ fn: () => ax(arr),
+ },
+ {
+ name: 'ax() array with compressed class names',
+ fn: () => ax(arrWithCompressedClassNames),
+ },
+ ]);
+
+ expect(benchmark).toMatchObject({
+ fastest: ['ax() array'],
+ });
+ }, 30000);
});
diff --git a/packages/react/src/runtime/__tests__/ax.test.ts b/packages/react/src/runtime/__tests__/ax.test.ts
index e938e1ebf..a49e454d3 100644
--- a/packages/react/src/runtime/__tests__/ax.test.ts
+++ b/packages/react/src/runtime/__tests__/ax.test.ts
@@ -1,70 +1,81 @@
import ax from '../ax';
describe('ax', () => {
- it('should join single classes together', () => {
- const result = ax(['foo', 'bar']);
-
- expect(result).toEqual('foo bar');
- });
-
- it('should join multi classes together', () => {
- const result = ax(['foo baz', 'bar']);
-
- expect(result).toEqual('foo baz bar');
- });
-
- it('should remove undefined', () => {
- const result = ax(['foo', 'bar', undefined]);
-
- expect(result).toEqual('foo bar');
- });
-
- it('should ensure the last atomic declaration of a single group wins', () => {
- const result = ax(['_aaaabbbb', '_aaaacccc']);
-
- expect(result).toEqual('_aaaacccc');
- });
-
- it('should ensure the last atomic declaration of many single groups wins', () => {
- const result = ax(['_aaaabbbb', '_aaaacccc', '_aaaadddd', '_aaaaeeee']);
-
- expect(result).toEqual('_aaaaeeee');
- });
-
- it('should ensure the last atomic declaration of a multi group wins', () => {
- const result = ax(['_aaaabbbb _aaaacccc']);
-
- expect(result).toEqual('_aaaacccc');
- });
-
- it('should ensure the last atomic declaration of many multi groups wins', () => {
- const result = ax(['_aaaabbbb _aaaacccc _aaaadddd _aaaaeeee']);
-
- expect(result).toEqual('_aaaaeeee');
- });
-
- it('should not remove any atomic declarations if there are no duplicate groups', () => {
- const result = ax(['_aaaabbbb', '_bbbbcccc']);
-
- expect(result).toEqual('_aaaabbbb _bbbbcccc');
- });
-
- it('should not apply conditional class', () => {
- const isEnabled: boolean = (() => false)();
- const result = ax([isEnabled && 'foo', 'bar']);
-
- expect(result).toEqual('bar');
- });
-
- it('should ignore non atomic declarations', () => {
- const result = ax(['hello_there', 'hello_world']);
-
- expect(result).toEqual('hello_there hello_world');
- });
-
- it('should ignore non atomic declarations when atomic declarations exist', () => {
- const result = ax(['hello_there', 'hello_world', '_aaaabbbb']);
-
- expect(result).toEqual('hello_there hello_world _aaaabbbb');
+ const isEnabled: boolean = (() => false)();
+
+ it.each([
+ ['should handle empty array', [], undefined],
+ ['should handle array with undefined', [undefined], undefined],
+ ['should join single classes together', ['foo', 'bar'], 'foo bar'],
+ ['should join multi classes together', ['foo baz', 'bar'], 'foo baz bar'],
+ ['should remove undefined', ['foo', 'bar', undefined], 'foo bar'],
+ [
+ 'should ensure the last atomic declaration of a single group wins',
+ ['_aaaabbbb', '_aaaacccc'],
+ '_aaaacccc',
+ ],
+ [
+ 'should ensure the last atomic declaration of a single group with short class name wins',
+ ['_aaaabbbb', '_aaaacccc', '_aaaa_a'],
+ 'a',
+ ],
+ [
+ 'should ensure the last atomic declaration of many single groups wins',
+ ['_aaaabbbb', '_aaaacccc', '_aaaadddd', '_aaaaeeee'],
+ '_aaaaeeee',
+ ],
+ [
+ 'should ensure the last atomic declaration of many single groups with short class name wins',
+ ['_aaaabbbb', '_aaaacccc', '_aaaa_a', '_aaaa_b'],
+ 'b',
+ ],
+ [
+ 'should ensure the last atomic declaration of a multi group wins',
+ ['_aaaabbbb _aaaacccc'],
+ '_aaaacccc',
+ ],
+ [
+ 'should ensure the last atomic declaration of a multi group with short class name wins',
+ ['_aaaa_e', '_aaaabbbb _aaaacccc'],
+ '_aaaacccc',
+ ],
+ [
+ 'should ensure the last atomic declaration of many multi groups wins',
+ ['_aaaabbbb _aaaacccc _aaaadddd _aaaaeeee'],
+ '_aaaaeeee',
+ ],
+ [
+ 'should ensure the last atomic declaration of many multi groups with short class name wins',
+ ['_aaaabbbb', '_aaaa_a', '_bbbb_b', '_ddddcccc'],
+ 'a b _ddddcccc',
+ ],
+ [
+ 'should not remove any atomic declarations if there are no duplicate groups',
+ ['_aaaabbbb', '_bbbbcccc'],
+ '_aaaabbbb _bbbbcccc',
+ ],
+ [
+ 'should not remove any atomic declarations if there are short class name and no duplicate groups',
+ ['_eeee_e', '_aaaabbbb', '_bbbbcccc'],
+ 'e _aaaabbbb _bbbbcccc',
+ ],
+ ['should not apply conditional class', [isEnabled && 'foo', 'bar'], 'bar'],
+ [
+ 'should ignore non atomic declarations',
+ ['hello_there', 'hello_world'],
+ 'hello_there hello_world',
+ ],
+ [
+ 'should ignore non atomic declarations when atomic declarations exist',
+ ['hello_there', 'hello_world', '_aaaabbbb'],
+ 'hello_there hello_world _aaaabbbb',
+ ],
+ [
+ 'should ignore non atomic declarations when atomic declarations with short class name exist',
+ ['hello_there', 'hello_world', '_aaaa_a'],
+ 'hello_there hello_world a',
+ ],
+ ])('%s', (_, params, result) => {
+ expect(result).toEqual(ax(params));
});
});
diff --git a/packages/react/src/runtime/ax.ts b/packages/react/src/runtime/ax.ts
index fa92d0a78..92948ff58 100644
--- a/packages/react/src/runtime/ax.ts
+++ b/packages/react/src/runtime/ax.ts
@@ -28,10 +28,8 @@ const ATOMIC_GROUP_LENGTH = 5;
* @param classes
*/
export default function ax(classNames: (string | undefined | false)[]): string | undefined {
- if (classNames.length <= 1 && (!classNames[0] || classNames[0].indexOf(' ') === -1)) {
- // short circuit if there's no custom class names.
- return classNames[0] || undefined;
- }
+ // short circuit if there's no class names.
+ if (classNames.length <= 1 && !classNames[0]) return undefined;
const atomicGroups: Record = {};
@@ -45,11 +43,11 @@ export default function ax(classNames: (string | undefined | false)[]): string |
for (let x = 0; x < groups.length; x++) {
const atomic = groups[x];
- const atomicGroupName = atomic.slice(
- 0,
- atomic.charCodeAt(0) === UNDERSCORE_UNICODE ? ATOMIC_GROUP_LENGTH : undefined
- );
- atomicGroups[atomicGroupName] = atomic;
+ const isAtomic = atomic.charCodeAt(0) === UNDERSCORE_UNICODE;
+ const isCompressed = isAtomic && atomic.charCodeAt(5) === UNDERSCORE_UNICODE;
+
+ const atomicGroupName = isAtomic ? atomic.slice(0, ATOMIC_GROUP_LENGTH) : atomic;
+ atomicGroups[atomicGroupName] = isCompressed ? atomic.slice(ATOMIC_GROUP_LENGTH + 1) : atomic;
}
}
diff --git a/packages/webpack-loader/src/compiled-loader.ts b/packages/webpack-loader/src/compiled-loader.ts
index fe8119be9..5618fe9db 100644
--- a/packages/webpack-loader/src/compiled-loader.ts
+++ b/packages/webpack-loader/src/compiled-loader.ts
@@ -32,6 +32,7 @@ function getLoaderOptions(context: LoaderContext) {
ssr = false,
optimizeCss = true,
addComponentName = false,
+ classNameCompressionMap = undefined,
}: CompiledLoaderOptions = typeof context.getOptions === 'undefined'
? // Webpack v4 flow
getOptions(context)
@@ -75,6 +76,9 @@ function getLoaderOptions(context: LoaderContext) {
addComponentName: {
type: 'boolean',
},
+ classNameCompressionMap: {
+ type: 'object',
+ },
},
});
@@ -91,6 +95,7 @@ function getLoaderOptions(context: LoaderContext) {
ssr,
optimizeCss,
addComponentName,
+ classNameCompressionMap,
};
}
@@ -163,6 +168,8 @@ export default async function compiledLoader(
'@compiled/babel-plugin',
{
...options,
+ // Turn off compressing class names if stylesheet extraction is off
+ classNameCompressionMap: options.extract && options.classNameCompressionMap,
onIncludedFiles: (files: string[]) => includedFiles.push(...files),
resolver: {
// The resolver needs to be synchronous, as babel plugins must be synchronous
diff --git a/packages/webpack-loader/src/types.ts b/packages/webpack-loader/src/types.ts
index 92f4d9bc8..91913378b 100644
--- a/packages/webpack-loader/src/types.ts
+++ b/packages/webpack-loader/src/types.ts
@@ -78,6 +78,14 @@ export interface CompiledLoaderOptions {
* Default to `false`
*/
addComponentName?: boolean;
+
+ /**
+ * A map holds the key-value pairs between full Atomic class names and the compressed ones
+ * i.e. { '_aaaabbbb': 'a' }
+ *
+ * Default to `undefined`
+ */
+ classNameCompressionMap?: object;
}
export interface CompiledExtractPluginOptions {