postcss-sort-media-queries 6.3.3 → 6.4.4

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.
Files changed (3) hide show
  1. package/README.md +88 -490
  2. package/package.json +6 -5
  3. package/src/index.js +137 -137
package/README.md CHANGED
@@ -14,494 +14,33 @@
14
14
 
15
15
  🌏 **English** ▫ [**O'zbek**](README-UZ.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
+
21
+ In addition, the plugin fully supports **CSS Media Queries Level 4**, including modern **range syntax**.
22
+
23
+ Sorting works based on [OlehDutchenko/sort-css-media-queries](https://github.com/OlehDutchenko/sort-css-media-queries).
20
24
 
21
25
  ## Table of Contents
22
26
 
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
27
  - [Install](#install)
29
28
  - [Usage](#usage)
30
29
  - [Options](#options)
31
30
  - [sort](#sort)
32
31
  - [Custom sort function](#custom-sort-function)
33
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)
34
37
  - [Changelog](#changelog)
35
38
  - [License](#license)
36
39
  - [Other PostCSS plugins](#other-postcss-plugins)
37
40
  - [Thanks 💪](#thanks)
38
41
 
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
- }
146
-
147
- .print-only-global-5 {
148
- display: block;
149
- }
150
-
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**
337
-
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
- ```
501
-
502
42
  ## Install
503
43
 
504
- First thing's, install the module:
505
44
 
506
45
  ```
507
46
  npm install postcss postcss-sort-media-queries --save-dev
@@ -528,9 +67,8 @@ If you already use PostCSS, add the plugin to plugins list:
528
67
  module.exports = {
529
68
  plugins: [
530
69
  + require('postcss-sort-media-queries')({
531
- + sort: 'mobile-first', // default value
70
+ + sort: 'mobile-first' | 'desktop-first' | function // default ('mobile-first')
532
71
  + }),
533
- require('autoprefixer')
534
72
  ]
535
73
  }
536
74
  ```
@@ -544,7 +82,6 @@ module.exports = {
544
82
  + // custom sorting function
545
83
  + }
546
84
  + }),
547
- require('autoprefixer')
548
85
  ]
549
86
  }
550
87
  ```
@@ -554,9 +91,9 @@ and set this plugin in settings.
554
91
 
555
92
  ## Options
556
93
 
557
- > Sorting works based on [OlehDutchenko/sort-css-media-queries](https://github.com/OlehDutchenko/sort-css-media-queries) function.
94
+ > Sorting works based on [OlehDutchenko/sort-css-media-queries](https://github.com/OlehDutchenko/sort-css-media-queries)
558
95
 
559
- ### sort
96
+ ### Sort
560
97
 
561
98
  This option support **string** or **function** values.
562
99
 
@@ -564,26 +101,15 @@ This option support **string** or **function** values.
564
101
  - `{string}` `'desktop-first'` - desktop first sorting
565
102
  - `{function}` your own sorting function
566
103
 
567
- #### `'mobile-first'`
568
-
569
- ```js
570
- postcss([
571
- sortMediaQueries({
572
- sort: 'mobile-first' // default
573
- })
574
- ]).process(css);
575
- ```
576
-
577
- #### `'desktop-first'`
104
+ #### `'mobile-first'` or `'desktop-first'`
578
105
 
579
106
  ```js
580
107
  postcss([
581
108
  sortMediaQueries({
582
- sort: 'desktop-first'
109
+ sort: 'mobile-first' | 'desktop-first' // default (mobile-first)
583
110
  })
584
111
  ]).process(css);
585
112
  ```
586
-
587
113
  ### Custom sort function
588
114
  ```js
589
115
  postcss([
@@ -615,6 +141,78 @@ postcss([
615
141
 
616
142
  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
143
 
144
+ ## Online demo
145
+
146
+ And here is the [Online Demo]
147
+
148
+ ## Examples
149
+
150
+ ### Mobile first sorting (with nested media queries)
151
+
152
+ #### Before
153
+
154
+ ```bash
155
+ root
156
+
157
+ ├── (min-width: 1400px)
158
+ ├── (min-width: 1200px)
159
+
160
+ └── @layer reset
161
+
162
+ ├── (min-width: 1200px)
163
+ │ ├── (min-width: 992px)
164
+ │ └── (min-width: 768px)
165
+
166
+ └── (min-width: 768px)
167
+ ├── (min-width: 640px)
168
+ └── (min-width: 320px)
169
+ ```
170
+
171
+ #### After
172
+
173
+ ```bash
174
+ root
175
+
176
+ ├── @layer reset
177
+ │ │
178
+ │ ├── (min-width: 768px)
179
+ │ │ ├── (min-width: 320px)
180
+ │ │ └── (min-width: 640px)
181
+ │ │
182
+ │ └── (min-width: 1200px)
183
+ │ ├── (min-width: 768px)
184
+ │ └── (min-width: 992px)
185
+
186
+ ├── (min-width: 1200px)
187
+ └── (min-width: 1400px)
188
+ ```
189
+
190
+ ### Desktop first sorting
191
+
192
+ **Before**
193
+ ```css
194
+ root
195
+
196
+ ├── (width < 640px)
197
+ ├── (min-width: 760px)
198
+ ├── (width < 640px)
199
+ ├── (min-width: 1280px)
200
+ ├── (max-width: 760px)
201
+ └── (max-width: 640px)
202
+ ```
203
+
204
+ **After**
205
+
206
+ ```bash
207
+ root
208
+
209
+ ├── (min-width: 760px)
210
+ ├── (max-width: 640px)
211
+ ├── (width < 640px)
212
+ ├── (max-width: 760px)
213
+ └── (min-width: 1280px)
214
+ ```
215
+
618
216
  ---
619
217
 
620
218
  ## Changelog
@@ -627,7 +225,7 @@ See [Releases history]
627
225
 
628
226
  ## Other PostCSS plugins
629
227
 
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
228
+ - [`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
229
 
632
230
  ## Thanks
633
231
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "postcss-sort-media-queries",
3
3
  "description": "PostCSS plugin for sorting and combining CSS media queries with mobile first / **desktop first methodologies",
4
- "version": "6.3.3",
4
+ "version": "6.4.4",
5
5
  "engines": {
6
6
  "node": ">=20.0.0"
7
7
  },
@@ -37,6 +37,7 @@
37
37
  "module": "src/index.js",
38
38
  "exports": {
39
39
  ".": {
40
+ "types": "./src/index.d.ts",
40
41
  "require": "./build/wrapper.cjs",
41
42
  "import": "./src/index.js",
42
43
  "default": "./src/index.js"
@@ -56,13 +57,13 @@
56
57
  },
57
58
  "devDependencies": {
58
59
  "@eslint/js": "^10.0.1",
59
- "@vitest/coverage-v8": "^4.1.0",
60
- "esbuild": "^0.27.4",
61
- "eslint": "^10.1.0",
60
+ "@vitest/coverage-v8": "^4.1.2",
61
+ "esbuild": "^0.28.0",
62
+ "eslint": "^10.2.0",
62
63
  "globals": "^17.4.0",
63
64
  "husky": "^9.1.7",
64
65
  "prettier": "3.8.1",
65
- "vitest": "^4.1.0"
66
+ "vitest": "^4.1.2"
66
67
  },
67
68
  "peerDependencies": {
68
69
  "nanoid": "^5.1.6",
package/src/index.js CHANGED
@@ -1,137 +1,137 @@
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 = 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;