postcss-sort-media-queries 6.3.3 → 6.5.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/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # PostCSS Sort Media Queries
2
2
 
3
- [PostCSS]: https://github.com/postcss/postcss
4
- [official docs]: https://github.com/postcss/postcss#usage
5
- [Online Demo]: https://yunusga.uz/postcss-sort-media-queries/
6
- [MIT]: https://github.com/yunusga/postcss-sort-media-queries/blob/master/LICENSE
3
+ [PostCSS]: https://github.com/postcss/postcss
4
+ [official docs]: https://github.com/postcss/postcss#usage
5
+ [Online Demo]: https://yunusga.uz/postcss-sort-media-queries/
6
+ [MIT]: https://github.com/yunusga/postcss-sort-media-queries/blob/master/LICENSE
7
7
  [Releases history]: https://github.com/yunusga/postcss-sort-media-queries/blob/master/CHANGELOG.md
8
8
 
9
9
  [![npm](https://img.shields.io/npm/v/postcss-sort-media-queries.svg)](https://www.npmjs.com/package/postcss-sort-media-queries) [![Node.js CI](https://github.com/yunusga/postcss-sort-media-queries/actions/workflows/test.yml/badge.svg?branch=main&event=status)](https://github.com/yunusga/postcss-sort-media-queries/actions/workflows/test.yml)
@@ -12,497 +12,35 @@
12
12
 
13
13
  <img src="logo.svg?sanitize=true" align="right" title="PostCSS sort media queries logotype" width="100" height="100">
14
14
 
15
- 🌏 **English** ▫ [**O'zbek**](README-UZ.md)
15
+ 🌏 **English** ▫ [**O'zbek**](README-UZ.md) ▫ [**简体中文**](README-ZH.md)
16
16
 
17
- [PostCSS] plugin for sorting and combining CSS media queries with **mobile first** / **desktop first** methodologies.
17
+ **PostcSS Sort Qedia Queries** is a powerful and flexible [PostCSS] plugin for sorting and combining CSS media queries using **mobile-first** or **desktop-first** methodologies. It helps maintain a clean, predictable stylesheet structure, improves readability, and prevents unexpected style overrides.
18
18
 
19
- > From v6.0.0 plugin supported nested media queries and ESM usage
19
+ A key advantage of this plugin is its **full support for nested media queries**, ensuring correct processing and ordering even in complex, deeply structured stylesheets. This makes it perfectly suited for modern CSS workflows that rely on nesting via PostCSS or preprocessor-like patterns.
20
20
 
21
- ## Table of Contents
22
-
23
- - [Online demo](#online-demo)
24
- - [Examples](#examples)
25
- - [Mobile first sorting](#mobile-first-sorting)
26
- - [Desktop first sorting](#desktop-first-sorting)
27
- - [Nested media queries sorting](#nested-media-queries-sorting)
28
- - [Install](#install)
29
- - [Usage](#usage)
30
- - [Options](#options)
31
- - [sort](#sort)
32
- - [Custom sort function](#custom-sort-function)
33
- - [Sort configuration](#sort-configuration)
34
- - [Changelog](#changelog)
35
- - [License](#license)
36
- - [Other PostCSS plugins](#other-postcss-plugins)
37
- - [Thanks 💪](#thanks)
38
-
39
- ## Online demo
40
-
41
- And here is the [Online Demo]
42
-
43
- ## Examples
44
-
45
- ### Mobile first sorting
46
-
47
- **Before**
48
-
49
- ```css
50
- @media (min-width: 1400px) {}
51
- @media (min-width: 1200px) {}
52
-
53
- @layer reset {
54
-
55
- @media (min-width: 1200px) {
56
- @media (min-width: 992px) {}
57
- @media (min-width: 768px) {}
58
- }
59
-
60
- @media (min-width: 768px) {
61
- @media (min-width: 640px) {}
62
- @media (min-width: 320px) {}
63
- }
64
- }
65
- ```
66
-
67
- **After**
68
-
69
- ```css
70
- @layer reset {
71
-
72
- @media (min-width: 768px) {
73
- @media (min-width: 320px) {}
74
- @media (min-width: 640px) {}
75
- }
76
-
77
- @media (min-width: 1200px) {
78
- @media (min-width: 768px) {}
79
- @media (min-width: 992px) {}
80
- }
81
- }
82
-
83
- @media (min-width: 1200px) {}
84
- @media (min-width: 1400px) {}
85
- ```
86
-
87
- ### Desktop first sorting
88
-
89
- **Before**
90
- ```css
91
- @media screen and (width < 640px) {
92
- .header { color: #cdcdcd }
93
- }
94
- @media screen and (min-width: 760px) {
95
- .desktop-first { color: #cdcdcd }
96
- }
97
- @media screen and (width < 640px) {
98
- .main { color: #cdcdcd }
99
- }
100
- @media screen and (min-width: 1280px) {
101
- .desktop-first { color: #cdcdcd }
102
- }
103
- @media screen and (max-width: 760px) {
104
- .footer { color: #cdcdcd }
105
- }
106
- @media screen and (max-width: 640px) {
107
- .footer { color: #cdcdcd }
108
- }
109
- ```
110
-
111
- **After**
112
-
113
- ```css
114
- @media screen and (max-width: 760px) {
115
- .footer { color: #cdcdcd }
116
- }
117
- @media screen and (width < 640px) {
118
- /* combined */
119
- .header { color: #cdcdcd }
120
- .main { color: #cdcdcd }
121
- .footer { color: #cdcdcd }
122
- }
123
- @media screen and (min-width: 760px) {
124
- .desktop-first { color: #cdcdcd }
125
- }
126
- @media screen and (min-width: 1280px) {
127
- .desktop-first { color: #cdcdcd }
128
- }
129
- ```
130
-
131
- ### Nested media queries sorting
132
-
133
- **Before**
134
-
135
- ```css
136
- @media (min-width: 710px) {
137
- .print-only-global-2 {
138
- display: block;
139
- }
140
- }
141
-
142
- @media (min-width: 1210px) {
143
- .print-only-global-4 {
144
- display: block;
145
- }
21
+ In addition, the plugin fully supports **CSS Media Queries Level 4**, including modern **range syntax**.
146
22
 
147
- .print-only-global-5 {
148
- display: block;
149
- }
23
+ Sorting works based on [OlehDutchenko/sort-css-media-queries](https://github.com/OlehDutchenko/sort-css-media-queries).
150
24
 
151
- .print-only-global-6 {
152
- display: block;
153
- }
154
- }
155
-
156
- @media (min-width: 310px) {
157
- .print-only-global-1 {
158
- display: block;
159
- }
160
- }
161
-
162
- @media (min-width: 710px) {
163
- .print-only-global-3 {
164
- display: block;
165
- }
166
- }
167
-
168
- @media (min-width: 1210px) {
169
- .print-only-global-7 {
170
- display: block;
171
- }
172
- }
173
-
174
- @media print {
175
- .print-only {
176
- display: block;
177
- }
178
-
179
- .print-only-parent-1 {
180
- display: block;
181
- }
182
-
183
- @media (orientation: landscape) {
184
- .print-only {
185
- color: black;
186
- }
187
-
188
- @media (min-width: 910px) {
189
- .nested-landscape-3 {
190
- color: black;
191
- }
192
-
193
- .nested-landscape-4 {
194
- color: black;
195
- }
196
-
197
- .nested-landscape-5 {
198
- color: black;
199
- }
200
- }
201
-
202
- @media (min-width: 810px) {
203
- .nested-landscape-2 {
204
- color: black;
205
- }
206
- }
207
-
208
- @media (min-width: 710px) {
209
- .nested-landscape-1 {
210
- color: black;
211
- }
212
- }
213
-
214
- .nested-landscape-0 {
215
- color: black;
216
- }
217
-
218
- @media (min-width: 710px) {
219
- .nested-landscape-4 {
220
- color: black;
221
- }
222
-
223
- .nested-landscape-5 {
224
- color: black;
225
- }
226
-
227
- .nested-landscape-6 {
228
- color: black;
229
- }
230
- }
231
-
232
- @media (min-width: 710px) {
233
- .nested-landscape-8 {
234
- color: black;
235
- }
236
-
237
- .nested-landscape-9 {
238
- color: black;
239
- }
240
- }
241
- }
242
-
243
- @media (min-width: 500px) {
244
- .print-only {
245
- color: black;
246
- }
247
- }
248
-
249
- .print-only-parent-2 {
250
- display: block;
251
- }
252
-
253
- @media (min-width: 500px) {
254
- .print-only {
255
- color: black;
256
- }
257
- }
258
-
259
- @media (orientation: portrait) {
260
- .print-only {
261
- color: gray;
262
- }
263
- }
264
-
265
- .print-only-parent-3 {
266
- display: block;
267
- }
268
-
269
- @media (min-width: 320px) {
270
- .print-only {
271
- color: black;
272
- }
273
- }
274
-
275
- @media (orientation: landscape) {
276
- .print-only-landscape-1-1 {
277
- color: gray;
278
- }
279
-
280
- .print-only-landscape-1-2 {
281
- color: gray;
282
- }
283
-
284
- .print-only-landscape-1-3 {
285
- color: gray;
286
- }
287
- }
288
- }
289
-
290
- @layer base {
291
-
292
- @media (min-width: 1220px) {
293
- .print-only-1200-1 {
294
- color: black;
295
- }
296
- }
297
-
298
- @media (min-width: 320px) {
299
- .print-only-320-1 {
300
- color: black;
301
- }
302
- }
303
-
304
- @media (min-width: 1220px) {
305
- .print-only-1200-2 {
306
- color: black;
307
- }
308
- }
309
-
310
- @media (min-width: 620px) {
311
- .print-only-640-1 {
312
- color: black;
313
- }
314
- }
315
-
316
- @media (min-width: 320px) {
317
- .print-only-320-2 {
318
- color: black;
319
- }
320
- }
321
-
322
- @media (min-width: 320px) {
323
- .print-only-320-3 {
324
- color: black;
325
- }
326
- }
327
-
328
- @media (min-width: 620px) {
329
- .print-only-640-2 {
330
- color: black;
331
- }
332
- }
333
- }
334
- ```
335
-
336
- **After**
25
+ ## Table of Contents
337
26
 
338
- ```css
339
- @layer base {
340
- @media (min-width: 320px) {
341
- .print-only-320-1 {
342
- color: black;
343
- }
344
- .print-only-320-2 {
345
- color: black;
346
- }
347
- .print-only-320-3 {
348
- color: black;
349
- }
350
- }
351
- @media (min-width: 620px) {
352
- .print-only-640-1 {
353
- color: black;
354
- }
355
- .print-only-640-2 {
356
- color: black;
357
- }
358
- }
359
- @media (min-width: 1220px) {
360
- .print-only-1200-1 {
361
- color: black;
362
- }
363
- .print-only-1200-2 {
364
- color: black;
365
- }
366
- }
367
- }
368
- @media (min-width: 310px) {
369
- .print-only-global-1 {
370
- display: block;
371
- }
372
- }
373
- @media (min-width: 710px) {
374
- .print-only-global-2 {
375
- display: block;
376
- }
377
- .print-only-global-3 {
378
- display: block;
379
- }
380
- }
381
- @media (min-width: 1210px) {
382
- .print-only-global-4 {
383
- display: block;
384
- }
385
-
386
- .print-only-global-5 {
387
- display: block;
388
- }
389
-
390
- .print-only-global-6 {
391
- display: block;
392
- }
393
- .print-only-global-7 {
394
- display: block;
395
- }
396
- }
397
- @media print {
398
- .print-only {
399
- display: block;
400
- }
401
-
402
- .print-only-parent-1 {
403
- display: block;
404
- }
405
-
406
- .print-only-parent-2 {
407
- display: block;
408
- }
409
-
410
- .print-only-parent-3 {
411
- display: block;
412
- }
413
-
414
- @media (min-width: 320px) {
415
- .print-only {
416
- color: black;
417
- }
418
- }
419
-
420
- @media (min-width: 500px) {
421
- .print-only {
422
- color: black;
423
- }
424
- .print-only {
425
- color: black;
426
- }
427
- }
428
-
429
- @media (orientation: landscape) {
430
- .print-only {
431
- color: black;
432
- }
433
-
434
- .nested-landscape-0 {
435
- color: black;
436
- }
437
- .print-only-landscape-1-1 {
438
- color: gray;
439
- }
440
-
441
- .print-only-landscape-1-2 {
442
- color: gray;
443
- }
444
-
445
- .print-only-landscape-1-3 {
446
- color: gray;
447
- }
448
-
449
- @media (min-width: 710px) {
450
- .nested-landscape-1 {
451
- color: black;
452
- }
453
- .nested-landscape-4 {
454
- color: black;
455
- }
456
-
457
- .nested-landscape-5 {
458
- color: black;
459
- }
460
-
461
- .nested-landscape-6 {
462
- color: black;
463
- }
464
- .nested-landscape-8 {
465
- color: black;
466
- }
467
-
468
- .nested-landscape-9 {
469
- color: black;
470
- }
471
- }
472
-
473
- @media (min-width: 810px) {
474
- .nested-landscape-2 {
475
- color: black;
476
- }
477
- }
478
-
479
- @media (min-width: 910px) {
480
- .nested-landscape-3 {
481
- color: black;
482
- }
483
-
484
- .nested-landscape-4 {
485
- color: black;
486
- }
487
-
488
- .nested-landscape-5 {
489
- color: black;
490
- }
491
- }
492
- }
493
-
494
- @media (orientation: portrait) {
495
- .print-only {
496
- color: gray;
497
- }
498
- }
499
- }
500
- ```
27
+ - [Install](#install)
28
+ - [Usage](#usage)
29
+ - [Options](#options)
30
+ - [sort](#sort)
31
+ - [Custom sort function](#custom-sort-function)
32
+ - [Sort configuration](#sort-configuration)
33
+ - [Online demo](#online-demo)
34
+ - [Examples (with nested media queries)](#examples)
35
+ - [Sorting options](#mobile-first-sorting)
36
+ - [Desktop first sorting](#desktop-first-sorting)
37
+ - [Changelog](#changelog)
38
+ - [License](#license)
39
+ - [Other PostCSS plugins](#other-postcss-plugins)
40
+ - [Thanks 💪](#thanks)
501
41
 
502
42
  ## Install
503
43
 
504
- First thing's, install the module:
505
-
506
44
  ```
507
45
  npm install postcss postcss-sort-media-queries --save-dev
508
46
  ```
@@ -513,13 +51,12 @@ Check you project for existed PostCSS config: `postcss.config.js`
513
51
  in the project root, `"postcss"` section in `package.json`
514
52
  or `postcss` in bundle config.
515
53
 
516
-
517
54
  ```js
518
55
  // CJS
519
- let sortCssMq = require('postcss-sort-media-queries');
56
+ let sortCssMq = require("postcss-sort-media-queries");
520
57
 
521
58
  // ESM
522
- import sortCssMq from 'postcss-sort-media-queries';
59
+ import sortCssMq from "postcss-sort-media-queries";
523
60
  ```
524
61
 
525
62
  If you already use PostCSS, add the plugin to plugins list:
@@ -528,14 +65,14 @@ If you already use PostCSS, add the plugin to plugins list:
528
65
  module.exports = {
529
66
  plugins: [
530
67
  + require('postcss-sort-media-queries')({
531
- + sort: 'mobile-first', // default value
68
+ + sort: 'mobile-first' | 'desktop-first' | function // default ('mobile-first')
532
69
  + }),
533
- require('autoprefixer')
534
70
  ]
535
71
  }
536
72
  ```
537
73
 
538
74
  or with custom sort function
75
+
539
76
  ```diff
540
77
  module.exports = {
541
78
  plugins: [
@@ -544,7 +81,6 @@ module.exports = {
544
81
  + // custom sorting function
545
82
  + }
546
83
  + }),
547
- require('autoprefixer')
548
84
  ]
549
85
  }
550
86
  ```
@@ -554,9 +90,9 @@ and set this plugin in settings.
554
90
 
555
91
  ## Options
556
92
 
557
- > Sorting works based on [OlehDutchenko/sort-css-media-queries](https://github.com/OlehDutchenko/sort-css-media-queries) function.
93
+ > Sorting works based on [OlehDutchenko/sort-css-media-queries](https://github.com/OlehDutchenko/sort-css-media-queries)
558
94
 
559
- ### sort
95
+ ### Sort
560
96
 
561
97
  This option support **string** or **function** values.
562
98
 
@@ -564,34 +100,25 @@ This option support **string** or **function** values.
564
100
  - `{string}` `'desktop-first'` - desktop first sorting
565
101
  - `{function}` your own sorting function
566
102
 
567
- #### `'mobile-first'`
568
-
569
- ```js
570
- postcss([
571
- sortMediaQueries({
572
- sort: 'mobile-first' // default
573
- })
574
- ]).process(css);
575
- ```
576
-
577
- #### `'desktop-first'`
103
+ #### `'mobile-first'` or `'desktop-first'`
578
104
 
579
105
  ```js
580
106
  postcss([
581
107
  sortMediaQueries({
582
- sort: 'desktop-first'
583
- })
108
+ sort: "mobile-first" | "desktop-first", // default (mobile-first)
109
+ }),
584
110
  ]).process(css);
585
111
  ```
586
112
 
587
113
  ### Custom sort function
114
+
588
115
  ```js
589
116
  postcss([
590
117
  sortMediaQueries({
591
118
  function(a, b) {
592
119
  return a.localeCompare(b);
593
- }
594
- })
120
+ },
121
+ }),
595
122
  ]).process(css);
596
123
  ```
597
124
 
@@ -608,13 +135,86 @@ postcss([
608
135
  sortMediaQueries({
609
136
  configuration: {
610
137
  unitlessMqAlwaysFirst: true, // or false
611
- }
612
- })
138
+ },
139
+ }),
613
140
  ]).process(css);
614
141
  ```
615
142
 
616
143
  Or alternatively create a `sort-css-mq.config.json` file in the root of your project. Or add property `sortCssMQ: {}` in your `package.json`.
617
144
 
145
+ ## Online demo
146
+
147
+ And here is the [Online Demo]
148
+
149
+ ## Examples
150
+
151
+ ### Mobile first sorting (with nested media queries)
152
+
153
+ #### Before
154
+
155
+ ```bash
156
+ root
157
+
158
+ ├── (min-width: 1400px)
159
+ ├── (min-width: 1200px)
160
+
161
+ └── @layer reset
162
+
163
+ ├── (min-width: 1200px)
164
+ │ ├── (min-width: 992px)
165
+ │ └── (min-width: 768px)
166
+
167
+ └── (min-width: 768px)
168
+ ├── (min-width: 640px)
169
+ └── (min-width: 320px)
170
+ ```
171
+
172
+ #### After
173
+
174
+ ```bash
175
+ root
176
+
177
+ ├── @layer reset
178
+ │ │
179
+ │ ├── (min-width: 768px)
180
+ │ │ ├── (min-width: 320px)
181
+ │ │ └── (min-width: 640px)
182
+ │ │
183
+ │ └── (min-width: 1200px)
184
+ │ ├── (min-width: 768px)
185
+ │ └── (min-width: 992px)
186
+
187
+ ├── (min-width: 1200px)
188
+ └── (min-width: 1400px)
189
+ ```
190
+
191
+ ### Desktop first sorting
192
+
193
+ **Before**
194
+
195
+ ```css
196
+ root
197
+
198
+ ├── (width < 640px)
199
+ ├── (min-width: 760px)
200
+ ├── (width < 640px)
201
+ ├── (min-width: 1280px)
202
+ ├── (max-width: 760px)
203
+ └── (max-width: 640px)
204
+ ```
205
+
206
+ **After**
207
+
208
+ ```bash
209
+ root
210
+
211
+ ├── (min-width: 760px)
212
+ ├── (max-width: 640px)
213
+ ├── (width < 640px)
214
+ ├── (max-width: 760px)
215
+ └── (min-width: 1280px)
216
+ ```
217
+
618
218
  ---
619
219
 
620
220
  ## Changelog
@@ -627,7 +227,7 @@ See [Releases history]
627
227
 
628
228
  ## Other PostCSS plugins
629
229
 
630
- - [`postcss-momentum-scrolling`](https://github.com/solversgroup/postcss-momentum-scrolling) - plugin for adding **momentum** style scrolling behavior (`-webkit-overflow-scrolling:touch`) for elements with overflow (scroll, auto) on iOS
230
+ - [`postcss-momentum-scrolling`](https://github.com/solversgroup/postcss-momentum-scrolling) - plugin for adding **momentum** style scrolling behavior (`-webkit-overflow-scrolling:touch`) for elements with overflow (scroll, auto) on iOS (**deprecated for modern Safari**)
631
231
 
632
232
  ## Thanks
633
233
 
@@ -639,3 +239,4 @@ See [Releases history]
639
239
  - Khayot Razzakov [@Khayotbek1](https://github.com/Khayotbek1)
640
240
  - ReindDooyeweerd [@ReindDooyeweerd](https://github.com/ReindDooyeweerd)
641
241
  - msev [@msev](https://github.com/msev)
242
+ - ajiho [@ajiho](https://github.com/ajiho)
@@ -334,9 +334,11 @@ function plugin(options = {}) {
334
334
  atRule.remove();
335
335
  });
336
336
  if (atRules) {
337
- sortAtRules(Object.keys(atRules), options, sortCSSmq).forEach((query) => {
338
- parent.append(atRules[query]);
339
- });
337
+ sortAtRules(Object.keys(atRules), options, sortCSSmq).forEach(
338
+ (query) => {
339
+ parent.append(atRules[query]);
340
+ }
341
+ );
340
342
  }
341
343
  });
342
344
  root.walkAtRules("media", (parent) => {
@@ -0,0 +1,17 @@
1
+ import { Plugin } from "postcss";
2
+
3
+ type SortOption =
4
+ | "mobile-first"
5
+ | "desktop-first"
6
+ | ((a: string, b: string) => number);
7
+
8
+ interface Options {
9
+ sort?: SortOption;
10
+ configuration?: {
11
+ unitlessMqAlwaysFirst?: boolean;
12
+ };
13
+ }
14
+
15
+ declare const plugin: (options?: Options) => Plugin;
16
+
17
+ export default plugin;
package/dist/index.js ADDED
@@ -0,0 +1,139 @@
1
+ import createSort from "sort-css-media-queries/create-sort";
2
+ import { nanoid } from "nanoid";
3
+
4
+ // PostCSS plugin to sort CSS @media rules according to a configurable order.
5
+ // The plugin groups top-level and nested media at-rules, merges rules
6
+ // with identical queries, and re-inserts them in the desired order.
7
+
8
+ // Helper that ensures `options.sort` is a function and sorts queries.
9
+ function sortAtRules(queries, options, sortCSSmq) {
10
+ if (typeof options.sort !== "function") {
11
+ options.sort =
12
+ options.sort === "desktop-first" ? sortCSSmq.desktopFirst : sortCSSmq;
13
+ }
14
+
15
+ return queries.sort(options.sort);
16
+ }
17
+
18
+ function getDepth(node) {
19
+ let depth = 0;
20
+
21
+ for (let p = node.parent; p; p = p.parent) {
22
+ depth++;
23
+ }
24
+
25
+ return depth;
26
+ }
27
+
28
+ function plugin(options = {}) {
29
+ // Set default options and allow user overrides
30
+ options = Object.assign(
31
+ {
32
+ sort: "mobile-first",
33
+ configuration: false,
34
+ },
35
+ options,
36
+ );
37
+
38
+ // Create a sorter based on configuration (from sort-css-media-queries)
39
+ const sortCSSmq = createSort(options.configuration);
40
+
41
+ return {
42
+ postcssPlugin: "postcss-sort-media-queries",
43
+
44
+ // Execute once after the entire tree has been parsed
45
+ OnceExit(root, { AtRule }) {
46
+ // Collect parent nodes that contain media at-rules. We separate
47
+ // top-level (`root`) parents from nested parents so ordering
48
+ // semantics can be preserved independently.
49
+ let parents = [];
50
+
51
+ // Walk all @media at-rules and group their parents
52
+ root.walkAtRules("media", (atRule) => {
53
+ if (!atRule.parent.groupId) {
54
+ let groupId = nanoid();
55
+
56
+ atRule.parent.groupId = groupId;
57
+
58
+ parents[groupId] = {
59
+ parent: atRule.parent,
60
+ depth: getDepth(atRule.parent),
61
+ };
62
+ }
63
+
64
+ return;
65
+ });
66
+
67
+ if (!parents) {
68
+ return;
69
+ }
70
+
71
+ parents = Object.fromEntries(
72
+ Object.entries(parents).sort(([, a], [, b]) => {
73
+ return b.depth - a.depth;
74
+ }),
75
+ );
76
+
77
+ Object.keys(parents).forEach((groupId) => {
78
+ let { parent } = parents[groupId];
79
+
80
+ // Filter only @media nodes from the parent's children
81
+ let medias = parent.nodes.filter(
82
+ (node) => node.type === "atrule" && node.name === "media",
83
+ );
84
+
85
+ if (!medias) {
86
+ return;
87
+ }
88
+
89
+ let atRules = [];
90
+
91
+ medias.forEach((atRule) => {
92
+ if (!atRules[atRule.params]) {
93
+ atRules[atRule.params] = new AtRule({
94
+ name: atRule.name,
95
+ params: atRule.params,
96
+ source: atRule.source,
97
+ });
98
+ }
99
+
100
+ [...atRule.nodes].forEach((node) => {
101
+ atRules[atRule.params].append(node);
102
+ });
103
+
104
+ // Remove the original at-rule since its contents have been
105
+ // merged into `atRules[atRule.params]`.
106
+ atRule.remove();
107
+ });
108
+
109
+ // Sort query keys and append merged at-rules back to the parent
110
+ if (atRules) {
111
+ sortAtRules(Object.keys(atRules), options, sortCSSmq).forEach(
112
+ (query) => {
113
+ parent.append(atRules[query]);
114
+ },
115
+ );
116
+ }
117
+ });
118
+
119
+ root.walkAtRules("media", (parent) => {
120
+ // Filter only @media nodes from the parent's children
121
+ let medias = parent.nodes.filter(
122
+ (node) => node.type === "atrule" && node.name === "media",
123
+ );
124
+
125
+ if (!medias) {
126
+ return;
127
+ }
128
+
129
+ medias.forEach((atRule) => {
130
+ parent.append(atRule);
131
+ });
132
+ });
133
+ },
134
+ };
135
+ }
136
+
137
+ plugin.postcss = true;
138
+
139
+ export default plugin;
@@ -1,6 +1,6 @@
1
- 'use strict';
1
+ "use strict";
2
2
 
3
- const mod = require('./index.cjs');
3
+ const mod = require("./index.cjs");
4
4
 
5
5
  const plugin = mod.default;
6
6
 
package/package.json CHANGED
@@ -1,74 +1,77 @@
1
- {
2
- "name": "postcss-sort-media-queries",
3
- "description": "PostCSS plugin for sorting and combining CSS media queries with mobile first / **desktop first methodologies",
4
- "version": "6.3.3",
5
- "engines": {
6
- "node": ">=20.0.0"
7
- },
8
- "keywords": [
9
- "postcss",
10
- "postcss-plugin",
11
- "css",
12
- "css-optimizations",
13
- "sort",
14
- "mobile-first",
15
- "desktop-first",
16
- "mediaquery",
17
- "media-queries",
18
- "mq",
19
- "responsive-css",
20
- "combine-media-query",
21
- "sort-media-query",
22
- "css-mqpacker",
23
- "node-css-mqpacker"
24
- ],
25
- "author": "Yunus Gaziyev <yunusga@yandex.ru>",
26
- "license": "MIT",
27
- "repository": {
28
- "type": "git",
29
- "url": "https://github.com/yunusga/postcss-sort-media-queries.git"
30
- },
31
- "bugs": {
32
- "url": "https://github.com/yunusga/postcss-sort-media-queries/issues"
33
- },
34
- "homepage": "https://yunusga.uz/postcss-sort-media-queries",
35
- "type": "module",
36
- "main": "src/index.js",
37
- "module": "src/index.js",
38
- "exports": {
39
- ".": {
40
- "require": "./build/wrapper.cjs",
41
- "import": "./src/index.js",
42
- "default": "./src/index.js"
43
- }
44
- },
45
- "files": [
46
- "build"
47
- ],
48
- "scripts": {
49
- "test": "npm run build && vitest run",
50
- "test:watch": "vitest --watch",
51
- "coverage": "vitest --coverage",
52
- "lint": "eslint",
53
- "prepare": "husky",
54
- "build": "npm run build:cjs",
55
- "build:cjs": "esbuild src/index.js --outfile=build/index.cjs --format=cjs --bundle --platform=node --external:postcss"
56
- },
57
- "devDependencies": {
58
- "@eslint/js": "^10.0.1",
59
- "@vitest/coverage-v8": "^4.1.0",
60
- "esbuild": "^0.27.4",
61
- "eslint": "^10.1.0",
62
- "globals": "^17.4.0",
63
- "husky": "^9.1.7",
64
- "prettier": "3.8.1",
65
- "vitest": "^4.1.0"
66
- },
67
- "peerDependencies": {
68
- "nanoid": "^5.1.6",
69
- "postcss": "^8.5.6"
70
- },
71
- "dependencies": {
72
- "sort-css-media-queries": "^3.0.3"
73
- }
74
- }
1
+ {
2
+ "name": "postcss-sort-media-queries",
3
+ "description": "PostCSS plugin for sorting and combining CSS media queries with mobile first / **desktop first methodologies",
4
+ "version": "6.5.0",
5
+ "engines": {
6
+ "node": ">=20.0.0"
7
+ },
8
+ "keywords": [
9
+ "postcss",
10
+ "postcss-plugin",
11
+ "css",
12
+ "css-optimizations",
13
+ "sort",
14
+ "mobile-first",
15
+ "desktop-first",
16
+ "mediaquery",
17
+ "media-queries",
18
+ "mq",
19
+ "responsive-css",
20
+ "combine-media-query",
21
+ "sort-media-query",
22
+ "css-mqpacker",
23
+ "node-css-mqpacker"
24
+ ],
25
+ "author": "Yunus Gaziyev <yunusga@yandex.ru>",
26
+ "license": "MIT",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/yunusga/postcss-sort-media-queries.git"
30
+ },
31
+ "bugs": {
32
+ "url": "https://github.com/yunusga/postcss-sort-media-queries/issues"
33
+ },
34
+ "homepage": "https://yunusga.uz/postcss-sort-media-queries",
35
+ "type": "module",
36
+ "main": "src/index.js",
37
+ "module": "src/index.js",
38
+ "exports": {
39
+ ".": {
40
+ "types": "./dist/index.d.ts",
41
+ "require": "./dist/wrapper.cjs",
42
+ "import": "./dist/index.js",
43
+ "default": "./dist/index.js"
44
+ }
45
+ },
46
+ "files": [
47
+ "dist"
48
+ ],
49
+ "scripts": {
50
+ "test": "npm run build && vitest run",
51
+ "test:watch": "vitest --watch",
52
+ "coverage": "vitest --coverage",
53
+ "lint": "eslint",
54
+ "format": "prettier --write .",
55
+ "prepare": "husky",
56
+ "build": "npm run build:cjs && cpy \"src/**/*\" dist",
57
+ "build:cjs": "esbuild src/index.js --outfile=dist/index.cjs --format=cjs --bundle --platform=node --external:postcss"
58
+ },
59
+ "devDependencies": {
60
+ "@eslint/js": "^10.0.1",
61
+ "@vitest/coverage-v8": "^4.1.2",
62
+ "cpy-cli": "^7.0.0",
63
+ "esbuild": "^0.28.0",
64
+ "eslint": "^10.2.0",
65
+ "globals": "^17.4.0",
66
+ "husky": "^9.1.7",
67
+ "prettier": "3.8.1",
68
+ "vitest": "^4.1.2"
69
+ },
70
+ "peerDependencies": {
71
+ "nanoid": "^5.1.6",
72
+ "postcss": "^8.5.6"
73
+ },
74
+ "dependencies": {
75
+ "sort-css-media-queries": "^3.0.5"
76
+ }
77
+ }
package/src/index.js CHANGED
@@ -1,137 +1,139 @@
1
- import createSort from 'sort-css-media-queries/create-sort';
2
- import { nanoid } from 'nanoid';
3
-
4
- // PostCSS plugin to sort CSS @media rules according to a configurable order.
5
- // The plugin groups top-level and nested media at-rules, merges rules
6
- // with identical queries, and re-inserts them in the desired order.
7
-
8
- // Helper that ensures `options.sort` is a function and sorts queries.
9
- function sortAtRules(queries, options, sortCSSmq) {
10
- if (typeof options.sort !== 'function') {
11
- options.sort = options.sort === 'desktop-first' ? sortCSSmq.desktopFirst : sortCSSmq;
12
- }
13
-
14
- return queries.sort(options.sort);
15
- }
16
-
17
- function getDepth(node) {
18
- let depth = 0;
19
-
20
- for (let p = node.parent; p; p = p.parent) {
21
- depth++;
22
- }
23
-
24
- return depth;
25
- }
26
-
27
- function plugin(options = {}) {
28
-
29
- // Set default options and allow user overrides
30
- options = Object.assign(
31
- {
32
- sort: 'mobile-first',
33
- configuration: false,
34
- },
35
- options
36
- );
37
-
38
- // Create a sorter based on configuration (from sort-css-media-queries)
39
- const sortCSSmq = createSort(options.configuration);
40
-
41
- return {
42
- postcssPlugin: 'postcss-sort-media-queries',
43
-
44
- // Execute once after the entire tree has been parsed
45
- OnceExit(root, { AtRule }) {
46
- // Collect parent nodes that contain media at-rules. We separate
47
- // top-level (`root`) parents from nested parents so ordering
48
- // semantics can be preserved independently.
49
- let parents = [];
50
-
51
- // Walk all @media at-rules and group their parents
52
- root.walkAtRules('media', (atRule) => {
53
- if (!atRule.parent.groupId) {
54
- let groupId = nanoid();
55
-
56
- atRule.parent.groupId = groupId;
57
-
58
- parents[groupId] = {
59
- parent: atRule.parent,
60
- depth: getDepth(atRule.parent),
61
- }
62
- }
63
-
64
- return;
65
- });
66
-
67
- if (!parents) {
68
- return;
69
- }
70
-
71
- parents = Object.fromEntries(
72
- Object.entries(parents).sort(([, a], [, b]) => {
73
- return b.depth - a.depth;
74
- })
75
- );
76
-
77
- Object.keys(parents).forEach((groupId) => {
78
- let { parent } = parents[groupId];
79
-
80
- // Filter only @media nodes from the parent's children
81
- let medias = parent.nodes.filter(
82
- (node) => node.type === 'atrule' && node.name === 'media'
83
- );
84
-
85
- if (!medias) {
86
- return;
87
- }
88
-
89
- let atRules = [];
90
-
91
- medias.forEach((atRule) => {
92
- if (!atRules[atRule.params]) {
93
- atRules[atRule.params] = new AtRule({
94
- name: atRule.name,
95
- params: atRule.params,
96
- source: atRule.source,
97
- });
98
- }
99
-
100
- [...atRule.nodes].forEach((node) => {
101
- atRules[atRule.params].append(node);
102
- });
103
-
104
- // Remove the original at-rule since its contents have been
105
- // merged into `atRules[atRule.params]`.
106
- atRule.remove();
107
- });
108
-
109
- // Sort query keys and append merged at-rules back to the parent
110
- if (atRules) {
111
- sortAtRules(Object.keys(atRules), options, sortCSSmq).forEach((query) => {
112
- parent.append(atRules[query]);
113
- });
114
- }
115
- });
116
-
117
- root.walkAtRules('media', (parent) => {
118
- // Filter only @media nodes from the parent's children
119
- let medias = parent.nodes.filter(
120
- (node) => node.type === 'atrule' && node.name === 'media'
121
- );
122
-
123
- if (!medias) {
124
- return;
125
- }
126
-
127
- medias.forEach((atRule) => {
128
- parent.append(atRule);
129
- });
130
- });
131
- }
132
- };
133
- }
134
-
135
- plugin.postcss = true;
136
-
137
- export default plugin;
1
+ import createSort from "sort-css-media-queries/create-sort";
2
+ import { nanoid } from "nanoid";
3
+
4
+ // PostCSS plugin to sort CSS @media rules according to a configurable order.
5
+ // The plugin groups top-level and nested media at-rules, merges rules
6
+ // with identical queries, and re-inserts them in the desired order.
7
+
8
+ // Helper that ensures `options.sort` is a function and sorts queries.
9
+ function sortAtRules(queries, options, sortCSSmq) {
10
+ if (typeof options.sort !== "function") {
11
+ options.sort =
12
+ options.sort === "desktop-first" ? sortCSSmq.desktopFirst : sortCSSmq;
13
+ }
14
+
15
+ return queries.sort(options.sort);
16
+ }
17
+
18
+ function getDepth(node) {
19
+ let depth = 0;
20
+
21
+ for (let p = node.parent; p; p = p.parent) {
22
+ depth++;
23
+ }
24
+
25
+ return depth;
26
+ }
27
+
28
+ function plugin(options = {}) {
29
+ // Set default options and allow user overrides
30
+ options = Object.assign(
31
+ {
32
+ sort: "mobile-first",
33
+ configuration: false,
34
+ },
35
+ options,
36
+ );
37
+
38
+ // Create a sorter based on configuration (from sort-css-media-queries)
39
+ const sortCSSmq = createSort(options.configuration);
40
+
41
+ return {
42
+ postcssPlugin: "postcss-sort-media-queries",
43
+
44
+ // Execute once after the entire tree has been parsed
45
+ OnceExit(root, { AtRule }) {
46
+ // Collect parent nodes that contain media at-rules. We separate
47
+ // top-level (`root`) parents from nested parents so ordering
48
+ // semantics can be preserved independently.
49
+ let parents = [];
50
+
51
+ // Walk all @media at-rules and group their parents
52
+ root.walkAtRules("media", (atRule) => {
53
+ if (!atRule.parent.groupId) {
54
+ let groupId = nanoid();
55
+
56
+ atRule.parent.groupId = groupId;
57
+
58
+ parents[groupId] = {
59
+ parent: atRule.parent,
60
+ depth: getDepth(atRule.parent),
61
+ };
62
+ }
63
+
64
+ return;
65
+ });
66
+
67
+ if (!parents) {
68
+ return;
69
+ }
70
+
71
+ parents = Object.fromEntries(
72
+ Object.entries(parents).sort(([, a], [, b]) => {
73
+ return b.depth - a.depth;
74
+ }),
75
+ );
76
+
77
+ Object.keys(parents).forEach((groupId) => {
78
+ let { parent } = parents[groupId];
79
+
80
+ // Filter only @media nodes from the parent's children
81
+ let medias = parent.nodes.filter(
82
+ (node) => node.type === "atrule" && node.name === "media",
83
+ );
84
+
85
+ if (!medias) {
86
+ return;
87
+ }
88
+
89
+ let atRules = [];
90
+
91
+ medias.forEach((atRule) => {
92
+ if (!atRules[atRule.params]) {
93
+ atRules[atRule.params] = new AtRule({
94
+ name: atRule.name,
95
+ params: atRule.params,
96
+ source: atRule.source,
97
+ });
98
+ }
99
+
100
+ [...atRule.nodes].forEach((node) => {
101
+ atRules[atRule.params].append(node);
102
+ });
103
+
104
+ // Remove the original at-rule since its contents have been
105
+ // merged into `atRules[atRule.params]`.
106
+ atRule.remove();
107
+ });
108
+
109
+ // Sort query keys and append merged at-rules back to the parent
110
+ if (atRules) {
111
+ sortAtRules(Object.keys(atRules), options, sortCSSmq).forEach(
112
+ (query) => {
113
+ parent.append(atRules[query]);
114
+ },
115
+ );
116
+ }
117
+ });
118
+
119
+ root.walkAtRules("media", (parent) => {
120
+ // Filter only @media nodes from the parent's children
121
+ let medias = parent.nodes.filter(
122
+ (node) => node.type === "atrule" && node.name === "media",
123
+ );
124
+
125
+ if (!medias) {
126
+ return;
127
+ }
128
+
129
+ medias.forEach((atRule) => {
130
+ parent.append(atRule);
131
+ });
132
+ });
133
+ },
134
+ };
135
+ }
136
+
137
+ plugin.postcss = true;
138
+
139
+ export default plugin;