Skip to content

Commit c4e6b7c

Browse files
authoredApr 26, 2023
Introduce ac (#1437)
* Introduce ac * benchmark ac * Amend test case name * Update runtime library and Conditionally use total 1774.13 via babel-plugin * Make sure ac is properly exported * Regenerate Flow type * Update ac size limit * Add changeset * Ensure only one Compiled runtime * Some micro perf improvement * Change size-limit and generate Flow * Inline side-effects * Adjust size limit * Add changeset for TS compiler target change
1 parent c8cc26d commit c4e6b7c

27 files changed

+550
-91
lines changed
 

‎.changeset/hip-numbers-enjoy.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@compiled/babel-plugin': minor
3+
'@compiled/react': minor
4+
---
5+
6+
Introduce a new runtime class name library, which resolves the `ax` chaining issue. The new library is used only if class name compression is enabled.

‎.changeset/honest-moles-agree.md

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@compiled/babel-plugin-strip-runtime': minor
3+
'@compiled/parcel-transformer': minor
4+
'@compiled/parcel-optimizer': minor
5+
'@compiled/webpack-loader': minor
6+
'@compiled/eslint-plugin': minor
7+
'@compiled/babel-plugin': minor
8+
'@compiled/react': minor
9+
'@compiled/utils': minor
10+
'@compiled/css': minor
11+
'@compiled/codemods': minor
12+
'@compiled/jest': minor
13+
---
14+
15+
Change TypeScript compiler target from es5 to es6.

‎package.json

+18-11
Original file line numberDiff line numberDiff line change
@@ -125,25 +125,32 @@
125125
},
126126
"size-limit": [
127127
{
128-
"path": "./packages/react/dist/browser/runtime.js",
128+
"path": "./packages/react/dist/browser/runtime/css-custom-property.js",
129129
"limit": "50B",
130-
"import": "{ ix }",
131-
"ignore": [
132-
"react"
133-
]
130+
"import": "ix"
131+
},
132+
{
133+
"path": "./packages/react/dist/browser/runtime/ax.js",
134+
"limit": "195B",
135+
"import": "ax"
136+
},
137+
{
138+
"path": "./packages/react/dist/browser/runtime/ac.js",
139+
"limit": "332B",
140+
"import": "ac, { clearCache }"
134141
},
135142
{
136-
"path": "./packages/react/dist/browser/runtime.js",
137-
"limit": "189B",
138-
"import": "{ ax }",
143+
"path": "./packages/react/dist/browser/runtime/style.js",
144+
"limit": "475B",
145+
"import": "CS",
139146
"ignore": [
140147
"react"
141148
]
142149
},
143150
{
144-
"path": "./packages/react/dist/browser/runtime.js",
145-
"limit": "473B",
146-
"import": "{ CC, CS }",
151+
"path": "./packages/react/dist/browser/runtime/style-cache.js",
152+
"limit": "99B",
153+
"import": "CC",
147154
"ignore": [
148155
"react"
149156
]

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

+21-7
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ describe('babel plugin', () => {
218218
}
219219
);
220220

221-
expect(actual).toIncludeMultiple(['.a{font-size:12px}', 'ax(["_1wyb_a", __cmplp.className])']);
221+
expect(actual).toIncludeMultiple(['.a{font-size:12px}', 'ac(["_1wyb_a", __cmplp.className])']);
222222
});
223223

224224
it('should compress class name for css props', () => {
@@ -235,7 +235,7 @@ describe('babel plugin', () => {
235235
}
236236
);
237237

238-
expect(actual).toIncludeMultiple(['.a{font-size:12px}', 'ax(["_1wyb_a"])']);
238+
expect(actual).toIncludeMultiple(['.a{font-size:12px}', 'ac(["_1wyb_a"])']);
239239
});
240240

