@karlhillx/css-rules-sorter 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Karl Hill <karlhillx@gmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # CSS Rules Sorter
2
+
3
+ ✨ A PostCSS plugin to automatically sort CSS selectors and media queries for cleaner, more maintainable stylesheets.
4
+
5
+ [![npm version](https://badge.fury.io/js/@karlhillx/css-rules-sorter.svg)](https://badge.fury.io/js/@karlhillx/css-rules-sorter)
6
+ [![MIT License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
7
+ [![PostCSS 8+](https://img.shields.io/badge/PostCSS-8%2B-787CB5)](https://github.com/postcss/postcss)
8
+ [![GitHub Actions CI](https://github.com/karlhillx/@karlhillx/css-rules-sorter/actions/workflows/ci.yml/badge.svg)](https://github.com/karlhillx/@karlhillx/css-rules-sorter/actions/workflows/ci.yml)
9
+ [![Jest Tests](https://img.shields.io/badge/Tests-Jest-8854d6)](https://github.com/karlhillx/@karlhillx/css-rules-sorter/tree/main/test)
10
+
11
+ ## Key Features
12
+
13
+ - **Alphabetical Selector Sorting**: Automatically sorts top-level selectors and rules within media queries for consistent code.
14
+ - **Intelligent Media Query Management**: Flexible ordering (mobile-first or desktop-first) and grouping using `postcss-sort-media-queries`.
15
+ - **Advanced Methodologies**: Built-in support for BEM-aware sorting.
16
+ - **Cascade Layer Support**: Automatically reorders `@layer` blocks to match your defined priority.
17
+ - **Modern PostCSS Support**: Fully compatible with the latest PostCSS 8+ API.
18
+ - **Developer-Focused**: Includes TypeScript definitions, comprehensive tests, and linting.
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ npm install @karlhillx/css-rules-sorter postcss --save-dev
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ Add `@karlhillx/css-rules-sorter` to your PostCSS configuration (e.g., `postcss.config.js`):
29
+
30
+ ```javascript
31
+ // postcss.config.js
32
+ module.exports = {
33
+ plugins: [
34
+ require('@karlhillx/css-rules-sorter')({
35
+ sort: 'mobile-first', // or 'desktop-first'
36
+ selectorSort: 'natural', // or 'bem' or 'specificity'
37
+ propertyShorthand: 'expand', // or 'collapse'
38
+ }),
39
+ ],
40
+ };
41
+ ```
42
+
43
+ ## Configuration Options
44
+
45
+ | Option | Type | Default | Description |
46
+ | ------------------ | --------- | ---------------- | ------------------------------------------------------------------------------ |
47
+ | `sort` | `string` | `'mobile-first'` | Sets the media query order: `'mobile-first'` or `'desktop-first'`. |
48
+ | `selectorSort` | `string` | `'natural'` | Defines the method for sorting selectors: `'natural'`, `'specificity'`, or `'bem'`. |
49
+ | `propertySort` | `string` | `'none'` | Sorts properties within each rule: `'none'` or `'alphabetical'`. |
50
+ | `propertyShorthand`| `string` | `'none'` | Manages shorthand properties: `'none'`, `'expand'`, or `'collapse'`. |
51
+ | `sortLayers` | `boolean` | `true` | Sorts CSS cascade layers based on the first `@layer` definition. |
52
+ | `groupByMediaType` | `boolean` | `true` | Groups media queries by their type (e.g., `screen`, `print`). |
53
+
54
+ ## Advanced Features
55
+
56
+ ### BEM Selector Sorting
57
+
58
+ Set `selectorSort: 'bem'` to sort selectors based on the Block, Element, Modifier (BEM) methodology. This ensures your component structures stay logically grouped.
59
+
60
+ **Before:**
61
+ ```css
62
+ .card__header--small { font-size: 0.8em; }
63
+ .card--featured { border: 1px solid blue; }
64
+ .card { color: black; }
65
+ .card__header { font-weight: bold; }
66
+ ```
67
+
68
+ **After:**
69
+ ```css
70
+ .card { color: black; }
71
+ .card--featured { border: 1px solid blue; }
72
+ .card__header { font-weight: bold; }
73
+ .card__header--small { font-size: 0.8em; }
74
+ ```
75
+
76
+ ### Shorthand Property Management
77
+
78
+ Use `propertyShorthand` to enforce a specific style for `margin` and `padding`.
79
+
80
+ - **`'expand'`**: Breaks down shorthands like `margin: 10px 20px;` into four longhand properties. Excellent for preventing accidental overrides.
81
+ - **`'collapse'`**: Combines longhand properties into a single shorthand when all four sides are present.
82
+
83
+ ### Cascade Layer Sorting
84
+
85
+ Set `sortLayers: true` to automatically reorder your `@layer` blocks to match the priority defined in your first `@layer` rule.
86
+
87
+ **Before:**
88
+ ```css
89
+ @layer components, base, reset;
90
+
91
+ @layer base { body { color: red; } }
92
+ @layer reset { * { margin: 0; } }
93
+ @layer components { .btn { padding: 1em; } }
94
+ ```
95
+
96
+ **After:**
97
+ ```css
98
+ @layer reset { * { margin: 0; } }
99
+ @layer base { body { color: red; } }
100
+ @layer components { .btn { padding: 1em; } }
101
+ ```
102
+
103
+ ## Development
104
+
105
+ ```bash
106
+ # Install all dependencies
107
+ npm install
108
+
109
+ # Run tests
110
+ npm test
111
+
112
+ # Lint code
113
+ npm run lint
114
+
115
+ # Format code
116
+ npm run format
117
+ ```
118
+
119
+ ## Contributing
120
+
121
+ 1. Fork the repository.
122
+ 2. Create a new feature branch (`git checkout -b feature/your-feature`).
123
+ 3. Commit your changes (`git commit -m 'Add your feature'`).
124
+ 4. Push to the branch (`git push origin feature/your-feature`).
125
+ 5. Create a Pull Request.
126
+
127
+ ## License
128
+
129
+ MIT License. See [LICENSE](LICENSE) for details.
package/index.js ADDED
@@ -0,0 +1,286 @@
1
+ const sortMediaQueries = require('postcss-sort-media-queries');
2
+
3
+ /**
4
+ * Parses a BEM selector into its parts.
5
+ * .block__element--modifier
6
+ */
7
+ function parseBEM(selector) {
8
+ // Support both .block__element and .block--modifier and .block__element--modifier
9
+ // and simple .block
10
+ const match = selector.match(/^\.([a-zA-Z0-9-]+?)(?:__([a-zA-Z0-9-]+?))?(?:--([a-zA-Z0-9-]+?))?$/);
11
+ if (!match) return { block: selector, element: '', modifier: '', original: selector };
12
+ return {
13
+ block: match[1],
14
+ element: match[2] || '',
15
+ modifier: match[3] || '',
16
+ original: selector
17
+ };
18
+ }
19
+
20
+ /**
21
+ * Compare two BEM selectors.
22
+ */
23
+ function compareBEM(a, b) {
24
+ const bemA = parseBEM(a);
25
+ const bemB = parseBEM(b);
26
+
27
+ if (bemA.block !== bemB.block) {
28
+ return bemA.block.localeCompare(bemB.block);
29
+ }
30
+
31
+ // Same block.
32
+ // Desired order:
33
+ // 1. Block itself (.card)
34
+ // 2. Block modifiers (.card--featured)
35
+ // 3. Elements (.card__header)
36
+ // 4. Element modifiers (.card__header--active)
37
+
38
+ // Case 1: Both are the base block
39
+ if (!bemA.element && !bemA.modifier && !bemB.element && !bemB.modifier) return 0;
40
+
41
+ // Case 2: One is base block
42
+ if (!bemA.element && !bemA.modifier) return -1;
43
+ if (!bemB.element && !bemB.modifier) return 1;
44
+
45
+ // Case 3: Both are block modifiers (no element)
46
+ if (bemA.modifier && !bemA.element && bemB.modifier && !bemB.element) {
47
+ return bemA.modifier.localeCompare(bemB.modifier);
48
+ }
49
+
50
+ // Case 4: One is block modifier, other is element (or element modifier)
51
+ if (bemA.modifier && !bemA.element && bemB.element) return -1;
52
+ if (bemB.modifier && !bemB.element && bemA.element) return 1;
53
+
54
+ // Case 5: Both are elements
55
+ if (bemA.element && bemB.element) {
56
+ if (bemA.element !== bemB.element) {
57
+ return bemA.element.localeCompare(bemB.element);
58
+ }
59
+ // Same element, check modifiers
60
+ if (!bemA.modifier && bemB.modifier) return -1;
61
+ if (bemA.modifier && !bemB.modifier) return 1;
62
+ return (bemA.modifier || '').localeCompare(bemB.modifier || '');
63
+ }
64
+
65
+ return a.localeCompare(b);
66
+ }
67
+
68
+ /**
69
+ * Expand shorthand properties.
70
+ */
71
+ function expandShorthand(decl) {
72
+ if (decl.prop === 'border') {
73
+ const values = decl.value.split(/\s+/);
74
+ const width = values.find(v => v.match(/^\d|thin|medium|thick/)) || 'medium';
75
+ const style = values.find(v => v.match(/none|hidden|dotted|dashed|solid|double|groove|ridge|inset|outset/)) || 'none';
76
+ const color = values.find(v => !v.match(/^\d|thin|medium|thick|none|hidden|dotted|dashed|solid|double|groove|ridge|inset|outset/)) || 'black';
77
+
78
+ decl.cloneBefore({ prop: 'border-width', value: width });
79
+ decl.cloneBefore({ prop: 'border-style', value: style });
80
+ decl.cloneBefore({ prop: 'border-color', value: color });
81
+ decl.remove();
82
+ } else if (decl.prop === 'margin' || decl.prop === 'padding') {
83
+ const values = decl.value.split(/\s+/);
84
+ let top, right, bottom, left;
85
+
86
+ if (values.length === 1) {
87
+ top = right = bottom = left = values[0];
88
+ } else if (values.length === 2) {
89
+ top = bottom = values[0];
90
+ right = left = values[1];
91
+ } else if (values.length === 3) {
92
+ top = values[0];
93
+ right = left = values[1];
94
+ bottom = values[2];
95
+ } else {
96
+ top = values[0];
97
+ right = values[1];
98
+ bottom = values[2];
99
+ left = values[3];
100
+ }
101
+
102
+ decl.cloneBefore({ prop: `${decl.prop}-top`, value: top });
103
+ decl.cloneBefore({ prop: `${decl.prop}-right`, value: right });
104
+ decl.cloneBefore({ prop: `${decl.prop}-bottom`, value: bottom });
105
+ decl.cloneBefore({ prop: `${decl.prop}-left`, value: left });
106
+ decl.remove();
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Collapse longhand properties.
112
+ */
113
+ function collapseLonghands(rule) {
114
+ const properties = {};
115
+ rule.walkDecls(decl => {
116
+ properties[decl.prop] = decl;
117
+ });
118
+
119
+ // Border collapse
120
+ if (properties['border-width'] && properties['border-style'] && properties['border-color']) {
121
+ properties['border-width'].cloneBefore({
122
+ prop: 'border',
123
+ value: `${properties['border-width'].value} ${properties['border-style'].value} ${properties['border-color'].value}`
124
+ });
125
+ properties['border-width'].remove();
126
+ properties['border-style'].remove();
127
+ properties['border-color'].remove();
128
+ }
129
+
130
+ // Margin/Padding collapse
131
+ ['margin', 'padding'].forEach(type => {
132
+ const top = properties[`${type}-top`];
133
+ const right = properties[`${type}-right`];
134
+ const bottom = properties[`${type}-bottom`];
135
+ const left = properties[`${type}-left`];
136
+
137
+ if (top && right && bottom && left) {
138
+ let value;
139
+ if (top.value === right.value && top.value === bottom.value && top.value === left.value) {
140
+ value = top.value;
141
+ } else if (top.value === bottom.value && right.value === left.value) {
142
+ value = `${top.value} ${right.value}`;
143
+ } else if (right.value === left.value) {
144
+ value = `${top.value} ${right.value} ${bottom.value}`;
145
+ } else {
146
+ value = `${top.value} ${right.value} ${bottom.value} ${left.value}`;
147
+ }
148
+
149
+ top.cloneBefore({ prop: type, value });
150
+ top.remove();
151
+ right.remove();
152
+ bottom.remove();
153
+ left.remove();
154
+ }
155
+ });
156
+ }
157
+
158
+ const specificity = (selector) => {
159
+ const selectorWithoutPseudoElements = selector.replace(/::?[a-zA-Z-]+/g, '');
160
+ const ids = (selectorWithoutPseudoElements.match(/#/g) || []).length;
161
+ const classesAndAttributes = (selectorWithoutPseudoElements.match(/\.|\[/g) || []).length;
162
+ const elements = (selectorWithoutPseudoElements.match(/[a-zA-Z-]+\b(?!#|\.)/g) || []).length;
163
+ return ids * 100 + classesAndAttributes * 10 + elements;
164
+ };
165
+
166
+ /**
167
+ * PostCSS plugin to sort CSS rules
168
+ * @param {Object} opts
169
+ * @returns {import('postcss').Plugin}
170
+ */
171
+ const cssRulesSorterPlugin = (opts = {}) => {
172
+ const defaultOptions = {
173
+ sort: 'mobile-first',
174
+ selectorSort: 'natural',
175
+ propertySort: 'natural',
176
+ propertyShorthand: 'none',
177
+ sortLayers: false,
178
+ groupByMediaType: true,
179
+ };
180
+
181
+ const config = { ...defaultOptions, ...opts };
182
+
183
+ return {
184
+ postcssPlugin: 'css-rules-sorter',
185
+ Once(root) {
186
+ // 0. Layer sorting
187
+ if (config.sortLayers) {
188
+ const layerOrderAtRule = root.nodes.find(node => node.type === 'atrule' && node.name === 'layer' && node.params.includes(','));
189
+ if (layerOrderAtRule) {
190
+ const order = layerOrderAtRule.params.split(',').map(s => s.trim());
191
+ const layerBlocks = root.nodes.filter(node => node.type === 'atrule' && node.name === 'layer' && !node.params.includes(','));
192
+
193
+ const sortedLayers = [...layerBlocks].sort((a, b) => {
194
+ return order.indexOf(a.params.trim()) - order.indexOf(b.params.trim());
195
+ });
196
+
197
+ // Re-insert in order after the definition
198
+ let lastNode = layerOrderAtRule;
199
+ sortedLayers.forEach(layer => {
200
+ const clone = layer.clone();
201
+ layer.remove();
202
+ lastNode.after(clone);
203
+ lastNode = clone;
204
+ });
205
+ }
206
+ }
207
+
208
+ // 1. Property Management
209
+ root.walkRules(rule => {
210
+ if (config.propertyShorthand === 'expand') {
211
+ rule.walkDecls(expandShorthand);
212
+ } else if (config.propertyShorthand === 'collapse') {
213
+ collapseLonghands(rule);
214
+ }
215
+
216
+ // Property Sorting
217
+ if (config.propertySort === 'natural') {
218
+ const decls = rule.nodes.filter(node => node.type === 'decl');
219
+ if (decls.length > 1) {
220
+ const sorted = [...decls].sort((a, b) => a.prop.localeCompare(b.prop));
221
+ decls.forEach((decl, idx) => {
222
+ decl.replaceWith(sorted[idx].clone());
223
+ });
224
+ }
225
+ }
226
+ });
227
+
228
+ const sortSelectors = (a, b) => {
229
+ if (config.selectorSort === 'bem') {
230
+ return compareBEM(a.selector, b.selector);
231
+ }
232
+ if (config.selectorSort === 'specificity') {
233
+ return specificity(a.selector) - specificity(b.selector);
234
+ }
235
+ return a.selector.toLowerCase().localeCompare(b.selector.toLowerCase());
236
+ };
237
+
238
+ // 2. Sort rules within media queries
239
+ root.walkAtRules('media', (atRule) => {
240
+ const rules = atRule.nodes.filter((node) => node.type === 'rule');
241
+ if (rules.length === 0) return;
242
+
243
+ const sorted = [...rules].sort(sortSelectors);
244
+
245
+ // Replace rules in place
246
+ rules.forEach((rule, idx) => {
247
+ rule.replaceWith(sorted[idx].clone());
248
+ });
249
+ });
250
+
251
+ // 3. Sort top-level rules
252
+ const topRules = root.nodes.filter((node) => node.type === 'rule');
253
+
254
+ if (topRules.length > 0) {
255
+ const sortedTopRules = [...topRules].sort(sortSelectors);
256
+
257
+ topRules.forEach((rule, idx) => {
258
+ rule.replaceWith(sortedTopRules[idx].clone());
259
+ });
260
+ }
261
+ },
262
+ async OnceExit(root, { postcss }) {
263
+ await postcss([sortMediaQueries(config)]).process(root, { from: undefined });
264
+ },
265
+ };
266
+ };
267
+
268
+ cssRulesSorterPlugin.postcss = true;
269
+
270
+ /**
271
+ * Main export as a function that can also be used as a standalone processor
272
+ */
273
+ function main(opts = {}) {
274
+ const plugin = cssRulesSorterPlugin(opts);
275
+
276
+ // Attach process method for standalone usage (backward compatibility)
277
+ plugin.process = async (css) => {
278
+ const postcss = require('postcss');
279
+ const result = await postcss([plugin]).process(css, { from: undefined });
280
+ return result.css;
281
+ };
282
+
283
+ return plugin;
284
+ }
285
+
286
+ module.exports = main;
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@karlhillx/css-rules-sorter",
3
+ "version": "1.1.0",
4
+ "description": "PostCSS-based CSS rules sorter that alphabetically sorts selectors and media queries",
5
+ "main": "index.js",
6
+ "types": "index.d.ts",
7
+ "files": [
8
+ "index.js",
9
+ "README.md",
10
+ "LICENSE",
11
+ "test/"
12
+ ],
13
+ "scripts": {
14
+ "test": "jest --coverage",
15
+ "lint": "eslint .",
16
+ "format": "prettier --write .",
17
+ "prepare": "npm test"
18
+ },
19
+ "keywords": [
20
+ "css",
21
+ "postcss",
22
+ "sorter",
23
+ "css-sorter",
24
+ "stylesheet"
25
+ ],
26
+ "author": "Karl Hill",
27
+ "license": "MIT",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/karlhillx/css-rules-sorter.git"
31
+ },
32
+ "dependencies": {
33
+ "postcss": "^8.4.0",
34
+ "postcss-sort-media-queries": "^4.0.0"
35
+ },
36
+ "devDependencies": {
37
+ "@eslint/js": "^10.0.1",
38
+ "eslint": "^10.0.3",
39
+ "eslint-config-prettier": "^10.1.8",
40
+ "eslint-plugin-jest": "^29.15.0",
41
+ "globals": "^17.4.0",
42
+ "jest": "^29.0.0",
43
+ "prettier": "^3.8.1"
44
+ }
45
+ }
@@ -0,0 +1,122 @@
1
+ const cssRulesSorter = require('../index');
2
+ const postcss = require('postcss');
3
+
4
+ async function run(input, opts = {}) {
5
+ const result = await postcss([cssRulesSorter(opts)]).process(input, { from: undefined });
6
+ return result.css;
7
+ }
8
+
9
+ describe('css-rules-sorter', () => {
10
+ test('works with standalone process method', async () => {
11
+ const input = `.z { color: 0; } .a { color: 1; }`;
12
+ const sorter = cssRulesSorter();
13
+ const output = await sorter.process(input);
14
+ expect(output).toMatch(/\.a[\s\S]*\.z/);
15
+ });
16
+
17
+ describe('Basic Sorting', () => {
18
+ test('sorts top-level selectors alphabetically', async () => {
19
+ const input = `
20
+ .zebra { color: black; }
21
+ .apple { color: red; }
22
+ .banana { color: yellow; }
23
+ `;
24
+ const output = await run(input);
25
+ expect(output).toMatch(/\.apple[\s\S]*\.banana[\s\S]*\.zebra/);
26
+ });
27
+
28
+ test('sorts selectors within media queries alphabetically', async () => {
29
+ const input = `
30
+ @media (min-width: 768px) {
31
+ .zebra { color: black; }
32
+ .apple { color: red; }
33
+ }
34
+ `;
35
+ const output = await run(input);
36
+ expect(output).toMatch(/@media[\s\S]*\.apple[\s\S]*\.zebra/);
37
+ });
38
+
39
+ test('organizes media queries (mobile-first by default)', async () => {
40
+ const input = `
41
+ @media (min-width: 1024px) { .large { color: blue; } }
42
+ @media (min-width: 768px) { .medium { color: green; } }
43
+ .top { color: red; }
44
+ `;
45
+ const output = await run(input);
46
+ const mediumIndex = output.indexOf('min-width: 768px');
47
+ const largeIndex = output.indexOf('min-width: 1024px');
48
+ expect(mediumIndex).toBeLessThan(largeIndex);
49
+ });
50
+ });
51
+
52
+ describe('Architectural Features', () => {
53
+ test('BEM sorting groups modifiers and elements correctly', async () => {
54
+ const input = `
55
+ .card__header { color: grey; }
56
+ .card--featured { color: gold; }
57
+ .card { color: black; }
58
+ `;
59
+ const output = await run(input, { selectorSort: 'bem' });
60
+ const card = output.indexOf('.card {');
61
+ const featured = output.indexOf('.card--featured');
62
+ const header = output.indexOf('.card__header');
63
+ expect(card).toBeLessThan(featured);
64
+ expect(featured).toBeLessThan(header);
65
+ });
66
+
67
+ test('Specificity sorting orders low to high', async () => {
68
+ const input = `
69
+ #id { color: red; }
70
+ .class { color: blue; }
71
+ element { color: green; }
72
+ `;
73
+ const output = await run(input, { selectorSort: 'specificity' });
74
+ const el = output.indexOf('element {');
75
+ const cl = output.indexOf('.class {');
76
+ const id = output.indexOf('#id {');
77
+ expect(el).toBeLessThan(cl);
78
+ expect(cl).toBeLessThan(id);
79
+ });
80
+
81
+ test('Shorthand expansion (expand)', async () => {
82
+ const input = `.box { margin: 10px 20px; }`;
83
+ const output = await run(input, { propertyShorthand: 'expand' });
84
+ expect(output).toContain('margin-top: 10px');
85
+ expect(output).toContain('margin-right: 20px');
86
+ expect(output).toContain('margin-bottom: 10px');
87
+ expect(output).toContain('margin-left: 20px');
88
+ });
89
+
90
+ test('Shorthand collapsing (collapse)', async () => {
91
+ const input = `.box { margin-top: 10px; margin-right: 20px; margin-bottom: 10px; margin-left: 20px; }`;
92
+ const output = await run(input, { propertyShorthand: 'collapse' });
93
+ expect(output).toContain('margin: 10px 20px');
94
+ });
95
+
96
+ test('Cascade Layer reordering', async () => {
97
+ const input = `
98
+ @layer components, reset, base;
99
+ @layer base { body { margin: 0; } }
100
+ @layer reset { * { box-sizing: border-box; } }
101
+ @layer components { .card { padding: 1rem; } }
102
+ `;
103
+ const output = await run(input, { sortLayers: true });
104
+ const comp = output.indexOf('@layer components {');
105
+ const reset = output.indexOf('@layer reset {');
106
+ const base = output.indexOf('@layer base {');
107
+ expect(comp).toBeLessThan(reset);
108
+ expect(reset).toBeLessThan(base);
109
+ });
110
+
111
+ test('Property sorting alphabetically by default', async () => {
112
+ const input = `.box { z-index: 10; color: red; background: blue; }`;
113
+ const output = await run(input);
114
+ const background = output.indexOf('background: blue');
115
+ const color = output.indexOf('color: red');
116
+ const zIndex = output.indexOf('z-index: 10');
117
+
118
+ expect(background).toBeLessThan(color);
119
+ expect(color).toBeLessThan(zIndex);
120
+ });
121
+ });
122
+ });