Skip to content

Commit

Permalink
Rework extraction to support nested atRules
Browse files Browse the repository at this point in the history
This is closer to my webpack plugin and simplifies adding new features.

Furthermore I've moved the combine logic into an own plugin to move it completely out of this repo in v2 to support more use cases.
  • Loading branch information
SassNinja committed Oct 28, 2019
1 parent f7cf6c7 commit b0248d5
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 b0248d5

Please sign in to comment.