241241
it('should compress class name for ClassNames', () => {
@@ -256,7 +256,7 @@ describe('babel plugin', () => {
256256
}
257257
);
258258

259-
expect(actual).toIncludeMultiple(['.a{font-size:12px}', 'className={ax(["_1wyb_a"])']);
259+
expect(actual).toIncludeMultiple(['.a{font-size:12px}', 'className={ac(["_1wyb_a"])']);
260260
});
261261

262262
it('should compress class names with atrules', () => {
@@ -274,7 +274,7 @@ describe('babel plugin', () => {
274274

275275
expect(actual).toIncludeMultiple([
276276
'@media (max-width:1250px){.a{font-size:12px}}',
277-
'ax(["_pz52_a"])',
277+
'ac(["_pz52_a"])',
278278
]);
279279
});
280280

@@ -295,7 +295,7 @@ describe('babel plugin', () => {
295295
expect(actual).toIncludeMultiple([
296296
'.a:active{color:red}',
297297
'.b:hover{font-size:12px}',
298-
'ax(["_e915_b _9h8h_a"])',
298+
'ac(["_e915_b _9h8h_a"])',
299299
]);
300300
});
301301

@@ -312,7 +312,7 @@ describe('babel plugin', () => {
312312
}
313313
);
314314

315-
expect(actual).toIncludeMultiple(['.a >div div div:hover{font-size:12px}', 'ax(["_1jkf_a"]']);
315+
expect(actual).toIncludeMultiple(['.a >div div div:hover{font-size:12px}', 'ac(["_1jkf_a"]']);
316316
});
317317

318318
it('should compress conditional class names', () => {
@@ -354,7 +354,21 @@ describe('babel plugin', () => {
354354
'._19pk19bv{margin-top:10px}',
355355
'.a{color:red}',
356356
'._1wyb1fwx{font-size:12px}',
357-
'ax(["_1wyb1fwx _syaz_a _19pk19bv"]',
357+
'ac(["_1wyb1fwx _syaz_a _19pk19bv"]',
358358
]);
359359
});
360+
361+
it('should import ac if compression map is provided', () => {
362+
const actual = transform(
363+
`
364+
import '@compiled/react';
365+
<div css={{ fontSize: 12 }} />
366+
`,
367+
{
368+
classNameCompressionMap: {},
369+
}
370+
);
371+
372+
expect(actual).toInclude('import { ac, ix, CC, CS } from "@compiled/react/runtime"');
373+
});
360374
});

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export default declare<State>((api) => {
8989

9090
preserveLeadingComments(path);
9191

92-
appendRuntimeImports(path);
92+
appendRuntimeImports(path, state);
9393

9494
const hasPragma = pragma.jsxImportSource || pragma.jsx;
9595

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { buildCodeFrameError, pickFunctionBody } from '../utils/ast';
66
import { compiledTemplate } from '../utils/build-compiled-component';
77
import { buildCssVariables } from '../utils/build-css-variables';
88
import { buildCss } from '../utils/css-builders';
9+
import { getRuntimeClassNameLibrary } from '../utils/get-runtime-class-name-library';
910
import { resolveIdentifierComingFromDestructuring } from '../utils/resolve-binding';
1011
import { transformCssItems } from '../utils/transform-css-items';
1112
import type { CSSOutput } from '../utils/types';
@@ -137,7 +138,11 @@ export const visitClassNamesPath = (path: NodePath<t.JSXElement>, meta: Metadata
137138
collectedVariables.push(...builtCss.variables);
138139
collectedSheets.push(...sheets);
139140

140-
path.replaceWith(t.callExpression(t.identifier('ax'), [t.arrayExpression(classNames)]));
141+
path.replaceWith(
142+
t.callExpression(t.identifier(getRuntimeClassNameLibrary(meta)), [
143+
t.arrayExpression(classNames),
144+
])
145+
);
141146
},
142147
});
143148

‎packages/babel-plugin/src/utils/append-runtime-imports.ts

+10-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import type { NodePath } from '@babel/traverse';
22
import * as t from '@babel/types';
33

