Skip to content

Commit a3a841d

Browse files
Update sorting logic for a lot more shorthands and attempt runtime sorting as well.
Concerns: - Runtime sorting does not work properly as we'd also need per-bucket nested sorting where `@media, :focus, border` needs to be sorted. - `shorthandFor` needs 40 more shorthand properties defined…
1 parent 59c6f2c commit a3a841d

File tree

11 files changed

+598
-36
lines changed

11 files changed

+598
-36
lines changed

packages/css/src/plugins/__tests__/sort-shorthand-declarations.test.ts

Lines changed: 159 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { sortAtomicStyleSheet } from '../sort-atomic-style-sheet';
55

66
const transform = (css: string, enabled = true) => {
77
const result = postcss([
8-
sortAtomicStyleSheet({ sortAtRulesEnabled: false, sortShorthandEnabled: enabled }),
8+
sortAtomicStyleSheet({ sortAtRulesEnabled: true, sortShorthandEnabled: enabled }),
99
]).process(css, {
1010
from: undefined,
1111
});
@@ -184,6 +184,164 @@ describe('sort shorthand vs. longhand declarations', () => {
184184
`);
185185
});
186186

187+
it("sorts 4 layers deep of shorthands from 'all' to 'border-block-start'", () => {
188+
const actual = transform(outdent`
189+
@media all {
190+
.f {
191+
display: block;
192+
}
193+
.e {
194+
border-block-start-color: transparent;
195+
}
196+
.d {
197+
border-block-start: none;
198+
}
199+
.c {
200+
border-top: none;
201+
}
202+
.b {
203+
border: none;
204+
}
205+
.a {
206+
all: unset;
207+
}
208+
}
209+
210+
.f:focus {
211+
display: block;
212+
}
213+
.e:hover {
214+
border-block-start-color: transparent;
215+
}
216+
.d:active {
217+
border-block-start: none;
218+
}
219+
.c[data-foo='bar'] {
220+
border-top: none;
221+
}
222+
.b[disabled] {
223+
border: none;
224+
}
225+
.a > .external {
226+
all: unset;
227+
}
228+
229+
.f {
230+
display: block;
231+
}
232+
.e {
233+
border-block-start-color: transparent;
234+
}
235+
.d {
236+
border-block-start: none;
237+
}
238+
.c {
239+
border-top: none;
240+
}
241+
.b {
242+
border: none;
243+
}
244+
.a {
245+
all: unset;
246+
}
247+
`);
248+
249+
expect(actual).toMatchInlineSnapshot(`
250+
"
251+
.a {
252+
all: unset;
253+
}
254+
.a > .external {
255+
all: unset;
256+
}
257+
.b[disabled] {
258+
border: none;
259+
}
260+
.c[data-foo='bar'] {
261+
border-top: none;
262+
}
263+
264+
.f {
265+
display: block;
266+
}
267+
.b {
268+
border: none;
269+
}
270+
.c {
271+
border-top: none;
272+
}
273+
.d {
274+
border-block-start: none;
275+
}
276+
.d:active {
277+
border-block-start: none;
278+
}
279+
.e {
280+
border-block-start-color: transparent;
281+
}
282+
283+
.f:focus {
284+
display: block;
285+
}
286+
.e:hover {
287+
border-block-start-color: transparent;
288+
}@media all {
289+
.a {
290+
all: unset;
291+
}
292+
.f {
293+
display: block;
294+
}
295+
.b {
296+
border: none;
297+
}
298+
.c {
299+
border-top: none;
300+
}
301+
.d {
302+
border-block-start: none;
303+
}
304+
.e {
305+
border-block-start-color: transparent;
306+
}
307+
}"
308+
`);
309+
});
310+
311+
it('sorts non-atomic classes inline, but only singular declaration rules against each other', () => {
312+
const actual = transform(outdent`
313+
.e { border-top: none; }
314+
.a {
315+
border-block-start: 1px solid;
316+
border-top: red;
317+
all: reset;
318+
border-block-start-color: transparent;
319+
border: 2px dashed;
320+
}
321+
.f { border-block-start-color: transparent; }
322+
.d { border: none; }
323+
.c { all: unset; }
324+
.b { all: unset; }
325+
`);
326+
327+
// WARNING: This may be wrong as `.a { … }` is not sorted as we expect, it _should_ be 'abcdef' not 'eabcdf'.
328+
// Is this a real scenario—multiple variables in a singular class?
329+
expect(actual).toMatchInlineSnapshot(`
330+
".e { border-top: none; }
331+
.a {
332+
all: reset;
333+
border: 2px dashed;
334+
border-top: red;
335+
border-block-start: 1px solid;
336+
border-block-start-color: transparent;
337+
}
338+
.b { all: unset; }
339+
.c { all: unset; }
340+
.d { border: none; }
341+
.f { border-block-start-color: transparent; }"
342+
`);
343+
});
344+
187345
describe('when disabled', () => {
188346
it('does nothing when crossover detected', () => {
189347
const actual = transformWithoutSorting(outdent`
Lines changed: 49 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,51 @@
1-
import type { ChildNode } from 'postcss';
2-
3-
// TODO: Would need a full list from https://developer.mozilla.org/en-US/docs/Web/CSS/Shorthand_properties
4-
// We _kind_ of have some of this in `expand-shorthands`, but only partially.
5-
const shorthandFor: { [key: string]: string[] } = {
6-
font: [
7-
'font-style',
8-
'font-variant',
9-
'font-weight',
10-
'font-stretch',
11-
'font-size',
12-
'line-height',
13-
'font-family',
14-
],
15-
outline: ['outline-color', 'outline-style', 'outline-width'],
1+
import { shorthandFor } from '@compiled/utils';
2+
import type { ChildNode, Declaration } from 'postcss';
3+
4+
const nodeIsDeclaration = (node: ChildNode): node is Declaration => node.type === 'decl';
5+
6+
const findDeclaration = (node: ChildNode): Declaration | Declaration[] | undefined => {
7+
if (nodeIsDeclaration(node)) {
8+
return node;
9+
}
10+
11+
if ('nodes' in node) {
12+
if (node.nodes.length === 1 && nodeIsDeclaration(node.nodes[0])) {
13+
return node.nodes[0];
14+
}
15+
16+
const declarations = node.nodes.map(findDeclaration).filter(Boolean) as Declaration[];
17+
18+
if (declarations.length === 1) {
19+
return declarations[0];
20+
}
21+
22+
return declarations;
23+
}
24+
};
25+
26+
const sortNodes = (a: ChildNode, b: ChildNode): number => {
27+
const aDecl = findDeclaration(a);
28+
const bDecl = findDeclaration(b);
29+
30+
// Don't worry about any array of declarations, this would be something like a group of AtRule versus a regular Rule
31+
// Those are sorted elsewhere…
32+
if (Array.isArray(aDecl) || Array.isArray(bDecl)) return 0;
33+
34+
if (!aDecl?.prop || !bDecl?.prop) return 0;
35+
36+
console.log('@@vs', aDecl.prop + ':' + aDecl.value, bDecl.prop + ':' + bDecl.value);
37+
38+
const aShorthand = shorthandFor[aDecl.prop];
39+
if (aShorthand === true || aShorthand?.includes(bDecl.prop)) {
40+
return -1;
41+
}
42+
43+
const bShorthand = shorthandFor[bDecl.prop];
44+
if (bShorthand === true || bShorthand?.includes(aDecl.prop)) {
45+
return 1;
46+
}
47+
48+
return 0;
1649
};
1750

1851
export const sortShorthandDeclarations = (nodes: ChildNode[]): void => {
@@ -25,18 +58,5 @@ export const sortShorthandDeclarations = (nodes: ChildNode[]): void => {
2558
}
2659
});
2760

28-
nodes.sort((a, b) => {
29-
if (a.type !== 'decl' || b.type !== 'decl') return 0;
30-
if (!a.prop || !b.prop) return 0;
31-
32-
if (shorthandFor[a.prop]?.includes(b.prop)) {
33-
return -1;
34-
}
35-
36-
if (shorthandFor[b.prop]?.includes(a.prop)) {
37-
return 1;
38-
}
39-
40-
return 0;
41-
});
61+
nodes.sort(sortNodes);
4262
};

packages/react/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
"jsx-dev-runtime"
7373
],
7474
"dependencies": {
75+
"@compiled/utils": "^0.11.1",
7576
"csstype": "^3.1.2"
7677
},
7778
"devDependencies": {

packages/react/src/runtime/__tests__/style-ssr.test.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,19 @@ describe('<Style />', () => {
4343
`._g1234567:visited{ color: grey; }`,
4444
`._h1234567:focus-visible{ color: white; }`,
4545
`._i1234567:focus-within{ color: black; }`,
46+
`._j1234567{ border: none; }`,
47+
`._j1234567{ border-top: 1px solid transparent; }`,
48+
`._j1234567{ border-top-color: red; }`,
49+
`._j1234567:focus{ border-top-color: transparent; }`,
50+
`._j1234567:focus{ border-color: blue; }`,
51+
`._j1234567:focus{ border: 1px solid; }`,
4652
]}
4753
</Style>
4854
);
4955

56+
// WARNING: This is wrong, the `focus` border properties are different than without focus, borders would be indeterministic.
5057
expect(result.split('</style>').join('</style>\n')).toMatchInlineSnapshot(`
51-
"<style data-cmpld="true">._c1234567{ display: block; }._d1234567:link{ color: green; }._g1234567:visited{ color: grey; }._i1234567:focus-within{ color: black; }._f1234567:focus{ color: pink; }._h1234567:focus-visible{ color: white; }._a1234567:hover{ color: red; }._b1234567:active{ color: blue; }@media (max-width: 800px){ ._e1234567{ color: yellow; } }</style>
58+
"<style data-cmpld="true">._j1234567{ border: none; }._j1234567{ border-top: 1px solid transparent; }._c1234567{ display: block; }._j1234567{ border-top-color: red; }._d1234567:link{ color: green; }._g1234567:visited{ color: grey; }._i1234567:focus-within{ color: black; }._f1234567:focus{ color: pink; }._j1234567:focus{ border-top-color: transparent; }._j1234567:focus{ border-color: blue; }._j1234567:focus{ border: 1px solid; }._h1234567:focus-visible{ color: white; }._a1234567:hover{ color: red; }._b1234567:active{ color: blue; }@media (max-width: 800px){ ._e1234567{ color: yellow; } }</style>
5259
"
5360
`);
5461
});

