Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recursive sort #50

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.vscode
node_modules/
npm-debug.log
yarn-error.log
Expand Down
1 change: 1 addition & 0 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ yarn.lock
.editorconfig
.gitattributes
.github
.vscode
index.test.js

CHANGELOG.md
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,18 @@ postcss([
]).process(css);
```

### Recursive

Sort nested media queries recursivey

```js
postcss([
sortMediaQueries({
recursive: true,
})
]).process(css);
```

---

## Changelog
Expand Down
115 changes: 74 additions & 41 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,34 +1,81 @@
function sortAtRules(queries, sort, sortCSSmq) {
if (typeof sort !== 'function') {
sort = sort === 'desktop-first' ? sortCSSmq.desktopFirst : sortCSSmq
}
// @ts-check

return queries.sort(sort)
}
/**
* @typedef {import('postcss/lib/postcss').Plugin} Plugin
* @typedef {import('postcss/lib/postcss').Root} Root
* @typedef {import('postcss/lib/postcss').AtRule} AtRule
* @typedef {import('sort-css-media-queries/lib')} SortCSSmq
*/

module.exports = (opts = {}) => {
/**
* Sort function declaration
* @callback sortFn
* @param {string} A - previous
* @param {string} B - next
* @returns {number}
*/

opts = Object.assign(
/**
* @typedef {object} sortCSSMediaQueriesOptions
* @property {boolean=} unitlessMqAlwaysFirst unitless media queries first. Defaults to false
*/

/**
* @typedef {object} pluginOptions
* @property {'mobile-first'|'desktop-first'|sortFn=} sort Sort strategy.
* @property {false|sortCSSMediaQueriesOptions=} configuration sort-css-media-queries options.
* @property {boolean=} onlyTopLevel Whether to sort the top level only or not. Defaults to false.
* @property {boolean=} recursive Whether to sort recursively or not. Defaults to false.
* @property {string|RegExp=} pattern string or regex pattern to math media querie like atRules. Defaults to 'media'.
*/

/**
* Plugin constructor
* @param {pluginOptions} opts - plugin options
* @returns {Plugin} - plugin object
*/
module.exports = (opts = {}) => {
const {sort, configuration, onlyTopLevel, pattern, recursive} = Object.assign(
{
sort: 'mobile-first',
configuration: false,
onlyTopLevel: false,
pattern: 'media',
},
opts
)

const createSort = require('sort-css-media-queries/lib/create-sort');
const sortCSSmq = opts.configuration ? createSort(opts.configuration) : require('sort-css-media-queries');
/** @type {SortCSSmq} */
const sortCSSmq = configuration ? createSort(configuration) : require('sort-css-media-queries');

return {
postcssPlugin: 'postcss-sort-media-queries',
OnceExit (root, { AtRule }) {
OnceExit (root, {AtRule}) {
/**
* Sort query strings
* @param {string[]} queries - Query string array
*/
function sortAtRules(queries) {
if (typeof sort !== 'function') {
return queries.sort(sort === 'desktop-first' ? sortCSSmq.desktopFirst : sortCSSmq)
}

return queries.sort(sort)
}

let atRules = [];
/**
* Recursively sort CSS media queries
* @template {AtRule|Root} T
* @param {T} localRoot - Root or atRule instance
*/
function recursivelySort(localRoot){
/** @type {{[key: string]: AtRule}} */
const atRules = {};

root.walkAtRules('media', atRule => {
if (opts.onlyTopLevel && atRule.parent.type === 'root') {
let query = atRule.params
localRoot.walkAtRules(pattern, atRule => {
const atRuleIndex = localRoot.index(atRule)
const query = atRule.params;

if (atRuleIndex === -1 || onlyTopLevel && atRule.parent && atRule.parent.type !== 'root') return;

if (!atRules[query]) {
atRules[query] = new AtRule({
Expand All @@ -38,37 +85,23 @@ module.exports = (opts = {}) => {
})
}

atRule.nodes.forEach(node => {
atRules[query].append(node.clone())
})

atRule.remove()
}
atRules[query].append(atRule.nodes);

if (!opts.onlyTopLevel) {
let query = atRule.params
atRule.remove();
})

if (!atRules[query]) {
atRules[query] = new AtRule({
name: atRule.name,
params: atRule.params,
source: atRule.source
})
}
const atRulesKeys = Object.keys(atRules);

atRule.nodes.forEach(node => {
atRules[query].append(node.clone())
})
if (!atRulesKeys.length) return;

atRule.remove()
}
})
sortAtRules(atRulesKeys).forEach(query => {
if (!onlyTopLevel && recursive) recursivelySort(atRules[query]);

if (atRules) {
sortAtRules(Object.keys(atRules), opts.sort, sortCSSmq).forEach(query => {
root.append(atRules[query])
})
localRoot.append(atRules[query]);
});
}

recursivelySort(root);
}
}
}
Expand Down
159 changes: 59 additions & 100 deletions index.test.js
Original file line number Diff line number Diff line change
@@ -1,154 +1,113 @@
let fs = require('fs')
let postcss = require('postcss')
let nested = require('postcss-nested')
let mediaMinMax = require('postcss-media-minmax')
let autoprefixer = require('autoprefixer')
let flexbugsFixes = require('postcss-flexbugs-fixes')

let plugin = require('./')

async function run (input, output, opts) {
let result = await postcss([plugin(opts)]).process(input, { from: undefined })
expect(result.css).toEqual(output)
expect(result.warnings()).toHaveLength(0)
}
// @ts-check

async function nestedRun (input, output, opts) {
let result = await postcss([nested, plugin(opts)]).process(input, { from: undefined })
expect(result.css).toEqual(output)
expect(result.warnings()).toHaveLength(0)
}
const fs = require('fs')
const postcss = require('postcss')
const nested = require('postcss-nested')
const mediaMinMax = require('postcss-media-minmax')
const autoprefixer = require('autoprefixer')
const flexbugsFixes = require('postcss-flexbugs-fixes')

async function mediaMinmaxBeforeNestedRun (input, output, opts) {
let result = await postcss([autoprefixer, flexbugsFixes, mediaMinMax, nested, plugin(opts)]).process(input, { from: undefined })
expect(result.css).toEqual(output)
expect(result.warnings()).toHaveLength(0)
}
const plugin = require('./')

async function mediaMinmaxAfterNestedRun (input, output, opts) {
let result = await postcss([autoprefixer, flexbugsFixes, nested, mediaMinMax, plugin(opts)]).process(input, { from: undefined })
expect(result.css).toEqual(output)
expect(result.warnings()).toHaveLength(0)
async function run (inputFileName, outputFileName, opts = {}, plugins = []) {
const input = fs.readFileSync(`./test/${inputFileName}.css`, 'utf8');
const output = fs.readFileSync(`./test/${outputFileName}.css`, 'utf8');
const result = await postcss([...plugins, plugin(opts)]).process(input, { from: undefined });

expect(result.css).toEqual(output);
expect(result.warnings()).toHaveLength(0);
}

it('[mf] simple #1', async () => {
let input = fs.readFileSync('./test/s1-mobile.in.css', 'utf8')
let output = fs.readFileSync('./test/s1-mobile.out.css', 'utf8')
await run(input, output, { sort: 'mobile-first' })
})
await run('s1-mobile.in', 's1-mobile.out', { sort: 'mobile-first' });
});

it('[df] simple #1', async () => {
let input = fs.readFileSync('./test/s1-desktop.in.css', 'utf8')
let output = fs.readFileSync('./test/s1-desktop.out.css', 'utf8')
await run(input, output, { sort: 'desktop-first' })
})
await run('s1-desktop.in', 's1-desktop.out', { sort: 'desktop-first' });
});

it('[mf] simple #2', async () => {
let input = fs.readFileSync('./test/s2-mobile.in.css', 'utf8')
let output = fs.readFileSync('./test/s2-mobile.out.css', 'utf8')
await run(input, output, { sort: 'mobile-first' })
})
await run('s2-mobile.in', 's2-mobile.out', { sort: 'mobile-first' });
});

it('[df] simple #2', async () => {
let input = fs.readFileSync('./test/s2-desktop.in.css', 'utf8')
let output = fs.readFileSync('./test/s2-desktop.out.css', 'utf8')
await run(input, output, { sort: 'desktop-first' })
})
await run('s2-desktop.in', 's2-desktop.out', { sort: 'desktop-first' });
});

it('[mf] without dimension #1', async () => {
let input = fs.readFileSync('./test/wd1-mobile.in.css', 'utf8')
let output = fs.readFileSync('./test/wd1-mobile.out.css', 'utf8')
await run(input, output, { sort: 'mobile-first' })
})
await run('wd1-mobile.in', 'wd1-mobile.out', { sort: 'mobile-first' });
});

it('[df] without dimension #1', async () => {
let input = fs.readFileSync('./test/wd1-desktop.in.css', 'utf8')
let output = fs.readFileSync('./test/wd1-desktop.out.css', 'utf8')
await run(input, output, { sort: 'desktop-first' })
})
await run('wd1-desktop.in', 'wd1-desktop.out', { sort: 'desktop-first' });
});

it('[mf] mixed #1', async () => {
let input = fs.readFileSync('./test/mixed-mobile.in.css', 'utf8')
let output = fs.readFileSync('./test/mixed-mobile.out.css', 'utf8')
await run(input, output, { sort: 'mobile-first' })
})
await run('mixed-mobile.in', 'mixed-mobile.out', { sort: 'mobile-first' });
});

it('[df] mixed #1', async () => {
let input = fs.readFileSync('./test/mixed-desktop.in.css', 'utf8')
let output = fs.readFileSync('./test/mixed-desktop.out.css', 'utf8')
await run(input, output, { sort: 'desktop-first' })
})
await run('mixed-desktop.in', 'mixed-desktop.out', { sort: 'desktop-first' });
});

it('[mf] configuration(mixed #1): unitlessMqAlwaysFirst: FALSE', async () => {
let input = fs.readFileSync('./test/configuration/false-mixed-mobile.in.css', 'utf8')
let output = fs.readFileSync('./test/configuration/false-mixed-mobile.out.css', 'utf8')
it('use custom sort function', async () => {
await run('custom-sort.in', 'custom-sort.out', { sort: (a, b) => a.length < b.length }, []);
});

let options = {
it('sort recursively', async () => {
await run('recursive-sort.in', 'recursive-sort.out', { recursive: true }, []);
});

it('[mf] configuration(mixed #1): unitlessMqAlwaysFirst: FALSE', async () => {
const options = {
configuration: {
unitlessMqAlwaysFirst: false,
}
}
await run(input, output, options)
})
await run('configuration/false-mixed-mobile.in', 'configuration/false-mixed-mobile.out', options)
});

it('[mf] configuration(mixed #1): unitlessMqAlwaysFirst: TRUE', async () => {
let input = fs.readFileSync('./test/configuration/true-mixed-mobile.in.css', 'utf8')
let output = fs.readFileSync('./test/configuration/true-mixed-mobile.out.css', 'utf8')

let options = {
const options = {
configuration: {
unitlessMqAlwaysFirst: true,
}
}
await run(input, output, options)
})
await run('configuration/true-mixed-mobile.in', 'configuration/true-mixed-mobile.out', options)
});

it('[df] configuration(mixed #2): unitlessMqAlwaysFirst: FALSE', async () => {
let input = fs.readFileSync('./test/configuration/false-mixed-desktop.in.css', 'utf8')
let output = fs.readFileSync('./test/configuration/false-mixed-desktop.out.css', 'utf8')

let options = {
const options = {
sort: 'desktop-first',
configuration: {
unitlessMqAlwaysFirst: false,
}
}
await run(input, output, options)
})
await run('configuration/false-mixed-desktop.in', 'configuration/false-mixed-desktop.out', options)
});

it('[df] configuration(mixed #2): unitlessMqAlwaysFirst: TRUE', async () => {
let input = fs.readFileSync('./test/configuration/true-mixed-desktop.in.css', 'utf8')
let output = fs.readFileSync('./test/configuration/true-mixed-desktop.out.css', 'utf8')

let options = {
const options = {
sort: 'desktop-first',
configuration: {
unitlessMqAlwaysFirst: true,
}
}
await run(input, output, options)
})
await run('configuration/true-mixed-desktop.in', 'configuration/true-mixed-desktop.out', options)
});

it('postcss nested', async () => {
let input = fs.readFileSync('./test/postcss.nested.in.css', 'utf8')
let output = fs.readFileSync('./test/postcss.nested.out.css', 'utf8')
await nestedRun(input, output)
})
await run('postcss.nested.in', 'postcss.nested.out', {}, [nested])
});

it('postcss media minmax -> nested', async () => {
let input = fs.readFileSync('./test/postcss.media.minmax.in.css', 'utf8')
let output = fs.readFileSync('./test/postcss.media.minmax.out.css', 'utf8')
await mediaMinmaxBeforeNestedRun(input, output)
})
await run('postcss.media.minmax.in', 'postcss.media.minmax.out', {}, [autoprefixer, flexbugsFixes, mediaMinMax, nested])
});

it('postcss nested -> media minmax', async () => {
let input = fs.readFileSync('./test/postcss.media.minmax.in.css', 'utf8')
let output = fs.readFileSync('./test/postcss.media.minmax.out.css', 'utf8')
await mediaMinmaxAfterNestedRun(input, output)
})
await run('postcss.media.minmax.in', 'postcss.media.minmax.out', {}, [autoprefixer, flexbugsFixes, nested, mediaMinMax])
});

it('only top level', async () => {
let input = fs.readFileSync('./test/only-top-level.in.css', 'utf8')
let output = fs.readFileSync('./test/only-top-level.out.css', 'utf8')
await run(input, output, { onlyTopLevel: true })
})
await run('only-top-level.in', 'only-top-level.out', { onlyTopLevel: true })
});