diff --git a/README.md b/README.md index 32dae41..ef1079f 100644 --- a/README.md +++ b/README.md @@ -126,13 +126,38 @@ By default the plugin uses the `from` value from the options of the loader or of } ``` +### config + +By default the plugin looks for a `postcss.config.js` file in your project's root (read [node-app-root-path](https://github.com/inxilpro/node-app-root-path) to understand how root is determined) and tries to apply all subsequent PostCSS plugins to the extracted CSS. + +In case this lookup doesn't suite you it's possible to specify the config path yourself. + +```javascript +'postcss-extract-media-query': { + config: path.join(__dirname, 'some/path/postcss.config.js') +} +``` + +It's also possible to pass the config as object to avoid any file resolution. + +```javascript +'postcss-extract-media-query': { + config: { + plugins: { + 'postcss-extract-media-query': {} + 'cssnano': {} + } + } +} +``` + ## Migration ### coming from 1.x Both options, `combine` and `minimize`, have been removed in v2 because the plugin parses your `postcss.config.js` now and applies all subsequent plugins to the extracted files as well. -So if you have used them you simply need to install appropriate PostCSS plugins (see below for example) and add them to your config. +So if you have used them you simply need to install appropriate PostCSS plugins (see below for example) and add them to your PostCSS config. ```bash npm install postcss-combine-media-query cssnano --save-dev @@ -145,6 +170,10 @@ plugins: { } ``` +### plugin authors + +If you're using this plugin via the api (e.g. for your own plugin) you should note it has changed from sync to async in v2. This was necessary in the course of going with promises. I'm not going to keep support of the sync api because it would make the code more complex than necessary and it's officially recommended to use async. Please check the tests to see how it has to be done now! + ## Webpack User? If you're using webpack you should use [media-query-plugin](https://github.com/SassNinja/media-query-plugin) which is built for webpack only and thus comes with several advantages such as applying all other loaders you've defined and hash support for caching. diff --git a/combine-media.js b/combine-media.js deleted file mode 100644 index 969bec3..0000000 --- a/combine-media.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * 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]); - }); - }; -}); \ No newline at end of file diff --git a/index.js b/index.js index fa40fe3..199fccc 100644 --- a/index.js +++ b/index.js @@ -4,39 +4,9 @@ const fs = require('fs'); const path = require('path'); const { green, yellow } = require('kleur'); const postcss = require('postcss'); -const rootPath = require('app-root-path').path +const SubsequentPlugins = require('./subsequent-plugins'); -function readConfigFile(file) { - if (file && !path.isAbsolute(file)) { - file = path.join(rootPath, file); - } - const filePath = file || path.join(rootPath, 'postcss.config.js'); - - if (fs.existsSync(filePath)) { - return require(filePath); - } - return {}; -} - -const config = readConfigFile(); -const allPluginNames = config.plugins ? Object.keys(config.plugins) : []; -const subsequentPluginNames = allPluginNames.slice(allPluginNames.indexOf('postcss-extract-media-query') + 1); -const subsequentPlugins = subsequentPluginNames.map(name => ({ name, mod: require(name), opts: config.plugins[name] })); - -function applySubsequentPlugins(css, filePath) { - const plugins = subsequentPlugins.map(plugin => plugin.mod(plugin.opts)) - - if (plugins.length) { - return new Promise(resolve => { - postcss(plugins) - .process(css, { from: filePath }) - .then(result => { - resolve(result.css); - }) - }); - } - return Promise.resolve(css); -} +const plugins = new SubsequentPlugins(); module.exports = postcss.plugin('postcss-extract-media-query', opts => { @@ -51,6 +21,10 @@ module.exports = postcss.plugin('postcss-extract-media-query', opts => { entry: null }, opts); + if (opts.config) { + plugins.updateConfig(opts.config); + } + // Deprecation warnings // TODO: remove in future if (typeof opts.whitelist === 'boolean') { @@ -114,42 +88,35 @@ module.exports = postcss.plugin('postcss-extract-media-query', opts => { const promises = []; - Object.keys(media).forEach(queryname => { - - promises.push(new Promise(resolve => { - - let { css } = getMedia(queryname); - - if (opts.output.path) { - + // gather promises only if output.path specified because otherwise + // nothing has been extracted + if (opts.output.path) { + Object.keys(media).forEach(queryname => { + promises.push(new Promise(resolve => { + let { css } = getMedia(queryname); const newFile = opts.output.name .replace(/\[name\]/g, name) .replace(/\[query\]/g, queryname) - .replace(/\[ext\]/g, ext) - + .replace(/\[ext\]/g, ext); const newFilePath = path.join(opts.output.path, newFile); const newFileDir = path.dirname(newFilePath); - if (opts.output.path) { - - applySubsequentPlugins(css, newFilePath).then(css => { - - if (!fs.existsSync(path.dirname(newFilePath))) { - // make sure we can write - fs.mkdirSync(newFileDir, { recursive: true }); - } - - fs.writeFileSync(newFilePath, css); - - if (opts.stats === true) { - console.log(green('[extracted media query]'), newFile); - } - resolve(); - }); - } - } - })); - }); + plugins.applyPlugins(css, newFilePath).then(css => { + + if (!fs.existsSync(path.dirname(newFilePath))) { + // make sure we can write + fs.mkdirSync(newFileDir, { recursive: true }); + } + fs.writeFileSync(newFilePath, css); + + if (opts.stats === true) { + console.log(green('[extracted media query]'), newFile); + } + resolve(); + }); + })); + }); + } return Promise.all(promises); }; diff --git a/subsequent-plugins.js b/subsequent-plugins.js new file mode 100644 index 0000000..86c5247 --- /dev/null +++ b/subsequent-plugins.js @@ -0,0 +1,74 @@ + +const fs = require('fs'); +const path = require('path'); +const postcss = require('postcss'); +const rootPath = require('app-root-path').path; + +class SubsequentPlugins { + constructor() { + this.config = {}; + this.updateConfig(); + } + + /** + * (Re)init with current postcss config + */ + _init() { + this.allNames = this.config.plugins ? Object.keys(this.config.plugins) : []; + this.subsequentNames = this.allNames.slice(this.allNames.indexOf('postcss-extract-media-query') + 1); + this.subsequentPlugins = this.subsequentNames.map(name => ({ + name, + mod: this.config.pluginsSrc && this.config.pluginsSrc[name] || require(name), + opts: this.config.plugins[name] + })); + } + + /** + * Updates the postcss config by resolving file path + * or by using the config file object + * + * @param {string|Object} file + * @returns {Object} + */ + updateConfig(file) { + if (typeof file === 'object') { + this.config = file; + this._init(); + return this.config; + } + if (typeof file === 'string' && !path.isAbsolute(file)) { + file = path.join(rootPath, file); + } + const filePath = file || path.join(rootPath, 'postcss.config.js'); + + if (fs.existsSync(filePath)) { + this.config = require(filePath); + } + this._init(); + return this.config; + } + + /** + * Apply all subsequent plugins to the (extracted) css + * + * @param {string} css + * @param {string} filePath + * @returns {string} + */ + applyPlugins(css, filePath) { + const plugins = this.subsequentPlugins.map(plugin => plugin.mod(plugin.opts)); + + if (plugins.length) { + return new Promise(resolve => { + postcss(plugins) + .process(css, { from: filePath, to: filePath }) + .then(result => { + resolve(result.css); + }); + }); + } + return Promise.resolve(css); + } +} + +module.exports = SubsequentPlugins; \ No newline at end of file diff --git a/test/options.js b/test/options.js index 776bf66..6506bd4 100644 --- a/test/options.js +++ b/test/options.js @@ -148,4 +148,43 @@ describe('Options', function() { }); }); + describe('config', function() { + it('should use opts.config if present to apply plugins', (done) => { + let precedingPluginCalls = 0; + const precedingPlugin = postcss.plugin('preceding-plugin', (opts) => { + return (root, result) => { + precedingPluginCalls++; + }; + }); + let subsequentPluginCalls = 0; + const subsequentPlugin = postcss.plugin('subsequent-plugin', (opts) => { + return (root, result) => { + subsequentPluginCalls++; + }; + }); + const opts = { + output: { + path: path.join(__dirname, 'output') + }, + stats: false, + config: { + pluginsSrc: { + 'preceding-plugin': precedingPlugin, + 'subsequent-plugin': subsequentPlugin + }, + plugins: { + 'preceding-plugin': {}, + 'postcss-extract-media-query': {}, + 'subsequent-plugin': {} + } + } + }; + postcss([ plugin(opts) ]).process(exampleFile, { from: 'test/data/example.css' }).then(() => { + assert.equal(precedingPluginCalls, 0); + assert.isAtLeast(subsequentPluginCalls, 1); + done(); + }); + }); + }); + });