postcss-sort-media-queries 6.1.0 → 6.3.2

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
@@ -24,13 +24,13 @@
24
24
  - [Examples](#examples)
25
25
  - [Mobile first sorting](#mobile-first-sorting)
26
26
  - [Desktop first sorting](#desktop-first-sorting)
27
+ - [Nested media queries sorting](#nested-media-queries-sorting)
27
28
  - [Install](#install)
28
29
  - [Usage](#usage)
29
30
  - [Options](#options)
30
31
  - [sort](#sort)
31
32
  - [Custom sort function](#custom-sort-function)
32
33
  - [Sort configuration](#sort-configuration)
33
- - [Only Top Level](#only-top-level)
34
34
  - [Changelog](#changelog)
35
35
  - [License](#license)
36
36
  - [Other PostCSS plugins](#other-postcss-plugins)
@@ -128,6 +128,377 @@ And here is the [Online Demo]
128
128
  }
129
129
  ```
130
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
+
131
502
  ## Install
132
503
 
133
504
  First thing's, install the module:
@@ -261,8 +632,10 @@ See [Releases history]
261
632
  ## Thanks
262
633
 
263
634
  - Andrey Sitnik [@ai](https://github.com/ai)
264
- - Oleh Dutchenko [@dutchenkoOleg](https://github.com/OlehDutchenko)
635
+ - Oleh Dutchenko [@OlehDutchenko](https://github.com/OlehDutchenko)
265
636
  - Jakub Caban [@Lustmored](https://github.com/Lustmored)
266
637
  - Dmytro Symonov [@Kassaila](https://github.com/Kassaila)
267
638
  - Kai Falkowski [@SassNinja](https://github.com/SassNinja)
268
639
  - Khayot Razzakov [@Khayotbek1](https://github.com/Khayotbek1)
640
+ - ReindDooyeweerd [@ReindDooyeweerd](https://github.com/ReindDooyeweerd)
641
+ - msev [@msev](https://github.com/msev)
package/build/index.cjs CHANGED
@@ -234,6 +234,36 @@ function createSort(configuration) {
234
234
  return sortCSSmq;
235
235
  }
236
236
 
237
+ // node_modules/nanoid/index.js
238
+ var import_node_crypto = require("node:crypto");
239
+
240
+ // node_modules/nanoid/url-alphabet/index.js
241
+ var urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
242
+
243
+ // node_modules/nanoid/index.js
244
+ var POOL_SIZE_MULTIPLIER = 128;
245
+ var pool;
246
+ var poolOffset;
247
+ function fillPool(bytes) {
248
+ if (!pool || pool.length < bytes) {
249
+ pool = Buffer.allocUnsafe(bytes * POOL_SIZE_MULTIPLIER);
250
+ import_node_crypto.webcrypto.getRandomValues(pool);
251
+ poolOffset = 0;
252
+ } else if (poolOffset + bytes > pool.length) {
253
+ import_node_crypto.webcrypto.getRandomValues(pool);
254
+ poolOffset = 0;
255
+ }
256
+ poolOffset += bytes;
257
+ }
258
+ function nanoid(size = 21) {
259
+ fillPool(size |= 0);
260
+ let id = "";
261
+ for (let i = poolOffset - size; i < poolOffset; i++) {
262
+ id += urlAlphabet[pool[i] & 63];
263
+ }
264
+ return id;
265
+ }
266
+
237
267
  // src/index.js
238
268
  function sortAtRules(queries, options, sortCSSmq) {
239
269
  if (typeof options.sort !== "function") {
@@ -241,6 +271,13 @@ function sortAtRules(queries, options, sortCSSmq) {
241
271
  }
242
272
  return queries.sort(options.sort);
243
273
  }
274
+ function getDepth(node) {
275
+ let depth = 0;
276
+ for (let p = node.parent; p; p = p.parent) {
277
+ depth++;
278
+ }
279
+ return depth;
280
+ }
244
281
  function plugin(options = {}) {
245
282
  options = Object.assign(
246
283
  {
@@ -254,55 +291,63 @@ function plugin(options = {}) {
254
291
  postcssPlugin: "postcss-sort-media-queries",
255
292
  // Execute once after the entire tree has been parsed
256
293
  OnceExit(root, { AtRule }) {
257
- let parents = {
258
- root: [],
259
- nested: []
260
- };
261
- let processed = /* @__PURE__ */ Symbol("processed");
294
+ let parents = [];
262
295
  root.walkAtRules("media", (atRule) => {
263
- if (atRule.parent[processed]) {
264
- return;
265
- }
266
- if (atRule.parent.type === "root") {
267
- parents.root.push(atRule.parent);
268
- }
269
- if (atRule.parent.type !== "root") {
270
- parents.nested.push(atRule.parent);
296
+ if (!atRule.parent.groupId) {
297
+ let groupId = nanoid();
298
+ atRule.parent.groupId = groupId;
299
+ parents[groupId] = {
300
+ parent: atRule.parent,
301
+ depth: getDepth(atRule.parent)
302
+ };
271
303
  }
272
- atRule.parent[processed] = true;
273
304
  return;
274
305
  });
275
- Object.keys(parents).forEach((type) => {
276
- if (!parents[type].length) {
306
+ if (!parents) {
307
+ return;
308
+ }
309
+ parents = Object.fromEntries(
310
+ Object.entries(parents).sort(([, a], [, b]) => {
311
+ return b.depth - a.depth;
312
+ })
313
+ );
314
+ Object.keys(parents).forEach((groupId) => {
315
+ let { parent } = parents[groupId];
316
+ let medias = parent.nodes.filter(
317
+ (node) => node.type === "atrule" && node.name === "media"
318
+ );
319
+ if (!medias) {
277
320
  return;
278
321
  }
279
- parents[type].forEach((parent) => {
280
- let media = parent.nodes.filter(
281
- (n) => n.type === "atrule" && n.name === "media"
282
- );
283
- if (!media) {
284
- return;
285
- }
286
- let atRules = [];
287
- media.forEach((atRule) => {
288
- let query = atRule.params;
289
- if (!atRules[query]) {
290
- atRules[query] = new AtRule({
291
- name: atRule.name,
292
- params: atRule.params,
293
- source: atRule.source
294
- });
295
- }
296
- atRule.nodes.forEach((node) => {
297
- atRules[query].append(node);
298
- });
299
- atRule.remove();
300
- });
301
- if (atRules) {
302
- sortAtRules(Object.keys(atRules), options, sortCSSmq).forEach((query) => {
303
- parent.append(atRules[query]);
322
+ let atRules = [];
323
+ medias.forEach((atRule) => {
324
+ if (!atRules[atRule.params]) {
325
+ atRules[atRule.params] = new AtRule({
326
+ name: atRule.name,
327
+ params: atRule.params,
328
+ source: atRule.source
304
329
  });
305
330
  }
331
+ [...atRule.nodes].forEach((node) => {
332
+ atRules[atRule.params].append(node);
333
+ });
334
+ atRule.remove();
335
+ });
336
+ if (atRules) {
337
+ sortAtRules(Object.keys(atRules), options, sortCSSmq).forEach((query) => {
338
+ parent.append(atRules[query]);
339
+ });
340
+ }
341
+ });
342
+ root.walkAtRules("media", (parent) => {
343
+ let medias = parent.nodes.filter(
344
+ (node) => node.type === "atrule" && node.name === "media"
345
+ );
346
+ if (!medias) {
347
+ return;
348
+ }
349
+ medias.forEach((atRule) => {
350
+ parent.append(atRule);
306
351
  });
307
352
  });
308
353
  }
package/build/wrapper.cjs CHANGED
@@ -1,9 +1,9 @@
1
- 'use strict';
2
-
3
- const mod = require('./index.cjs');
4
-
5
- const plugin = mod.default;
6
-
7
- plugin.postcss = mod.postcss;
8
-
9
- module.exports = plugin;
1
+ 'use strict';
2
+
3
+ const mod = require('./index.cjs');
4
+
5
+ const plugin = mod.default;
6
+
7
+ plugin.postcss = mod.postcss;
8
+
9
+ module.exports = plugin;
package/package.json CHANGED
@@ -1,9 +1,9 @@
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.1.0",
4
+ "version": "6.3.2",
5
5
  "engines": {
6
- "node": ">=22.0.0"
6
+ "node": ">=20.0.0"
7
7
  },
8
8
  "keywords": [
9
9
  "postcss",
@@ -31,7 +31,7 @@
31
31
  "bugs": {
32
32
  "url": "https://github.com/yunusga/postcss-sort-media-queries/issues"
33
33
  },
34
- "homepage": "https://github.com/yunusga/postcss-sort-media-queries",
34
+ "homepage": "https://yunusga.uz/postcss-sort-media-queries",
35
35
  "type": "module",
36
36
  "main": "src/index.js",
37
37
  "module": "src/index.js",
@@ -46,20 +46,26 @@
46
46
  "build"
47
47
  ],
48
48
  "scripts": {
49
- "test": "vitest run",
49
+ "test": "npm run build && vitest run",
50
50
  "test:watch": "vitest --watch",
51
51
  "coverage": "vitest --coverage",
52
+ "lint": "eslint",
53
+ "prepare": "husky",
52
54
  "build": "npm run build:cjs",
53
55
  "build:cjs": "esbuild src/index.js --outfile=build/index.cjs --format=cjs --bundle --platform=node --external:postcss"
54
56
  },
55
57
  "devDependencies": {
58
+ "@eslint/js": "^10.0.1",
56
59
  "@vitest/coverage-v8": "^4.0.18",
57
60
  "esbuild": "^0.27.3",
58
- "postcss": "^8.5.6",
61
+ "eslint": "^10.0.3",
62
+ "globals": "^17.4.0",
63
+ "husky": "^9.1.7",
59
64
  "prettier": "3.8.1",
60
65
  "vitest": "^4.0.18"
61
66
  },
62
67
  "peerDependencies": {
68
+ "nanoid": "^5.1.6",
63
69
  "postcss": "^8.5.6"
64
70
  },
65
71
  "dependencies": {
package/src/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import createSort from 'sort-css-media-queries/create-sort';
2
+ import { nanoid } from 'nanoid';
2
3
 
3
4
  // PostCSS plugin to sort CSS @media rules according to a configurable order.
4
5
  // The plugin groups top-level and nested media at-rules, merges rules
@@ -13,6 +14,16 @@ function sortAtRules(queries, options, sortCSSmq) {
13
14
  return queries.sort(options.sort);
14
15
  }
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
+
16
27
  function plugin(options = {}) {
17
28
 
18
29
  // Set default options and allow user overrides
@@ -35,82 +46,86 @@ function plugin(options = {}) {
35
46
  // Collect parent nodes that contain media at-rules. We separate
36
47
  // top-level (`root`) parents from nested parents so ordering
37
48
  // semantics can be preserved independently.
38
- let parents = {
39
- root: [],
40
- nested: [],
41
- };
42
-
43
- // Symbol used to mark parents that we've already collected
44
- let processed = Symbol('processed');
49
+ let parents = [];
45
50
 
46
51
  // Walk all @media at-rules and group their parents
47
52
  root.walkAtRules('media', (atRule) => {
48
- if (atRule.parent[processed]) {
49
- return;
50
- }
53
+ if (!atRule.parent.groupId) {
54
+ let groupId = nanoid();
51
55
 
52
- // If the parent is the root of the document, add to root list
53
- if (atRule.parent.type === 'root') {
54
- parents.root.push(atRule.parent);
55
- }
56
+ atRule.parent.groupId = groupId;
56
57
 
57
- // Otherwise treat it as a nested parent
58
- if (atRule.parent.type !== 'root') {
59
- parents.nested.push(atRule.parent);
58
+ parents[groupId] = {
59
+ parent: atRule.parent,
60
+ depth: getDepth(atRule.parent),
61
+ }
60
62
  }
61
63
 
62
- // Mark this parent so we don't collect it twice
63
- atRule.parent[processed] = true;
64
-
65
64
  return;
66
65
  });
67
66
 
68
- // For each parent group, merge and sort its media at-rules
69
- Object.keys(parents).forEach((type) => {
70
- if (!parents[type].length) {
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) {
71
86
  return;
72
87
  }
73
88
 
74
- parents[type].forEach((parent) => {
75
- // Filter only @media nodes from the parent's children
76
- let media = parent.nodes.filter(
77
- n => n.type === 'atrule' && n.name === 'media'
78
- );
89
+ let atRules = [];
79
90
 
80
- if (!media) {
81
- return;
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
+ });
82
98
  }
83
99
 
84
- // Combine at-rules with identical query params into a single
85
- // AtRule instance, cloning their children to preserve content.
86
- let atRules = [];
100
+ [...atRule.nodes].forEach((node) => {
101
+ atRules[atRule.params].append(node);
102
+ });
87
103
 
88
- media.forEach((atRule) => {
89
- let query = atRule.params;
104
+ // Remove the original at-rule since its contents have been
105
+ // merged into `atRules[atRule.params]`.
106
+ atRule.remove();
107
+ });
90
108
 
91
- if (!atRules[query]) {
92
- atRules[query] = new AtRule({
93
- name: atRule.name,
94
- params: atRule.params,
95
- source: atRule.source
96
- });
97
- }
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
+ });
98
116
 
99
- atRule.nodes.forEach((node) => {
100
- atRules[query].append(node);
101
- });
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
+ );
102
122
 
103
- // Remove the original at-rule since its contents have been
104
- // merged into `atRules[query]`.
105
- atRule.remove();
106
- });
123
+ if (!medias) {
124
+ return;
125
+ }
107
126
 
108
- // Sort query keys and append merged at-rules back to the parent
109
- if (atRules) {
110
- sortAtRules(Object.keys(atRules), options, sortCSSmq).forEach((query) => {
111
- parent.append(atRules[query]);
112
- });
113
- }
127
+ medias.forEach((atRule) => {
128
+ parent.append(atRule);
114
129
  });
115
130
  });
116
131
  }