From 141d302538e487a996ec64258906eb4da9abd203 Mon Sep 17 00:00:00 2001 From: Luke Edwards Date: Thu, 27 Jul 2017 13:10:30 -0700 Subject: [PATCH] Autoload PostCSS config (#284) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * extract setError helper * autoload varying postcss config files * refactor config/options handling - forgot that `opts` param defaults to empty object - imported taskr’s ‘isEmptyObj’ fn helper * add postcssrc test & fixture * second refactor; cleaner - look once for a file, then decide what to do based on result * add all autoload tests * allow “plugins” to be an array (json-types) * parse `config.options` thru requires * add final test * update readme docs --- packages/postcss/index.js | 109 +++++++++++++--- packages/postcss/readme.md | 59 +++++++-- .../postcss/test/fixtures/sub1/.postcssrc | 5 + .../postcss/test/fixtures/sub2/package.json | 8 ++ .../test/fixtures/sub3/postcss.config.js | 5 + .../postcss/test/fixtures/sub4/.postcssrc.js | 5 + .../postcss/test/fixtures/sub5/.postcssrc | 8 ++ packages/postcss/test/index.js | 117 ++++++++++++++++++ 8 files changed, 293 insertions(+), 23 deletions(-) create mode 100644 packages/postcss/test/fixtures/sub1/.postcssrc create mode 100644 packages/postcss/test/fixtures/sub2/package.json create mode 100644 packages/postcss/test/fixtures/sub3/postcss.config.js create mode 100644 packages/postcss/test/fixtures/sub4/.postcssrc.js create mode 100644 packages/postcss/test/fixtures/sub5/.postcssrc diff --git a/packages/postcss/index.js b/packages/postcss/index.js index cca9cb99..fb75fde6 100644 --- a/packages/postcss/index.js +++ b/packages/postcss/index.js @@ -1,21 +1,100 @@ 'use strict'; -const extn = require('path').extname; +const res = require('path').resolve; const postcss = require('postcss'); -module.exports = function (task) { - task.plugin('postcss', {}, function * (file, opts) { - opts = Object.assign({ plugins:[], options:{} }, opts); - - try { - const ctx = postcss(opts.plugins); - const out = yield ctx.process(file.data.toString(), opts); - file.data = Buffer.from(out.css); // write new data - } catch (error) { - task.emit('plugin_error', { - plugin: '@taskr/postcss', - error: error.message - }); +const base = { plugins:[], options:{} }; +const filenames = ['.postcssrc', '.postcssrc.js', 'postcss.config.js', 'package.json']; + +const isString = any => typeof any === 'string'; +const isObject = any => Boolean(any) && (any.constructor === Object); +const isEmptyObj = any => isObject(any) && Object.keys(any).length === 0; + +module.exports = function (task, utils) { + const rootDir = str => res(task.root, str); + const setError = msg => task.emit('plugin_error', { plugin:'@taskr/postcss', error:msg }); + const getConfig = arr => Promise.all(arr.map(utils.find)).then(res => res.filter(Boolean)).then(res => res[0]); + + task.plugin('postcss', { every:false }, function * (files, opts) { + let config, isJSON = false; + + if (isEmptyObj(opts)) { + // autoload a file + const fileConfig = yield getConfig(filenames.map(rootDir)); + // process if found one! + if (fileConfig !== void 0) { + try { + config = require(fileConfig); + } catch (err) { + try { + isJSON = true; // .rc file + config = JSON.parse(yield utils.read(fileConfig, 'utf8')); + } catch (_) { + return setError(err.message); + } + } + // handle config types + if (typeof config === 'function') { + config = config(base); // send default values + } else if (isObject(config)) { + // grab "postcss" key (package.json) + if (config.postcss !== void 0) { + config = config.postcss; + isJSON = true; + } + + // reconstruct plugins? + if (isObject(config.plugins)) { + let k, plugins=[]; + for (k in config.plugins) { + try { + plugins.push(require(k)(config.plugins[k])); + } catch (err) { + return setError(`Loading PostCSS plugin (${k}) failed with: ${err.message}`); + } + } + config.plugins = plugins; // update config + } else if (isJSON && Array.isArray(config.plugins)) { + const truthy = config.plugins.filter(Boolean); + let i=0, len=truthy.length, plugins=[]; + for (; i **Note:** There should be no need to set `options.to` and `options.from`. + +If you would like to [autoload external PostCSS config](#autoloaded-options), you must not define any `options` directly. + + ## Usage -The paths within `task.source()` should always point to files that you want transformed into `.css` files. +#### Embedded Options + +> Declare your PostCSS options directly within your `taskfile.js`: ```js -exports.test = function * (task) { +exports.styles = function * (task) { yield task.source('src/**/*.scss').postcss({ plugins: [ require('precss'), - require('autoprefixer') + require('autoprefixer')({ + browsers: ['last 2 versions'] + }) ], options: { parser: require('postcss-scss') } - }).target('dist'); + }).target('dist/css'); } ``` -## API +#### Autoloaded Options -### .postcss(options) +> Automatically detect & connect to existing PostCSS configurations -Check out PostCSS's [Options](https://github.com/postcss/postcss#options) documentation to see the available options. +If no [`options`](#api) were defined, `@taskr/postcss` will look for existing `.postcssrc`, `postcss.config.js`, and `.postcssrc.js` root-directory files. Similarly, it will honor a `"postcss"` key within your `package.json` file. + +* `.postcssrc` -- must be JSON; see [example](/test/fixtures/sub1/.postcssrc) +* `.postcssrc.js` -- can be JSON or `module.exports` a Function or Object; see [example](/test/fixtures/sub4/.postcssrc.js) +* `postcss.config.js` -- can be JSON or `module.exports` a Function or Object; see [example](/test/fixtures/sub3/postcss.config.js) +* `package.json` -- must use `"postcss"` key & must be JSON; see [example](/test/fixtures/sub2/package.json) + +> **Important:** If you take this route, you only need _one_ of the files mentioned! + +```js +// taskfile.js +exports.styles = function * (task) { + yield task.source('src/**/*.scss').postcss().target('dist/css'); +} +``` + +```js +// .postcssrc +{ + "plugins": { + "precss": {}, + "autoprefixer": { + "browsers": ["last 2 versions"] + } + }, + "options": { + "parser": "postcss-scss" + } +} +``` -> **Note:** There should be no need to set `options.to` and `options.from`. ## Support diff --git a/packages/postcss/test/fixtures/sub1/.postcssrc b/packages/postcss/test/fixtures/sub1/.postcssrc new file mode 100644 index 00000000..529e0a2b --- /dev/null +++ b/packages/postcss/test/fixtures/sub1/.postcssrc @@ -0,0 +1,5 @@ +{ + "plugins": { + "autoprefixer": {} + } +} diff --git a/packages/postcss/test/fixtures/sub2/package.json b/packages/postcss/test/fixtures/sub2/package.json new file mode 100644 index 00000000..a8b1b59b --- /dev/null +++ b/packages/postcss/test/fixtures/sub2/package.json @@ -0,0 +1,8 @@ +{ + "private": true, + "postcss": { + "plugins": { + "autoprefixer": {} + } + } +} diff --git a/packages/postcss/test/fixtures/sub3/postcss.config.js b/packages/postcss/test/fixtures/sub3/postcss.config.js new file mode 100644 index 00000000..8293deae --- /dev/null +++ b/packages/postcss/test/fixtures/sub3/postcss.config.js @@ -0,0 +1,5 @@ +const autoprefixer = require('autoprefixer'); + +module.exports = conf => ({ + plugins: conf.plugins.concat(autoprefixer) +}); diff --git a/packages/postcss/test/fixtures/sub4/.postcssrc.js b/packages/postcss/test/fixtures/sub4/.postcssrc.js new file mode 100644 index 00000000..3be3898b --- /dev/null +++ b/packages/postcss/test/fixtures/sub4/.postcssrc.js @@ -0,0 +1,5 @@ +module.exports = { + plugins: [ + require('autoprefixer') + ] +}; diff --git a/packages/postcss/test/fixtures/sub5/.postcssrc b/packages/postcss/test/fixtures/sub5/.postcssrc new file mode 100644 index 00000000..1e8e3163 --- /dev/null +++ b/packages/postcss/test/fixtures/sub5/.postcssrc @@ -0,0 +1,8 @@ +{ + "plugins": [ + "autoprefixer" + ], + "options": { + "parser": "postcss-scss" + } +} diff --git a/packages/postcss/test/index.js b/packages/postcss/test/index.js index 3ef79a68..aa726e8b 100644 --- a/packages/postcss/test/index.js +++ b/packages/postcss/test/index.js @@ -65,6 +65,123 @@ test('@taskr/postcss (options)', t => { }).start('foo'); }); +test('@taskr/postcss (postcssrc)', t => { + t.plan(2); + const taskr = new Taskr({ + plugins, + cwd: join(dir, 'sub1'), + tasks: { + *foo(f) { + const tmp = tmpDir('tmp-3'); + yield f.source(`${dir}/*.css`).postcss().target(tmp); + + const arr = yield f.$.expand(`${tmp}/*.*`); + t.equal(arr.length, 1, 'write one file to target'); + + const str = yield f.$.read(`${tmp}/foo.css`, 'utf8'); + t.true(/-webkit-box/.test(str), 'applies `autoprefixer` plugin transform'); + + yield f.clear(tmp); + } + } + }); + taskr.start('foo'); +}); + +test('@taskr/postcss (package.json)', t => { + t.plan(2); + const taskr = new Taskr({ + plugins, + cwd: join(dir, 'sub2'), + tasks: { + *foo(f) { + const tmp = tmpDir('tmp-4'); + yield f.source(`${dir}/*.css`).postcss().target(tmp); + + const arr = yield f.$.expand(`${tmp}/*.*`); + t.equal(arr.length, 1, 'write one file to target'); + + const str = yield f.$.read(`${tmp}/foo.css`, 'utf8'); + t.true(/-webkit-box/.test(str), 'applies `autoprefixer` plugin transform'); + + yield f.clear(tmp); + } + } + }); + taskr.start('foo'); +}); + +test('@taskr/postcss (postcss.config.js)', t => { + t.plan(2); + const taskr = new Taskr({ + plugins, + cwd: join(dir, 'sub3'), + tasks: { + *foo(f) { + const tmp = tmpDir('tmp-5'); + yield f.source(`${dir}/*.css`).postcss().target(tmp); + + const arr = yield f.$.expand(`${tmp}/*.*`); + t.equal(arr.length, 1, 'write one file to target'); + + const str = yield f.$.read(`${tmp}/foo.css`, 'utf8'); + t.true(/-webkit-box/.test(str), 'applies `autoprefixer` plugin transform'); + + yield f.clear(tmp); + } + } + }); + taskr.start('foo'); +}); + +test('@taskr/postcss (.postcssrc.js)', t => { + t.plan(2); + const taskr = new Taskr({ + plugins, + cwd: join(dir, 'sub4'), + tasks: { + *foo(f) { + const tmp = tmpDir('tmp-6'); + yield f.source(`${dir}/*.css`).postcss().target(tmp); + + const arr = yield f.$.expand(`${tmp}/*.*`); + t.equal(arr.length, 1, 'write one file to target'); + + const str = yield f.$.read(`${tmp}/foo.css`, 'utf8'); + t.true(/-webkit-box/.test(str), 'applies `autoprefixer` plugin transform'); + + yield f.clear(tmp); + } + } + }); + taskr.start('foo'); +}); + +test('@taskr/postcss (plugins + options)', t => { + t.plan(4); + const taskr = new Taskr({ + plugins, + cwd: join(dir, 'sub5'), + tasks: { + *foo(f) { + const tmp = tmpDir('tmp-7'); + yield f.source(`${dir}/*.scss`).postcss().target(tmp); + + const arr = yield f.$.expand(`${tmp}/*.*`); + t.equal(arr.length, 1, 'write one file to target'); + + const str = yield f.$.read(`${tmp}/bar.scss`, 'utf8'); + t.true(str.indexOf('-ms-flexbox') !== -1, 'applies prefixer to CSS lookalike'); + t.true(str.indexOf('-webkit-box-flex: val') !== -1, 'applies prefixer to SCSS mixin'); + t.ok(str, 'retains `.scss` file extension'); + + yield f.clear(tmp); + } + } + }); + taskr.start('foo'); +}); + // test('@taskr/postcss (inline)', t => { // t.plan(2); // create({