packages/react/src/runtime/sheet.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { getShorthandDepth } from '@compiled/utils';
2+
13
import { isCacheDisabled } from './cache';
24
import type { Bucket, StyleSheetOpts } from './types';
35

@@ -6,6 +8,11 @@ import type { Bucket, StyleSheetOpts } from './types';
68
* If changes are needed make sure that it aligns with the definition in `sort-at-rule-pseudos.tsx`.
79
*/
810
export const styleBucketOrdering: Bucket[] = [
11+
// shorthand properties
12+
's-root',
13+
's-1',
14+
's-2',
15+
's-3',
916
// catch-all
1017
'',
1118
// link
@@ -114,15 +121,26 @@ export const getStyleBucketName = (sheet: string): Bucket => {
114121
return 'm';
115122
}
116123

124+
const firstBracket = sheet.indexOf('{');
125+
117126
/**
118127
* We assume that classname will always be 9 character long,
119-
* using this the 10th character could be a pseudo declaration.
128+
* using this the 10th characters could be a pseudo declaration.
120129
*/
121130
if (sheet.charCodeAt(10) === 58 /* ":" */) {
122131
// We send through a subset of the string instead of the full pseudo name.
123132
// For example `"focus-visible"` name would instead of `"us-visible"`.
124133
// Return a mapped pseudo else the default catch all bucket.
125-
return pseudosMap[sheet.slice(14, sheet.indexOf('{'))] || '';
134+
const mapped = pseudosMap[sheet.slice(14, firstBracket)];
135+
if (mapped) return mapped;
136+
}
137+
138+
const property = sheet.slice(firstBracket + 1, sheet.indexOf(':', firstBracket)).trim();
139+
140+
const shorthandDepth = getShorthandDepth(property);
141+
if (shorthandDepth) {
142+
// NOTE: This doesn't actually work fully because there's various _layers_ of shorthands — up to 4 deep, I believe…
143+
return `s-${shorthandDepth}` as const;
126144
}
127145

128146
// Return default catch all bucket

packages/react/src/runtime/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ export interface StyleSheetOpts {
1111
* Buckets under which we will group our stylesheets
1212
*/
1313
export type Bucket =
14+
// shorthand properties
15+
| 's-root'
16+
| 's-1'
17+
| 's-2'
18+
| 's-3'
1419
// catch-all
1520
| ''
1621
// link

packages/react/tsconfig.browser.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@
1212
],
1313
"tsBuildInfoFile": "tsconfig.browser.tsbuildinfo"
1414
},
15-
"references": [{ "path": "../benchmark" }, { "path": "../jest" }]
15+
"references": [{ "path": "../benchmark" }, { "path": "../jest" }, { "path": "../utils" }]
1616
}

packages/react/tsconfig.cjs.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55
"outDir": "dist/cjs",
66
"tsBuildInfoFile": "tsconfig.cjs.tsbuildinfo"
77
},
8-
"references": [{ "path": "../benchmark" }, { "path": "../jest" }]
8+
"references": [{ "path": "../benchmark" }, { "path": "../jest" }, { "path": "../utils" }]
99
}

packages/react/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@
44
"outDir": "dist/esm",
55
"tsBuildInfoFile": "tsconfig.esm.tsbuildinfo"
66
},
7-
"references": [{ "path": "../benchmark" }, { "path": "../jest" }]
7+
"references": [{ "path": "../benchmark" }, { "path": "../jest" }, { "path": "../utils" }]
88
}

packages/utils/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ export { createError } from './error';
88
export { preserveLeadingComments } from './preserve-leading-comments';
99
export { INCREASE_SPECIFICITY_SELECTOR } from './increase-specificity';
1010
export { DEFAULT_PARSER_BABEL_PLUGINS } from './default-parser-babel-plugins';
11+
export { shorthandFor, getShorthandDepth } from './shorthand';

0 commit comments

Comments
 (0)