Skip to content

Commit

Permalink
Rework extraction to support nested atRules (#11)
Browse files Browse the repository at this point in the history
Rework extraction to support nested atRules
  • Loading branch information
SassNinja authored Oct 28, 2019
2 parents f7cf6c7 + b0248d5 commit 927d57f
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 62 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,13 @@ By default the params of the extracted media query is converted to kebab case an
}
```

### whitelist
### extractAll

By default the plugin extracts all media queries into separate files. If you want it to only extract the ones you've defined a certain name for (see `queries` option) you have to set this option `true`. This ignores all media queries that don't have a custom name defined.
By default the plugin extracts all media queries into separate files. If you want it to only extract the ones you've defined a certain name for (see `queries` option) you have to set this option `false`. This ignores all media queries that don't have a custom name defined.

```javascript
'postcss-extract-media-query': {
whitelist: true
extractAll: false
}
```

Expand Down
34 changes: 34 additions & 0 deletions combine-media.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* TODO: move this into own repo for more use cases
*/

const postcss = require('postcss');

module.exports = postcss.plugin('postcss-combine-media-query', opts => {

const atRules = {};

function addToAtRules(atRule) {
const key = atRule.params;

if (!atRules[key]) {
atRules[key] = postcss.atRule({ name: atRule.name, params: atRule.params });
}
atRule.nodes.forEach(node => {
atRules[key].append(node.clone());
});

atRule.remove();
}

return (root) => {

root.walkAtRules('media', atRule => {
addToAtRules(atRule);
});

Object.keys(atRules).forEach(key => {
root.append(atRules[key]);
});
};
});
130 changes: 75 additions & 55 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const path = require('path');
const chalk = require('chalk');
const postcss = require('postcss');
const csswring = require('csswring');
const combineMedia = require('./combine-media');

module.exports = postcss.plugin('postcss-extract-media-query', opts => {

Expand All @@ -15,34 +16,35 @@ module.exports = postcss.plugin('postcss-extract-media-query', opts => {
name: '[name]-[query].[ext]'
},
queries: {},
whitelist: false,
extractAll: true,
combine: true,
minimize: false,
stats: true
}, opts);

function addToAtRules(atRules, key, atRule) {

// init array for target key if undefined
if (!atRules[key]) {
atRules[key] = [];
// Deprecation warnings
// TODO: remove in future
if (typeof opts.whitelist === 'boolean') {
console.log(chalk.yellow(`[WARNING] whitelist option is deprecated and will be removed in future – please use extractAll`));
if (opts.whitelist === true) {
opts.extractAll = false;
}
}

// create new atRule if none existing or combine false
if (atRules[key].length < 1 || opts.combine === false) {
atRules[key].push(postcss.atRule({ name: atRule.name, params: atRule.params }));
}
const media = {};

// pointer to last item in array
const lastAtRule = atRules[key][atRules[key].length - 1];
function addMedia(key, css, query) {
if (!Array.isArray(media[key])) {
media[key] = [];
}
media[key].push({ css, query });
}

// append all rules
atRule.walkRules(rule => {
lastAtRule.append(rule);
});
function getMedia(key) {
const css = media[key].map(data => data.css).join('\n');
const query = media[key][0].query;

// remove atRule from original chunk
atRule.remove();
return { css, query };
}

return (root, result) => {
Expand All @@ -59,65 +61,83 @@ module.exports = postcss.plugin('postcss-extract-media-query', opts => {
const name = file[1];
const ext = file[2];

const newAtRules = {};

root.walkAtRules('media', atRule => {

// use custom query name if available (e.g. tablet)
// or otherwise the query key (converted to kebab case)
const hasCustomName = typeof opts.queries[atRule.params] === 'string';
const key = hasCustomName === true
? opts.queries[atRule.params]
: _.kebabCase(atRule.params);

// extract media atRule and concatenate with existing atRule (same key)
// if no whitelist set or if whitelist and atRule has custom query name match
if (opts.whitelist === false || hasCustomName === true) {
addToAtRules(newAtRules, key, atRule);
const query = atRule.params;
const queryname = opts.queries[query] || (opts.extractAll && _.kebabCase(query));

if (queryname) {
const css = postcss.root().append(atRule).toString();

addMedia(queryname, css, query);

if (opts.output.path) {
atRule.remove();
}
}
});

Object.keys(newAtRules).forEach(key => {
// emit file(s) with extracted css
if (opts.output.path) {

Object.keys(media).forEach(queryname => {

// emit extracted css file
if (opts.output.path) {
let { css } = getMedia(queryname);

const newFile = opts.output.name
.replace(/\[name\]/g, name)
.replace(/\[query\]/g, key)
.replace(/\[query\]/g, queryname)
.replace(/\[ext\]/g, ext)

const newFilePath = path.join(opts.output.path, newFile);

// create new root
// and append all extracted atRules with current key
const newRoot = postcss.root();
newAtRules[key].forEach(newAtRule => {
newRoot.append(newAtRule);
});
if (opts.combine === true) {
css = postcss([ combineMedia() ])
.process(css, { from: newFilePath })
.root
.toString();
}

if (opts.minimize === true) {
const newRootMinimized = postcss([ csswring() ])
.process(newRoot.toString(), { from: newFilePath })
.root;
fs.outputFileSync(newFilePath, newRootMinimized.toString());
const cssMinimized = postcss([ csswring() ])
.process(css, { from: newFilePath })
.root
.toString();
fs.outputFileSync(newFilePath, cssMinimized);
} else {
fs.outputFileSync(newFilePath, newRoot.toString());
fs.outputFileSync(newFilePath, css);
}


if (opts.stats === true) {
console.log(chalk.green('[extracted media query]'), newFile);
}
}
// if no output path defined (mostly testing purpose) merge back to root
else {
newAtRules[key].forEach(newAtRule => {
root.append(newAtRule);
});
}
});
}

});
// if no output path defined (mostly testing purpose) merge back to root
// TODO: remove this in v2 together with combine & minimize
else {

Object.keys(media).forEach(queryname => {

let { css } = getMedia(queryname);

if (opts.combine === true) {
css = postcss([ combineMedia() ])
.process(css, { from })
.root
.toString();
}
if (opts.minimize === true) {
css = postcss([ csswring() ])
.process(css, { from })
.root
.toString();
}

root.append(postcss.parse(css));
});
}

};

Expand Down
8 changes: 4 additions & 4 deletions test/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,21 @@ describe('Options', function() {
fs.removeSync('test/output');
});

describe('whitelist', function() {
describe('extractAll', function() {
it('true should cause to ignore all media queries except of the ones defined in the queries options', function() {
const opts = {
output: {
path: path.join(__dirname, 'output')
},
queries: {
'screen and (min-width: 999px)': 'whitelist'
'screen and (min-width: 999px)': 'extract-all'
},
whitelist: true,
extractAll: false,
stats: false
};
postcss([ plugin(opts) ]).process(exampleFile, { from: 'test/data/example.css'}).css;
const filesCount = fs.readdirSync('test/output/').length;
assert.isTrue(fs.existsSync('test/output/example-whitelist.css'));
assert.isTrue(fs.existsSync('test/output/example-extract-all.css'));
assert.equal(filesCount, 1);
});
});
Expand Down

0 comments on commit 927d57f

Please sign in to comment.