diff --git a/.travis.yml b/.travis.yml index ae99d9c..72cb113 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ language: node_js node_js: - "stable" + - "5" - "4" diff --git a/README.md b/README.md index 0494b11..59d56b7 100644 --- a/README.md +++ b/README.md @@ -18,264 +18,143 @@ Pack CSS into common shared bundles. which make `b.bundle()` output a stream manipulatable by [`gulp`] plugins. ## Example -Suppose we want to pack css in `/path/to/src` (not including those in its subdirectories) into `/path/to/build/bundle.css`. +Check the [example](example/reduce/). -There are already `blue.css` and `red.css` in `/path/to/src`, and they both depend upon `/path/to/src/node_modules/reset/index.js`. - -**Input** - -`blue.css`: -```css -@external "reset"; -@import "color"; -.blue { - color: $blue; +```js +var reduce = require('reduce-css') +var del = require('del') +var path = require('path') + +bundle(createBundler()) + +function createBundler(watch) { + var basedir = path.join(__dirname, 'src') + var b = reduce.create( + /* glob for entries */ + '*.css', + + /* options for depsify */ + { + basedir, + cache: {}, + packageCache: {}, + }, + + /* options for common-bundle */ + // single bundle + // 'bundle.css', + // multiple bundles + { + groups: '*.css', + common: 'common.css', + }, + + /* options for watchify2 */ + watch && { entryGlob: '*.css' } + ) + return b } -``` - -`red.css`: -```css -@external "reset"; -@external "./button"; -@import "color"; -.red { - color: $red; +function bundle(b) { + var build = path.join(__dirname, 'build') + del.sync(build) + return b.bundle().on('error', log) + .pipe(b.dest(build, { + maxSize: 0, + name: '[name].[hash]', + assetOutFolder: path.join(build, 'assets'), + })) } -``` - -`reset` contains styles to be shared. -We use `@external` to declare that -it should come before `a.css` and `b.css` in the final `bundle.css`. -```css -html, body { - margin: 0; - padding: 0; +function log() { + console.log.apply(console, [].map.call(arguments, function (msg) { + if (typeof msg === 'string') { + return msg + } + return JSON.stringify(msg, null, 2) + })) } -``` - -The `color` module is installed in `node_modules`, -and will be consumed by [`postcss`] when `@import`ed in css. -```css -$red: #FF0000; -$green: #00FF00; -$blue: #0000FF; ``` -`/path/to/src/button` is a button component, -shipped with a background image (`/path/to/src/button/button.png`), -as well as some styles (`/path/to/src/button/index.css`): -```css -@import "color"; -.button { - background-color: $red; - background-image: url(button.png); -} - -``` -The image will be inlined or copied to the build directory -after bundling, and the url in css will also be transformed to -reference to it correctly. - -**Building script** +To watch file changes: ```js -'use strict' - -const reduce = require('reduce-css') - -const build = __dirname + '/build' -const basedir = __dirname + '/src' -const b = reduce.create({ basedir }) -reduce.src('*.css', { cwd: basedir }) - .pipe(reduce.bundle(b, 'bundle.css')) - .pipe(reduce.dest(build, null, { - maxSize: 0, - name: '[name].[hash]', - assetOutFolder: build + '/assets', - })) - -``` - -**Output** - -`/path/to/build/bundle.css`: -```css -html, body { - margin: 0; - padding: 0; -} - -.blue { - color: #0000FF; -} - -.button { - background-color: #FF0000; - background-image: url(assets/button.161fff2.png); -} -.red { - color: #FF0000; -} +var b = createBundler(true) +b.on('update', function update() { + bundle(b) + return update +}()) ``` -The background image has been renamed and copied to `/path/to/build/assets/button.161fff2.png`. - -**Watch** - -To watch file changes: +To work with gulp: ```js -'use strict' - -const reduce = require('reduce-css') - -const build = __dirname + '/build' -const basedir = __dirname + '/src' -const b = reduce.create({ - basedir, - cache: {}, - packageCache: {}, +var gulp = require('gulp') +gulp.task('build', function () { + return bundle(createBundler()) }) -reduce.src('*.css', { cwd: basedir }) - .pipe(reduce.watch(b, 'bundle.css', { entryGlob: '*.css' })) - .on('bundle', function (bundleStream) { - bundleStream.pipe(reduce.dest(build, null, { - maxSize: 0, - name: '[name].[hash]', - assetOutFolder: build + '/assets', - })) - .on('data', file => console.log('bundle:', file.relative)) - .on('end', () => console.log('-'.repeat(40))) - }) - +gulp.task('watch', function (cb) { + var b = createBundler(true) + b.on('update', function update() { + bundle(b) + return update + }()) + b.on('close', cb) +}) ``` -**Common shared bundles** - -Check this [example](example/without-gulp/multi.js). - -## Work with Gulp -Check this [gulpfile](example/gulp/multi/gulpfile.js). - ## API -```javascript -const reduce = require('reduce-css') +```js +var reduce = require('reduce-css') +var b = reduce.create(entries, depsifyOptions, bundleOptions, watchifyOptions) ``` -### reduce.create(opts) +### reduce.create(entries, depsifyOptions, bundleOptions, watchifyOptions) Return a [`depsify`] instance. -`opts` is passed to the [`depsify`] constructor. - -If `opts.postcss` is not `false`, +* `entries`: patterns to locate input files. Check [`globby`] for more details. +* `depsifyOptions`: options for [`depsify`]. +If `depsifyOptions.postcss` is not `false`, the plugin [`reduce-css-postcss`] for [`depsify`] is applied, which use [`postcss`] to preprocess css. +* `bundleOptions`: options for [`common-bundle`]. +* `watchifyOptions`: options for [`watchify2`]. +If specified, file changes are watched. -### reduce.bundle(b, opts) -Return a transform: -* input: [`vinyl-fs#src`] -* output: `b.bundle()` - -**b** - -[`depsify`] instance. - -**opts** - -Options passed to `reduce.bundler`. - -### reduce.watch(b, opts, watchOpts) -Return a transform: -* input: [`vinyl-fs#src`]. -* output: actually no data flows out, - but you can listen to the `bundle` event (triggered on the returned transform) - to process the result of `b.bundle()`. - -`b` and `opts` are the same with `reduce.bundle(b, opts)` - -**watchOpts** - -Options passed to [`watchify2`]. - -To detect new entries, -provide a glob to detect entries as `watchOpts.entryGlob`. - -### reduce.src(patterns, opts) -Same with [`vinyl-fs#src`], except that `opts.read` defaults to `false`. +### b.bundle() +Return a [`vinyl`] stream, +which can be processed by gulp plugins. -### reduce.dest(outFolder, opts, urlOpts) -`outFolder` and `opts` are passed to [`vinyl-fs#dest`] directly. - -[`postcss-custom-url`] is used to transform `url()` expressions in css (paths transformed, assets copied or inlined). - -The actual processor is constructed as: ```js -const url = require('postcss-custom-url') -const postcss = require('postcss') -const urlProcessor = postcss(url([ - [ url.util.inline, urlOpts ], - [ url.util.copy, urlOpts ], -])) +b.bundle().pipe(require('gulp-uglifycss')()).pipe(b.dest('build')) ``` -### reduce.bundler(b, opts) -Plugin for creating common shared bundles. - -**opts** - -Default: `{}` +### b.dest(outFolder, urlTransformOptions) +Works almost the same with [`gulp.dest`], +except that file contents are transformed using [`postcss-custom-url`] +before written to disk. -* `Function` or `Array`: `b.plugin(opts)` will be executed. - Used to replace the default bundler [`common-bundle`]. -* `String`: all modules are packed into a single bundle, with `opts` the file path. -* otherwise: `opts` is passed to [`common-bundle`] directly. +`urlTransformOptions` is passed to both +the [inline](https://github.com/reducejs/postcss-custom-url#inline) +and [copy](https://github.com/reducejs/postcss-custom-url#copy) +transformers for [`postcss-custom-url`]. +The actual processor: ```js -const reduce = require('reduce-css') -const path = require('path') - -const b = reduce.create({ - entries: ['a.css', 'b.css'], - basedir: '/path/to/src', -}) -b.plugin(reduce.bundler, 'bundle.css') -b.bundle().pipe(reduce.dest('build')) - -``` - -### reduce.watcher(b, opts) -Plugin for watching file changes, addition and deletion. - -`opts` is passed to [`watchify2`] directly. - -A `bundle-stream` event is triggered whenever `b.bundle()` is provoked. - -```js -const reduce = require('reduce-css') -const path = require('path') -const b = reduce.create({ - entries: ['a.css', 'b.css'], - basedir: '/path/to/src', - cache: {}, - packageCache: {}, -}) -b.plugin(reduce.bundler, 'bundle.css') -b.plugin(reduce.watcher, { entryGlob: '*.css' }) -b.on('bundle-stream', function (bundleStream) { - // bundleStream is the result of `b.bundle()` - bundleStream.pipe(reduce.dest('build')) -}) -b.start() +var url = require('postcss-custom-url') +var postcss = require('postcss') +var urlProcessor = postcss(url([ + [ url.util.inline, urlTransformOptions ], + [ url.util.copy, urlTransformOptions ], +])) ``` @@ -293,6 +172,7 @@ b.start() [`gulp`]: https://www.npmjs.com/package/gulp [`watchify2`]: https://github.com/reducejs/watchify2 [`postcss-custom-url`]: https://github.com/reducejs/postcss-custom-url +[`vinyl`]: https://github.com/gulpjs/vinyl [`vinyl-fs#src`]: https://github.com/gulpjs/vinyl-fs#srcglobs-options -[`vinyl-fs#dest`]: https://github.com/gulpjs/vinyl-fs#destfolder-options -[`factor-bundle`]: https://www.npmjs.com/package/factor-bundle +[`gulp.dest`]: https://github.com/gulpjs/vinyl-fs#destfolder-options +[`globby`]: https://github.com/sindresorhus/globby diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 0000000..0a80a67 --- /dev/null +++ b/example/.gitignore @@ -0,0 +1,2 @@ +/node_modules/* +!/node_modules/reduce-css diff --git a/example/gulp/multi/gulpfile.js b/example/gulp/multi/gulpfile.js deleted file mode 100644 index 4b6f73f..0000000 --- a/example/gulp/multi/gulpfile.js +++ /dev/null @@ -1,61 +0,0 @@ -'use strict' - -const reduce = require('reduce-css') -const gulp = require('gulp') -const del = require('del') -const path = require('path') -const build = path.join(__dirname, 'build') - -gulp.task('clean', function () { - return del(build) -}) - -gulp.task('build', ['clean'], function () { - let basedir = path.join(__dirname, 'src') - let b = reduce.create({ - basedir, - resolve: { - paths: [path.join(__dirname, 'src', 'web_modules')], - }, - }) - return reduce.src('page/**/index.css', { cwd: basedir }) - .pipe(reduce.bundle(b, { - groups: 'page/**/index.css', - common: 'common.css', - })) - .pipe(reduce.dest(build, null, { - maxSize: 0, - name: '[name].[hash]', - assetOutFolder: path.join(build, 'assets'), - })) -}) - -gulp.task('watch', ['clean'], function () { - let basedir = path.join(__dirname, 'src') - let b = reduce.create({ - basedir, - resolve: { - paths: [path.join(__dirname, 'src', 'web_modules')], - }, - cache: {}, - packageCache: {}, - }) - let count = 1 - return gulp.src('page/**/index.css', { cwd: basedir }) - .pipe(reduce.watch(b, { - groups: 'page/**/index.css', - common: 'common.css', - }, { entryGlob: 'page/**/index.css' })) - .on('bundle', function (bundleStream) { - bundleStream.pipe(reduce.dest(build, null, { - maxSize: 0, - name: '[name].[hash]', - assetOutFolder: path.join(build, 'assets'), - })) - .on('data', file => console.log('bundle:', file.relative)) - .once('end', function () { - console.log('-'.repeat(40), count++ + '') - }) - }) -}) - diff --git a/example/gulp/reduce/gulpfile.js b/example/gulp/reduce/gulpfile.js deleted file mode 100644 index 21f4f70..0000000 --- a/example/gulp/reduce/gulpfile.js +++ /dev/null @@ -1,58 +0,0 @@ -'use strict' - -const reduce = require('reduce-css') -const gulp = require('gulp') -const del = require('del') -const path = require('path') -const build = path.join(__dirname, 'build') - -gulp.task('clean', function () { - return del(build) -}) - -gulp.task('build', ['clean'], function () { - let basedir = path.join(__dirname, 'src') - let b = reduce.create({ - basedir, - resolve: { - paths: [path.join(__dirname, 'src', 'web_modules')], - }, - }) - return reduce.src('page/**/index.css', { cwd: basedir }) - .pipe(reduce.bundle(b, 'bundle.css')) - .pipe(reduce.dest(build, null, { - maxSize: 0, - name: '[name].[hash]', - assetOutFolder: path.join(build, 'assets'), - })) -}) - -gulp.task('watch', ['clean'], function () { - let basedir = path.join(__dirname, 'src') - let b = reduce.create({ - basedir, - resolve: { - paths: [path.join(__dirname, 'src', 'web_modules')], - }, - cache: {}, - packageCache: {}, - }) - let count = 1 - return gulp.src('page/**/index.css', { cwd: basedir }) - .pipe(reduce.watch(b, 'bundle.css', { entryGlob: 'page/**/index.css' })) - .on('bundle', function (bundleStream) { - bundleStream.pipe(reduce.dest(build, null, { - maxSize: 0, - name: '[name].[hash]', - assetOutFolder: path.join(build, 'assets'), - })) - .on('data', file => console.log('bundle:', file.relative)) - .once('end', function () { - console.log('-'.repeat(40), count++ + '') - if (count > 3) { - b.close() - } - }) - }) -}) - diff --git a/example/gulp/reduce/src/page/blue/index.css b/example/gulp/reduce/src/page/blue/index.css deleted file mode 100644 index 38c8213..0000000 --- a/example/gulp/reduce/src/page/blue/index.css +++ /dev/null @@ -1,6 +0,0 @@ -@external "reset"; -@import "helper/color"; -.blue { - color: $blue; -} - diff --git a/example/gulp/reduce/src/page/red/index.css b/example/gulp/reduce/src/page/red/index.css deleted file mode 100644 index ed3ef7b..0000000 --- a/example/gulp/reduce/src/page/red/index.css +++ /dev/null @@ -1,11 +0,0 @@ -@external "reset"; -@external "component/button"; -@import "helper/color"; -.red { - color: $red; -} - -.button { - background-color: $red; -} - diff --git a/example/gulp/reduce/src/web_modules/component/button/index.css b/example/gulp/reduce/src/web_modules/component/button/index.css deleted file mode 100644 index b1e000e..0000000 --- a/example/gulp/reduce/src/web_modules/component/button/index.css +++ /dev/null @@ -1,5 +0,0 @@ -@import "helper/color"; -.button { - background-color: $green; - background-image: url(button.png); -} diff --git a/example/package.json b/example/package.json new file mode 100644 index 0000000..9f43560 --- /dev/null +++ b/example/package.json @@ -0,0 +1,7 @@ +{ + "name": "example", + "dependencies": { + "gulp": "^3.9.0", + "gulp-util": "^3.0.7" + } +} diff --git a/example/reduce/README.md b/example/reduce/README.md new file mode 100644 index 0000000..8c0b2d4 --- /dev/null +++ b/example/reduce/README.md @@ -0,0 +1,88 @@ +# Example + +Suppose we want to pack css in `/path/to/src` (not including those in its subdirectories) into `/path/to/build/bundle.css`. + +There are already `blue.css` and `red.css` in `/path/to/src`, and they both depend upon `/path/to/src/node_modules/reset/index.js`. + +## Input + +`blue.css`: +```css +@external "reset"; +@import "color"; +.blue { + color: $blue; +} + +``` + +`red.css`: +```css +@external "reset"; +@external "./button"; +@import "color"; +.red { + color: $red; +} + +``` + +`reset` contains styles to be shared. +We use `@external` to declare that +it should come before `a.css` and `b.css` in the final `bundle.css`. +```css +html, body { + margin: 0; + padding: 0; +} + +``` + +The `color` module is installed in `node_modules`, +and will be consumed by [`postcss`] when `@import`ed in css. +```css +$red: #FF0000; +$green: #00FF00; +$blue: #0000FF; + +``` + +`/path/to/src/button` is a button component with a background image (`/path/to/src/button/button.png`), +as well as some styles (`/path/to/src/button/index.css`): +```css +@import "color"; +.button { + background-color: $red; + background-image: url(button.png); +} + +``` +The image will be inlined or copied to the build directory +after bundling, and the url in css will also be transformed to +reference to it correctly. + +## Output + +`/path/to/build/bundle.css`: +```css +html, body { + margin: 0; + padding: 0; +} + +.blue { + color: #0000FF; +} + +.button { + background-color: #FF0000; + background-image: url(assets/button.161fff2.png); +} +.red { + color: #FF0000; +} + +``` + +The background image has been renamed and copied to `/path/to/build/assets/button.161fff2.png`. + diff --git a/example/reduce/build.js b/example/reduce/build.js new file mode 100644 index 0000000..2b7a4a4 --- /dev/null +++ b/example/reduce/build.js @@ -0,0 +1,80 @@ +var reduce = require('reduce-css') +var del = require('del') +var path = require('path') +var Transform = require('stream').Transform + +var basedir = path.join(__dirname, 'src') + +var i = process.argv.indexOf('-w') +if (i === -1) { + i = process.argv.indexOf('--watch') +} +var needWatch = i > -1 +if (needWatch) { + var b = createBundler(true) + b.on('update', function update() { + bundle(b) + return update + }()) +} else { + bundle(createBundler()) +} + +function createBundler(watch) { + var basedir = path.join(__dirname, 'src') + var b = reduce.create( + /* glob for entries */ + '*.css', + + /* options for depsify */ + { + basedir, + cache: {}, + packageCache: {}, + }, + + /* options for common-bundle */ + // single bundle + // 'bundle.css', + // multiple bundles + { + groups: '*.css', + common: 'common.css', + }, + + /* options for watchify2 */ + watch && { entryGlob: '*.css' } + ) + return b +} + +function bundle(b) { + var startTime = Date.now() + log('Start bundling') + var build = path.join(__dirname, 'build') + del.sync(build) + return b.bundle().on('error', log) + .pipe(Transform({ + objectMode: true, + transform: function (file, enc, next) { + log('-', file.relative, file.contents.length, 'bytes') + next(null, file) + } + })) + .pipe(b.dest(build, { + maxSize: 0, + name: '[name].[hash]', + assetOutFolder: path.join(build, 'assets'), + })) + .on('end', () => log('End bundling in', Date.now() - startTime, 'ms')) +} + +function log() { + console.log.apply(console, [].map.call(arguments, function (msg) { + if (typeof msg === 'string') { + return msg + } + return JSON.stringify(msg, null, 2) + })) +} + diff --git a/example/without-gulp/src/blue.css b/example/reduce/src/blue.css similarity index 100% rename from example/without-gulp/src/blue.css rename to example/reduce/src/blue.css diff --git a/example/gulp/multi/src/web_modules/component/button/button.png b/example/reduce/src/button/button.png similarity index 100% rename from example/gulp/multi/src/web_modules/component/button/button.png rename to example/reduce/src/button/button.png diff --git a/example/without-gulp/src/button/index.css b/example/reduce/src/button/index.css similarity index 100% rename from example/without-gulp/src/button/index.css rename to example/reduce/src/button/index.css diff --git a/example/gulp/multi/src/web_modules/helper/color/index.css b/example/reduce/src/node_modules/color/index.css similarity index 100% rename from example/gulp/multi/src/web_modules/helper/color/index.css rename to example/reduce/src/node_modules/color/index.css diff --git a/example/gulp/multi/src/node_modules/reset/index.css b/example/reduce/src/node_modules/reset/index.css similarity index 100% rename from example/gulp/multi/src/node_modules/reset/index.css rename to example/reduce/src/node_modules/reset/index.css diff --git a/example/without-gulp/src/red.css b/example/reduce/src/red.css similarity index 100% rename from example/without-gulp/src/red.css rename to example/reduce/src/red.css diff --git a/example/without-gulp/build.js b/example/without-gulp/build.js deleted file mode 100644 index 53918c3..0000000 --- a/example/without-gulp/build.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict' - -const del = require('del') -const reduce = require('reduce-css') - -const build = __dirname + '/build' -const basedir = __dirname + '/src' -const b = reduce.create({ basedir }) -del(build).then(function () { - reduce.src('*.css', { cwd: basedir }) - .pipe(reduce.bundle(b, 'bundle.css')) - .pipe(reduce.dest(build, null, { - maxSize: 0, - name: '[name].[hash]', - assetOutFolder: build + '/assets', - })) -}) - diff --git a/example/without-gulp/multi.js b/example/without-gulp/multi.js deleted file mode 100644 index fe082f4..0000000 --- a/example/without-gulp/multi.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict' - -const del = require('del') -const reduce = require('reduce-css') - -const build = __dirname + '/build' -const basedir = __dirname + '/src' -const b = reduce.create({ basedir }) -b.on('common.map', function (map) { - console.log('bundles:', Object.keys(map).join(', ')) -}) -del(build).then(function () { - reduce.src('*.css', { cwd: basedir }) - .pipe(reduce.bundle(b, { - groups: '*.css', - common: 'common.css', - })) - .pipe(reduce.dest(build, null, { - maxSize: 0, - name: '[name].[hash]', - assetOutFolder: build + '/assets', - })) -}) - diff --git a/example/without-gulp/src/button/button.png b/example/without-gulp/src/button/button.png deleted file mode 100644 index 872000b..0000000 Binary files a/example/without-gulp/src/button/button.png and /dev/null differ diff --git a/example/without-gulp/src/node_modules/color/index.css b/example/without-gulp/src/node_modules/color/index.css deleted file mode 100644 index 6049190..0000000 --- a/example/without-gulp/src/node_modules/color/index.css +++ /dev/null @@ -1,3 +0,0 @@ -$red: #FF0000; -$green: #00FF00; -$blue: #0000FF; diff --git a/example/without-gulp/src/node_modules/reset/index.css b/example/without-gulp/src/node_modules/reset/index.css deleted file mode 100644 index 20e182a..0000000 --- a/example/without-gulp/src/node_modules/reset/index.css +++ /dev/null @@ -1,5 +0,0 @@ -html, body { - margin: 0; - padding: 0; -} - diff --git a/example/without-gulp/watch.js b/example/without-gulp/watch.js deleted file mode 100644 index 1aef676..0000000 --- a/example/without-gulp/watch.js +++ /dev/null @@ -1,27 +0,0 @@ -'use strict' - -const del = require('del') -const reduce = require('reduce-css') - -const build = __dirname + '/build' -const basedir = __dirname + '/src' -const b = reduce.create({ - basedir, - cache: {}, - packageCache: {}, -}) - -del(build).then(function () { - reduce.src('*.css', { cwd: basedir }) - .pipe(reduce.watch(b, 'bundle.css', { entryGlob: '*.css' })) - .on('bundle', function (bundleStream) { - bundleStream.pipe(reduce.dest(build, null, { - maxSize: 0, - name: '[name].[hash]', - assetOutFolder: build + '/assets', - })) - .on('data', file => console.log('bundle:', file.relative)) - .on('end', () => console.log('-'.repeat(40))) - }) -}) - diff --git a/example/work-with-gulp/gulpfile.js b/example/work-with-gulp/gulpfile.js new file mode 100644 index 0000000..a6f4122 --- /dev/null +++ b/example/work-with-gulp/gulpfile.js @@ -0,0 +1,83 @@ +'use strict' + +const reduce = require('reduce-css') +const gulp = require('gulp') +const del = require('del') +const path = require('path') +const gutil = require('gulp-util') +const Transform = require('stream').Transform + +gulp.task('build', function () { + return bundle(createBundler()) +}) + +gulp.task('watch', function (cb) { + var b = createBundler(true) + b.on('update', function update() { + bundle(b) + return update + }()) + b.on('close', cb) +}) + +function createBundler(watch) { + var basedir = path.join(__dirname, 'src') + var b = reduce.create( + /* glob for entries */ + 'page/**/index.css', + + /* options for depsify */ + { + basedir, + resolve: { + paths: [path.join(__dirname, 'src', 'web_modules')], + }, + cache: {}, + packageCache: {}, + }, + + /* options for common-bundle */ + // single bundle + // 'bundle.css', + // multiple bundles + { + groups: 'page/**/index.css', + common: 'common.css', + }, + + /* options for watchify2 */ + watch && { entryGlob: 'page/**/index.css' } + ) + return b +} + +function bundle(b) { + var startTime = Date.now() + log('Start bundling') + var build = path.join(__dirname, 'build') + del.sync(build) + return b.bundle().on('error', log) + .pipe(Transform({ + objectMode: true, + transform: function (file, enc, next) { + log('-', file.relative, file.contents.length, 'bytes') + next(null, file) + } + })) + .pipe(b.dest(build, { + maxSize: 0, + name: '[name].[hash]', + assetOutFolder: path.join(build, 'assets'), + })) + .on('end', () => log('End bundling in', Date.now() - startTime, 'ms')) +} + +function log() { + gutil.log.apply(gutil, [].map.call(arguments, function (msg) { + if (typeof msg === 'string') { + return msg + } + return JSON.stringify(msg, null, 2) + })) +} + diff --git a/example/gulp/reduce/src/node_modules/reset/index.css b/example/work-with-gulp/src/node_modules/reset/index.css similarity index 100% rename from example/gulp/reduce/src/node_modules/reset/index.css rename to example/work-with-gulp/src/node_modules/reset/index.css diff --git a/example/gulp/multi/src/page/blue/index.css b/example/work-with-gulp/src/page/blue/index.css similarity index 100% rename from example/gulp/multi/src/page/blue/index.css rename to example/work-with-gulp/src/page/blue/index.css diff --git a/example/gulp/multi/src/page/red/index.css b/example/work-with-gulp/src/page/red/index.css similarity index 100% rename from example/gulp/multi/src/page/red/index.css rename to example/work-with-gulp/src/page/red/index.css diff --git a/example/gulp/reduce/src/web_modules/component/button/button.png b/example/work-with-gulp/src/web_modules/component/button/button.png similarity index 100% rename from example/gulp/reduce/src/web_modules/component/button/button.png rename to example/work-with-gulp/src/web_modules/component/button/button.png diff --git a/example/gulp/multi/src/web_modules/component/button/index.css b/example/work-with-gulp/src/web_modules/component/button/index.css similarity index 100% rename from example/gulp/multi/src/web_modules/component/button/index.css rename to example/work-with-gulp/src/web_modules/component/button/index.css diff --git a/example/gulp/reduce/src/web_modules/helper/color/index.css b/example/work-with-gulp/src/web_modules/helper/color/index.css similarity index 100% rename from example/gulp/reduce/src/web_modules/helper/color/index.css rename to example/work-with-gulp/src/web_modules/helper/color/index.css diff --git a/index.js b/index.js index e320e05..936fba0 100644 --- a/index.js +++ b/index.js @@ -1,171 +1,132 @@ 'use strict' -const stream = require('stream') -const vfs = require('vinyl-fs') -const postcss = require('postcss') -const url = require('postcss-custom-url') -const File = require('vinyl') -const combine = require('stream-combiner2') -const buffer = require('vinyl-buffer') -const Depsify = require('depsify') +var Stream = require('stream') +var vfs = require('vinyl-fs') +var PostCSS = require('postcss') +var url = require('postcss-custom-url') +var combine = require('stream-combiner2') +var buffer = require('vinyl-buffer') +var Depsify = require('depsify') +var path = require('path') +var glob = require('globby') +var sink = require('./lib/sink') -function bundler(b, opts) { - if (typeof opts === 'function' || Array.isArray(opts)) { - return b.plugin(opts) - } - - opts = opts || {} - if (typeof opts === 'string') { - opts = { groups: { output: opts } } - } +function through(write, end) { + return Stream.Transform({ + objectMode: true, + transform: write || function (o, enc, next) { next(null, o) }, + flush: end, + }) +} - let urlProcessor = postcss(url) - opts.pack = function (options) { - let pipeline = b.pack() - pipeline.pop() - pipeline.push( +function bundler(b, opts) { + var urlProcessor = PostCSS(url) + b.on('common.pipeline', function (bundleFile, pipeline) { + var pack = b.pack() + pack.pop() + pack.push( through(function (row, _, next) { urlProcessor.process(row.source, { from: row.file, - to: options.to, + to: bundleFile, }).then(function (result) { next(null, result.css) }) }) ) - return pipeline - } + var packPipeline = pipeline.get('pack') + packPipeline.splice.apply(packPipeline, [0, 1].concat(pack)) + }) b.plugin(require('common-bundle'), opts) -} - -function through(write, end) { - return stream.Transform({ - objectMode: true, - transform: write, - flush: end, + b.on('reset', function reset() { + b.pipeline.push(buffer()) + return reset + }()) + b.on('bundle', output => { + output.on('error', err => delete err.stream) }) } -function watcher(b, wopts) { - b.plugin(require('watchify2'), wopts) - let close = b.close +function watchify(b, opts) { + b.plugin(require('watchify2'), opts) + var close = b.close b.close = function () { close() b.emit('close') } - b.start = function () { - b.emit('bundle-stream', b.bundle()) - } - b.on('update', b.start) } -function bundle(b, opts) { - b.plugin(bundler, opts) +function urlify(outFolder, urlOpts) { + var urlProcessor = PostCSS(url([ + [ url.util.inline, urlOpts ], + [ url.util.copy, urlOpts ], + ])) - return through( - function (file, enc, next) { - b.add(file.path) - next() - }, - function (next) { - b.bundle() - .on('data', data => this.push(data)) - .on('end', next) - } - ) + return through(function (file, _, next) { + urlProcessor.process(file.contents.toString('utf8'), { + from: file.path, + to: path.resolve(outFolder, file.relative), + }).then(function (result) { + file.contents = Buffer(result.css) + next(null, file) + }, err => this.emit('error', err)) + }) } -function watch(b, opts, wopts) { - b.plugin(bundler, opts) - b.plugin(watcher, wopts) +function postcss(b, opts) { + b.plugin(require('reduce-css-postcss'), { + processorFilter: function (pipeline) { + pipeline.get('postcss-simple-import').push({ + resolve: b._options.resolve, + }) - return through( - function (file, enc, next) { - b.add(file.path) - next() - }, - function (next) { - b.on('bundle-stream', s => this.emit('bundle', s)) - b.once('close', next) - b.start() - } - ) -} + if (typeof opts === 'function') { + return opts(pipeline) + } -function src(pattern, opts) { - opts = opts || {} - opts.read = false - return vfs.src(pattern, opts) -} - -function dest(outFolder, outOpts, urlOpts) { - let files = [] - let emptyFiles = [] - let urlProcessor = postcss(url([ - [ url.util.inline, urlOpts ], - [ url.util.copy, urlOpts ], - ])) - return combine.obj( - buffer(), - through(function (file, _, next) { - if (file.isNull()) return next() - files.push(file) - let f = new File({ - cwd: file.cwd, - base: file.base, - path: file.path, - contents: null, - }) - emptyFiles.push(f) - next(null, f) - }), - vfs.dest(outFolder, outOpts), - through(function (file, _, next) { - let i = emptyFiles.indexOf(file) - let writePath = file.path - file = files[i] - urlProcessor.process( - file.contents.toString('utf8'), - { from: file.path, to: writePath } - ).then(function (result) { - file.contents = Buffer(result.css) - next(null, file) - }, err => this.emit('error', err)) - }), - vfs.dest(outFolder, outOpts) - ) + pipeline.push.apply( + pipeline, [].concat(opts).filter(Boolean) + ) + }, + }) } -function create(opts) { +function create(entries, opts, bundleOptions, watchOpts) { + if (typeof entries !== 'string' && !Array.isArray(entries)) { + bundleOptions = opts + opts = entries + entries = null + } opts = opts || {} - let b = new Depsify(Object.assign({ atRuleName: 'external' }, opts)) + var b = new Depsify(Object.assign({ atRuleName: 'external' }, opts)) if (opts.postcss !== false) { - b.plugin(require('reduce-css-postcss'), { - processorFilter: function (pipeline) { - pipeline.get('postcss-simple-import').push({ - resolve: b._options.resolve, - }) - - if (typeof opts.postcss === 'function') { - return opts.postcss(pipeline) - } - - pipeline.push.apply( - pipeline, [].concat(opts.postcss).filter(Boolean) - ) - }, - }) + b.plugin(postcss, opts.postcss) + } + if (entries) { + glob.sync(entries, { cwd: b._options.basedir }) + .forEach(function (file) { + b.add(file) + }) + } + b.plugin(bundler, bundleOptions) + if (watchOpts) { + b.plugin(watchify, typeof watchOpts === 'object' ? watchOpts : {}) + } + b.dest = function (outFolder, urlOpts) { + var output = combine.obj( + urlify(outFolder, urlOpts), + vfs.dest(outFolder) + ) + process.nextTick(sink(output)) + return output } return b } module.exports = { bundler, - watcher, - bundle, - watch, - dest, - src, + watchify, + urlify, create, } diff --git a/lib/sink.js b/lib/sink.js new file mode 100644 index 0000000..70eb50e --- /dev/null +++ b/lib/sink.js @@ -0,0 +1,49 @@ +'use strict' + +var Writable = require('stream').Writable + +function listenerCount(stream, evt) { + return stream.listeners(evt).length +} + +function hasListeners(stream) { + return !!(listenerCount(stream, 'readable') || listenerCount(stream, 'data')) +} + +function sink(stream) { + var sinkAdded = false + var sinkStream = new Writable({ objectMode: true }) + sinkStream._write = function (file, enc, cb) { cb() } + + function addSink() { + if (sinkAdded) { + return + } + + if (hasListeners(stream)) { + return + } + + sinkAdded = true + stream.pipe(sinkStream) + } + + function removeSink(evt) { + if (evt !== 'readable' && evt !== 'data') { + return + } + + if (hasListeners(stream)) { + sinkAdded = false + stream.unpipe(sinkStream) + } + } + + stream.on('newListener', removeSink) + stream.on('removeListener', removeSink) + stream.on('removeListener', addSink) + + return addSink +} + +module.exports = sink diff --git a/package.json b/package.json index 293e7c3..96276b6 100644 --- a/package.json +++ b/package.json @@ -30,13 +30,13 @@ }, "homepage": "https://github.com/reducejs/reduce-css#readme", "dependencies": { - "common-bundle": "^0.4.0", + "common-bundle": "^0.5.0", "depsify": "^4.0.0", + "globby": "^4.1.0", "postcss": "^5.0.5", "postcss-custom-url": "^4.0.0", "reduce-css-postcss": "^3.0.0", "stream-combiner2": "^1.1.1", - "vinyl": "^1.1.0", "vinyl-buffer": "^1.0.0", "vinyl-fs": "^2.2.1", "watchify2": "^0.1.0" @@ -45,10 +45,7 @@ "compare-directory": "^1.0.1", "del": "^2.0.2", "eslint": "^2.1.0", - "gulp": "^3.9.0", "mkdirp": "^0.5.1", - "postcss-advanced-variables": "^1.2.2", - "postcss-simple-import": "^3.0.0", "tap": "^5.0.0" } } diff --git a/test/multiple-bundle.js b/test/multiple-bundle.js index 44f6612..fcee41d 100644 --- a/test/multiple-bundle.js +++ b/test/multiple-bundle.js @@ -7,28 +7,29 @@ const del = require('del') const compare = require('compare-directory') const fixtures = path.resolve.bind(path, __dirname, 'fixtures') -const dest = fixtures.bind(null, 'build', 'multiple-bundles') -const expect = fixtures.bind(null, 'expected', 'multiple-bundles') +const build = fixtures('build', 'multiple-bundles') +const expect = fixtures('expected', 'multiple-bundles') test('multiple bundles', function(t) { - let basedir = fixtures('src') - let b = reduce.create({ basedir }) - del(dest()).then(function () { - reduce.src('*.css', { cwd: basedir }) - .pipe(reduce.bundle(b, { + del(build).then(function () { + var basedir = fixtures('src') + var b = reduce.create( + '*.css', + { basedir }, + { groups: '+(a|b).css', common: 'common.css', - })) - .pipe(reduce.dest(dest(), null, { - maxSize: 0, - useHash: true, - assetOutFolder: fixtures('build', 'multiple-bundles', 'images'), - })) - .on('data', () => {}) - .on('end', function () { - compare(t, ['**/*.css', '**/*.png'], dest(), expect()) - t.end() - }) + } + ) + b.bundle().pipe(b.dest(build, { + maxSize: 0, + useHash: true, + assetOutFolder: fixtures('build', 'multiple-bundles', 'images'), + })) + .on('end', function () { + compare(t, ['**/*.css', '**/*.png'], build, expect) + t.end() + }) }) }) diff --git a/test/single-bundle.js b/test/single-bundle.js index 42c9ebe..56929f0 100644 --- a/test/single-bundle.js +++ b/test/single-bundle.js @@ -7,24 +7,25 @@ const del = require('del') const compare = require('compare-directory') const fixtures = path.resolve.bind(path, __dirname, 'fixtures') -const dest = fixtures.bind(null, 'build', 'single-bundle') -const expect = fixtures.bind(null, 'expected', 'single-bundle') +const build = fixtures('build', 'single-bundle') +const expect = fixtures('expected', 'single-bundle') test('single bundle', function(t) { - let basedir = fixtures('src') - let b = reduce.create({ basedir }) - del(dest()).then(function () { - reduce.src('*.css', { cwd: basedir }) - .pipe(reduce.bundle(b, 'common.css')) - .pipe(reduce.dest(dest(), null, { - maxSize: 0, - assetOutFolder: fixtures('build', 'single-bundle', 'images'), - })) - .on('data', () => {}) - .on('end', function () { - compare(t, ['**/*.css', '**/*.png'], dest(), expect()) - t.end() - }) + del(build).then(function () { + var basedir = fixtures('src') + var b = reduce.create( + '*.css', + { basedir }, + 'common.css' + ) + b.bundle().pipe(b.dest(build, { + maxSize: 0, + assetOutFolder: fixtures('build', 'single-bundle', 'images'), + })) + .on('end', function () { + compare(t, ['**/*.css', '**/*.png'], build, expect) + t.end() + }) }) }) diff --git a/test/source-entries.js b/test/source-entries.js deleted file mode 100644 index bf27862..0000000 --- a/test/source-entries.js +++ /dev/null @@ -1,47 +0,0 @@ -'use strict' - -const test = require('tap').test -const reduce = require('..') -const path = require('path') -const fixtures = path.resolve.bind(path, __dirname, 'fixtures') -const fs = require('fs') -const del = require('del') -const DEST = fixtures('build', 'common.css') - -test('source entries', function(t) { - let b = reduce.create({ - entries: [ - { - file: '/a', - source: '', - }, - '/b', - ], - fileCache: { - '/b': 'b{}', - '/c': 'c{}', - '/d': 'd{}', - }, - resolve: function (file, parent) { - return path.resolve(parent.basedir, file) - }, - dependenciesFilter: function (deps, file) { - var base = path.basename(file) - return base === 'a' ? ['/c'] : ['/d'] - }, - }) - del(DEST).then(function () { - b.plugin(reduce.bundler, 'common.css') - b.bundle() - .pipe(reduce.dest(fixtures('build'))) - .on('data', () => {}) - .on('end', function () { - t.equal( - fs.readFileSync(DEST, 'utf8'), - 'd{}c{}b{}' - ) - t.end() - }) - }) -}) - diff --git a/test/watch.js b/test/watch.js index 6059540..fae1b84 100644 --- a/test/watch.js +++ b/test/watch.js @@ -48,19 +48,20 @@ entries.forEach(write) test('watch', function(t) { let count = 3 - let b = reduce.create({ basedir: src() }) - - reduce.src(['a.css', 'b.css'], { cwd: src() }) - .pipe(reduce.watch(b, { + let b = reduce.create( + ['a.css', 'b.css'], + { basedir: src() }, + { common: 'c.css', groups: '+(a|b).css', - })) - .on('bundle', function (bundleStream) { - bundleStream.pipe(reduce.dest(dest())) - .on('data', () => {}) - .once('finish', () => setTimeout(next, 50)) - }) - + }, + true + ) + b.on('update', function update() { + b.bundle().pipe(b.dest(dest())) + .once('end', () => setTimeout(next, 50)) + return update + }()) function next() { t.equal(