4+
import type { State } from '../types';
5+
46
/**
57
* Wrapper to make defining import specifiers easier.
68
* If `localName` is defined it will rename the import to it,
@@ -13,7 +15,9 @@ const importSpecifier = (name: string, localName?: string): t.ImportSpecifier =>
1315
return t.importSpecifier(t.identifier(name), t.identifier(localName || name));
1416
};
1517

16-
const COMPILED_RUNTIME_IMPORTS = ['ax', 'ix', 'CC', 'CS'];
18+
// Runtime function `ac` is less performant than `ax`, so we only want to import `ac` if classNameCompressionMap is provided.
19+
const COMPILED_RUNTIME_IMPORTS_WITH_COMPRESSION = ['ac', 'ix', 'CC', 'CS'];
20+
const COMPILED_RUNTIME_IMPORTS_WITHOUT_COMPRESSION = ['ax', 'ix', 'CC', 'CS'];
1721
const COMPILED_RUNTIME_MODULE = '@compiled/react/runtime';
1822

1923
/**
@@ -23,7 +27,11 @@ const COMPILED_RUNTIME_MODULE = '@compiled/react/runtime';
2327
*
2428
* @param path ImportDeclaration node path
2529
*/
26-
export const appendRuntimeImports = (path: NodePath<t.Program>): void => {
30+
export const appendRuntimeImports = (path: NodePath<t.Program>, state: State): void => {
31+
const COMPILED_RUNTIME_IMPORTS = state.opts.classNameCompressionMap
32+
? COMPILED_RUNTIME_IMPORTS_WITH_COMPRESSION
33+
: COMPILED_RUNTIME_IMPORTS_WITHOUT_COMPRESSION;
34+
2735
// Check if we have any sibling runtime import
2836
const previouslyDeclaredRuntimeDeclaration = path
2937
.get('body')

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

+5-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { Metadata } from '../types';
77

88
import { buildCssVariables } from './build-css-variables';
99
import { getJSXAttribute } from './get-jsx-attribute';
10+
import { getRuntimeClassNameLibrary } from './get-runtime-class-name-library';
1011
import { hoistSheet } from './hoist-sheet';
1112
import { transformCssItems } from './transform-css-items';
1213
import type { CSSOutput } from './types';
@@ -80,15 +81,17 @@ export const buildCompiledComponent = (
8081
const values: t.Expression[] = classNames.concat(classNameExpression);
8182

8283
classNameAttribute.value = t.jsxExpressionContainer(
83-
t.callExpression(t.identifier('ax'), [t.arrayExpression(values)])
84+
t.callExpression(t.identifier(getRuntimeClassNameLibrary(meta)), [t.arrayExpression(values)])
8485
);
8586
} else {
8687
// No class name - just push our own one.
8788
node.openingElement.attributes.push(
8889
t.jsxAttribute(
8990
t.jsxIdentifier('className'),
9091
t.jsxExpressionContainer(
91-
t.callExpression(t.identifier('ax'), [t.arrayExpression(classNames)])
92+
t.callExpression(t.identifier(getRuntimeClassNameLibrary(meta)), [
93+
t.arrayExpression(classNames),
94+
])
9295
)
9396
)
9497
);

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

+6-3
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ 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';
19+
import { compressClassNamesForRuntime } from './compress-class-names-for-runtime';
2020
import { getItemCss } from './css-builders';
21+
import { getRuntimeClassNameLibrary } from './get-runtime-class-name-library';
2122
import { hoistSheet } from './hoist-sheet';
2223
import { applySelectors, transformCssItems } from './transform-css-items';
2324
import type { CSSOutput, CssItem } from './types';
@@ -169,7 +170,9 @@ const styledTemplate = (opts: StyledTemplateOpts, meta: Metadata): t.Node => {
169170
{...${hasInvalidDomProps ? DOM_PROPS_IDENTIFIER_NAME : PROPS_IDENTIFIER_NAME}}
170171
style={%%styleProp%%}
171172
ref={${REF_IDENTIFIER_NAME}}
172-
className={ax([${classNames} ${PROPS_IDENTIFIER_NAME}.className])}
173+
className={${getRuntimeClassNameLibrary(
174+
meta
175+
)}([${classNames} ${PROPS_IDENTIFIER_NAME}.className])}
173176
/>
174177
</CC>
175178
);
@@ -240,7 +243,7 @@ export const buildStyledComponent = (tag: Tag, cssOutput: CSSOutput, meta: Metad
240243
const classNames = [
241244
...[
242245
t.stringLiteral(
243-
compressClassNamesForAx(
246+
compressClassNamesForRuntime(
244247
uniqueUnconditionalCssOutput.classNames,
245248
meta.state.opts.classNameCompressionMap
246249
).join(' ')

‎packages/babel-plugin/src/utils/compress-class-names-for-ax.ts ‎packages/babel-plugin/src/utils/compress-class-names-for-runtime.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
/**
22
* Compress class names based on `classNameCompressionMap`.
3-
* The compressed class name has a format of `_aaaa_a`, which is expected by `ax`.
3+
* The compressed class name has a format of `_aaaa_a`, which is expected by `ac`.
44
* `aaaa` is the atomic group and `a` is the compressed name.
55
*/
6-
export const compressClassNamesForAx = (
6+
export const compressClassNamesForRuntime = (
77
classNames: string[],
88
classNameCompressionMap?: { [index: string]: string }
99
): string[] => {
10+
// If no classNameCompressionMap, return original class names.
1011
if (!classNameCompressionMap) return classNames;
1112
return classNames.map((className) => {
1213
const compressedClassName =
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import type { Metadata } from '../types';
2+
3+
// If classNameCompressionMap is provided, import and use `ac`, otherwise use `ax`.
4+
// Although `ac` does what `ax` does plus handling compressed class names, `ax` is more performant than `ac`.
5+
// Therefore, we use `ax` by default unless classNameCompressionMap is provided.
6+
export const getRuntimeClassNameLibrary = (meta: Metadata): string => {
7+
return meta.state.opts.classNameCompressionMap ? 'ac' : 'ax';
8+
};

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

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

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

6-
import { compressClassNamesForAx } from './compress-class-names-for-ax';
6+
import { compressClassNamesForRuntime } from './compress-class-names-for-runtime';
77
import { getItemCss } from './css-builders';
88
import type { CssItem } from './types';
99

@@ -66,7 +66,7 @@ const transformCssItem = (
6666
item.operator,
6767
item.expression,
6868
t.stringLiteral(
69-
compressClassNamesForAx(
69+
compressClassNamesForRuntime(
7070
logicalCss.classNames,
7171
meta.state.opts.classNameCompressionMap
7272
).join(' ')
@@ -76,7 +76,7 @@ const transformCssItem = (
7676

7777
default:
7878
const css = transformCss(getItemCss(item), meta.state.opts);
79-
const className = compressClassNamesForAx(
79+
const className = compressClassNamesForRuntime(
8080
css.classNames,
8181
meta.state.opts.classNameCompressionMap
8282
).join(' ');

‎packages/react/package.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@
1717
},
1818
"license": "Apache-2.0",
1919
"author": "Michael Dougall",
20-
"sideEffects": false,
20+
"sideEffects": [
21+
"./dist/browser/runtime/index.js",
22+
"./dist/cjs/runtime/index.js",
23+
"./dist/esm/runtime/index.js"
24+
],
2125
"exports": {
2226
".": {
2327
"import": [

‎packages/react/src/__tests__/server-side-hydrate.test.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ describe('server side hydrate', () => {
1616
jest.resetModules();
1717
// We need to force this module to re-instantiate because on the client
1818
// when it does it will move all found SSRd style elements to the head.
19-
require('../runtime');
19+
require('../runtime/style-cache');
2020
};
2121

2222
const appendHTML = (markup: string) => {

‎packages/react/src/global.d.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export {};
2+
3+
declare global {
4+
interface Window {
5+
__COMPILED_IMPORTED__: undefined | true;
6+
}
7+
}

‎packages/react/src/runtime.js.flow

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
* Flowgen v1.20.1
55
* @flow
66
*/
7-
declare export { CC, CS, ax, ix } from './runtime/index';
7+
declare export { CC, CS, ax, ac, clearAcCache, ix } from './runtime/index';

‎packages/react/src/runtime.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export { CC, CS, ax, ix } from './runtime/index';
1+
export { CC, CS, ax, ac, clearAcCache, ix } from './runtime/index';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { runBenchmark } from '@compiled/benchmark';
2+
3+
import { ac } from '../ac';
4+
import ax from '../ax';
5+
6+
describe('ac vs ax benchmark', () => {
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` is an acceptable parameter so we want to include it in the test case.
22+
// Example: ax(['aaaabbbb', foo() && "aaaacccc"])
23+
undefined,
24+
];
25+
26+
// Remove undefined and join the strings
27+
const str = arr.slice(0, -1).join(' ');
28+
29+
const arrWithCompressedClassNames = arr.map((item) =>
30+
item ? `${item.slice(0, 4)}_${item.slice(8)}` : item
31+
);
32+
33+
const strWithCompressedClassNames = arr
34+
.map((item) => (item ? `${item.slice(0, 4)}_${item.slice(8)}` : item))
35+
.slice(0, -1)
36+
.join(' ');
37+
38+
it('compares ax array with ac array', async () => {
39+
// Remove undefined and join the strings
40+
const benchmark = await runBenchmark('ax', [
41+
{
42+
name: 'ax() array',
43+
fn: () => ax(arr),
44+
},
45+
{
46+
name: 'ac() array with compressed class names',
47+
fn: () => {
48+
ac(arrWithCompressedClassNames)?.toString();
49+
},
50+
},
51+
]);
52+
53+
expect(benchmark).toMatchObject({
54+
fastest: ['ax() array'],
55+
});
56+
}, 30000);
57+
58+
it('compares ax string with ac string', async () => {
59+
// Remove undefined and join the strings
60+
const benchmark = await runBenchmark('ax', [
61+
{
62+
name: 'ax() string',
63+
fn: () => ax([str, undefined]),
64+
},
65+
{
66+
name: 'ac() string with compressed class names',
67+
fn: () => {
68+
ac([strWithCompressedClassNames, undefined])?.toString();
69+
},
70+
},
71+
]);
72+
73+
expect(benchmark).toMatchObject({
74+
fastest: ['ax() string'],
75+
});
76+
}, 30000);
77+
78+
it('compares chaining ax with chaining ac', async () => {
79+
// Remove undefined and join the strings
80+
const benchmark = await runBenchmark('ax', [
81+
{
82+
name: 'chain ax() string',
83+
fn: () => ax([ax([str, undefined]), '_aaaabbbb']),
84+
},
85+
{
86+
name: 'chain ac() string with compressed class names',
87+
fn: () => {
88+
ac([ac([strWithCompressedClassNames, undefined]), '_aaaa_a'])?.toString();
89+
},
90+
},
91+
]);
92+
93+
expect(benchmark).toMatchObject({
94+
fastest: ['chain ac() string with compressed class names'],
95+
});
96+
}, 30000);
97+
});

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

-20
Original file line numberDiff line numberDiff line change
@@ -41,24 +41,4 @@ describe('ax benchmark', () => {
4141
fastest: ['ax() string'],
4242
});
4343
}, 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);
6444
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { ac, memoizedAc, getCache } from '../ac';
2+
3+
describe('ac', () => {
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(ac(params)?.toString());
80+
});
81+
82+
it('should ensure the last atomic declaration wins if calling ax multiple times with short class names', () => {
83+
expect(ac([ac(['_aaaa_b']), '_aaaa_c'])?.toString()).toEqual('c');
84+
});
85+
});
86+
87+
describe('memoizedAc', () => {
88+
it('should cache correctly', () => {
89+
memoizedAc([memoizedAc(['_aaaa_b', '_aaaabbbb', 'hello_world']), '_bbbb_d', '_aaaa_e']);
90+
91+
expect(getCache()).toMatchInlineSnapshot(`
92+
Map {
93+
"_aaaa_b _aaaabbbb hello_world" => AtomicGroups {
94+
"values": Map {
95+
"_aaaa" => "_aaaabbbb",
96+
"hello_world" => "hello_world",
97+
},
98+
},
99+
"_aaaabbbb hello_world _bbbb_d _aaaa_e" => AtomicGroups {
100+
"values": Map {
101+
"_aaaa" => "e",
102+
"hello_world" => "hello_world",
103+
"_bbbb" => "d",
104+
},
105+
},
106+
}
107+
`);
108+
});
109+
it('should not create a new ref', () => {
110+
expect(memoizedAc(['a'])).toBe(memoizedAc(['a']));
111+
});
112+
});

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

+2-27
Original file line numberDiff line numberDiff line change
@@ -14,51 +14,31 @@ describe('ax', () => {
1414
['_aaaabbbb', '_aaaacccc'],
1515
'_aaaacccc',
1616
],
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-
],
2217
[
2318
'should ensure the last atomic declaration of many single groups wins',
2419
['_aaaabbbb', '_aaaacccc', '_aaaadddd', '_aaaaeeee'],
2520
'_aaaaeeee',
2621
],
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-
],
3222
[
3323
'should ensure the last atomic declaration of a multi group wins',
3424
['_aaaabbbb _aaaacccc'],
3525
'_aaaacccc',
3626
],
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-
],
4227
[
4328
'should ensure the last atomic declaration of many multi groups wins',
4429
['_aaaabbbb _aaaacccc _aaaadddd _aaaaeeee'],
4530
'_aaaaeeee',
4631
],
4732
[
4833
'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',
34+
['_aaaabbbb', '_aaaaaaa', '_ddddbbb', '_ddddcccc'],
35+
'_aaaaaaa _ddddcccc',
5136
],
5237
[
5338
'should not remove any atomic declarations if there are no duplicate groups',
5439
['_aaaabbbb', '_bbbbcccc'],
5540
'_aaaabbbb _bbbbcccc',
5641
],
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-
],
6242
['should not apply conditional class', [isEnabled && 'foo', 'bar'], 'bar'],
6343
[
6444
'should ignore non atomic declarations',
@@ -70,11 +50,6 @@ describe('ax', () => {
7050
['hello_there', 'hello_world', '_aaaabbbb'],
7151
'hello_there hello_world _aaaabbbb',
7252
],
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-
],
7853
])('%s', (_, params, result) => {
7954
expect(result).toEqual(ax(params));
8055
});

‎packages/react/src/runtime/ac.js.flow

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* Flowtype definitions for ac
3+
* Generated by Flowgen from a Typescript Definition
4+
* Flowgen v1.20.1
5+
* @flow
6+
*/
7+
/**
8+
* Memoize the result of ac so if it is called with the same args, it returns immediately.
9+
* Also, to prevent useless React rerenders
10+
*/
11+
declare var cache: Map<any, any>;
12+
/**
13+
* `ac` returns an instance of AtomicGroups. The instance holds the knowledge of Atomic Group so we can chain `ac`.
14+
* e.g. <div className={ax([ax(['_aaaa_b']), '_aaaa_c'])} />
15+
*/
16+
declare class AtomicGroups {
17+
values: Map<string, string>;
18+
constructor(values: Map<string, string>): this;
19+
toString(): string;
20+
}
21+
/**
22+
* Joins classes together and ensures atomic declarations of a single group exist.
23+
* Atomic declarations take the form of `_{group}{value}` (always prefixed with an underscore),
24+
* where both `group` and `value` are hashes **four characters long**.
25+
* Class names can be of any length,
26+
* this function can take both atomic declarations and class names.
27+
*
28+
* Input:
29+
*
30+
* ```
31+
* ax(['_aaaabbbb', '_aaaacccc'])
32+
* ```
33+
*
34+
* Output:
35+
*
36+
* ```
37+
* '_aaaacccc'
38+
* ```
39+
* @param classes
40+
*/
41+
declare export function ac(
42+
classNames: (AtomicGroups | string | void | false)[]
43+
): AtomicGroups | void;
44+
declare export function memoizedAc(
45+
classNames: (AtomicGroups | string | void | false)[]
46+
): AtomicGroups | void;
47+
declare var _default: typeof ac;
48+
declare export default typeof _default;
49+
/**
50+
* Provide an opportunity to clear the cache to prevent memory leak.
51+
*/
52+
declare export function clearCache(): void;
53+
/**
54+
* Expose cache
55+
*/
56+
declare export function getCache(): typeof cache;

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

+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import { isServerEnvironment } from './is-server-environment';
2+
3+
const UNDERSCORE_UNICODE = 95;
4+
5+
/**
6+
* This length includes the underscore,
7+
* e.g. `"_1s4A"` would be a valid atomic group hash.
8+
*/
9+
const ATOMIC_GROUP_LENGTH = 5;
10+
11+
/**
12+
* Memoize the result of ac so if it is called with the same args, it returns immediately.
13+
* Also, to prevent useless React rerenders
14+
*/
15+
const cache = new Map();
16+
17+
/**
18+
* `ac` returns an instance of AtomicGroups. The instance holds the knowledge of Atomic Group so we can chain `ac`.
19+
* e.g. <div className={ax([ax(['_aaaa_b']), '_aaaa_c'])} />
20+
*/
21+
class AtomicGroups {
22+
values: Map<string, string>;
23+
constructor(values: Map<string, string>) {
24+
// An object stores the relation between Atomic group and actual class name
25+
// e.g. { "aaaa": "a" } `aaaa` is the Atomic group and `a` is the actual class name
26+
this.values = values;
27+
}
28+
toString() {
29+
let str = '';
30+
31+
for (const [, value] of this.values) {
32+
str += value + ' ';
33+
}
34+
35+
return str.slice(0, -1);
36+
}
37+
}
38+
39+
/**
40+
* Joins classes together and ensures atomic declarations of a single group exist.
41+
* Atomic declarations take the form of `_{group}{value}` (always prefixed with an underscore),
42+
* where both `group` and `value` are hashes **four characters long**.
43+
* Class names can be of any length,
44+
* this function can take both atomic declarations and class names.
45+
*
46+
* Input:
47+
*
48+
* ```
49+
* ax(['_aaaabbbb', '_aaaacccc'])
50+
* ```
51+
*
52+
* Output:
53+
*
54+
* ```
55+
* '_aaaacccc'
56+
* ```
57+
*
58+
* @param classes
59+
*/
60+
export function ac(
61+
classNames: (AtomicGroups | string | undefined | false)[]
62+
): AtomicGroups | undefined {
63+
// short circuit if there's no class names.
64+
if (classNames.length <= 1 && !classNames[0]) return undefined;
65+
66+
const atomicGroups: Map<string, string> = new Map();
67+
68+
for (let i = 0; i < classNames.length; i++) {
69+
const cls = classNames[i];
70+
if (!cls) {
71+
continue;
72+
}
73+
74+
if (typeof cls === 'string') {
75+
const groups = cls.split(' ');
76+
77+
for (let x = 0; x < groups.length; x++) {
78+
const atomic = groups[x];
79+
const isAtomic = atomic.charCodeAt(0) === UNDERSCORE_UNICODE;
80+
const isCompressed = isAtomic && atomic.charCodeAt(5) === UNDERSCORE_UNICODE;
81+
82+
const atomicGroupName = isAtomic ? atomic.slice(0, ATOMIC_GROUP_LENGTH) : atomic;
83+
atomicGroups.set(
84+
atomicGroupName,
85+
isCompressed ? atomic.slice(ATOMIC_GROUP_LENGTH + 1) : atomic
86+
);
87+
}
88+
} else {
89+
// if cls is an instance of AtomicGroups, transfer its values to `atomicGroups`
90+
for (const [key, value] of cls.values) {
91+
atomicGroups.set(key, value);
92+
}
93+
}
94+
}
95+
96+
return new AtomicGroups(atomicGroups);
97+
}
98+
99+
export function memoizedAc(
100+
classNames: (AtomicGroups | string | undefined | false)[]
101+
): AtomicGroups | undefined {
102+
// short circuit if there's no class names.
103+
if (classNames.length <= 1 && !classNames[0]) return undefined;
104+
105+
// build the cacheKey based on the function argument
106+
// e.g. if the argument is ["_aaaabbbb", "_aaaa_a", "some-class-name"],
107+
// then the cacheKey is "_aaaabbbb _aaaa_a some-class-name"
108+
let cacheKey = '';
109+
for (let i = 0; i < classNames.length; i += 1) {
110+
const current = classNames[i];
111+
// continue if current is undefined, false, or ""
112+
if (!current) continue;
113+
cacheKey += current + ' ';
114+
}
115+
116+
cacheKey = cacheKey.slice(0, -1);
117+
118+
if (cache.has(cacheKey)) return cache.get(cacheKey);
119+
120+
const result = ac(classNames);
121+
122+
cache.set(cacheKey, result);
123+
124+
return result;
125+
}
126+
127+
// Memoization is primarily used to prevent React from unncessary re-rendering.
128+
// Use unmemoizedAc on server-side because We don't need to worry about re-rendering on server-side.
129+
export default isServerEnvironment() ? ac : memoizedAc;
130+
131+
/**
132+
* Provide an opportunity to clear the cache to prevent memory leak.
133+
*/
134+
export function clearCache(): void {
135+
cache.clear();
136+
}
137+
138+
/**
139+
* Expose cache
140+
*/
141+
export function getCache(): typeof cache {
142+
return cache;
143+
}

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

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

3436
const atomicGroups: Record<string, string> = {};
3537

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

4446
for (let x = 0; x < groups.length; x++) {
4547
const atomic = groups[x];
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;
48+
const atomicGroupName = atomic.slice(
49+
0,
50+
atomic.charCodeAt(0) === UNDERSCORE_UNICODE ? ATOMIC_GROUP_LENGTH : undefined
51+
);
52+
atomicGroups[atomicGroupName] = atomic;
5153
}
5254
}
5355

‎packages/react/src/runtime/index.js.flow

+1
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@
77
declare export { default as CS } from './style';
88
declare export { default as CC } from './style-cache';
99
declare export { default as ax } from './ax';
10+
declare export { default as ac, clearCache as clearAcCache } from './ac';
1011
declare export { default as ix } from './css-custom-property';

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

+12
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,16 @@
11
export { default as CS } from './style';
22
export { default as CC } from './style-cache';
33
export { default as ax } from './ax';
4+
export { default as ac, clearCache as clearAcCache } from './ac';
45
export { default as ix } from './css-custom-property';
6+
7+
// Ensure only one `@compiled/runtime` exist in the bundle.
8+
// This is because `ac` and `style-cache` need to access a singlton.
9+
if (typeof window !== 'undefined') {
10+
if (typeof window.__COMPILED_IMPORTED__ !== 'undefined') {
11+
throw new Error(
12+
'Multiple instances of Compiled Runtime have been found on the page. A likely cause is that muliple versions of `@compiled/react` exist in JS bundle.'
13+
);
14+
}
15+
window.__COMPILED_IMPORTED__ = true;
16+
}

‎packages/tsconfig.options.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"resolveJsonModule": true,
1717
"sourceMap": true,
1818
"strict": true,
19-
"target": "es5",
19+
"target": "es6",
2020
"typeRoots": ["../node_modules/@types", "../types"]
2121
}
2222
}

0 commit comments

Comments
 (0)
Please sign in to comment.