├── .gitignore ├── .npmrc ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.js ├── package.json └── test ├── expected-sf-static.css ├── expected-static.css ├── expected.css ├── index.js ├── source-dynamic-2.js ├── source-dynamic.js ├── source-sf-static.js ├── source-static.js └── source.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | tmp/ 4 | npm-debug.log* 5 | .DS_Store 6 | .nyc_output/ 7 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: linux 2 | dist: bionic 3 | language: node_js 4 | node_js: 5 | - 12 6 | - 10 7 | - 8 8 | script: "npm run test:cov" 9 | 10 | jobs: 11 | include: 12 | - node_js: stable 13 | after_script: | 14 | npm i -g codecov 15 | npx nyc report --reporter=text-lcov | codecov --pipe 16 | # Node.js 6 is not supported by nyc, so run tests without code coverage 17 | - node_js: 6 18 | script: npm run test 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # css-extract change log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | This project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## 2.0.0 8 | * Update dependencies. ([@goto-bus-stop][] in [#16][]) 9 | * Add test for `insertCss(someDynamicValue)`. ([@ahdinosaur][] in [#12][]) 10 | 11 | [@goto-bus-stop]: https://github.com/goto-bus-stop 12 | [@ahdinosaur]: https://github.com/ahdinosaur 13 | [#12]: https://github.com/stackcss/css-extract/pull/12 14 | [#16]: https://github.com/stackcss/css-extract/pull/16 15 | 16 | ## 1.3.1 17 | * Update static-module. ([@s3ththompson][] in [#15][]) 18 | * Update stability badge to stable. 19 | 20 | [@s3ththompson]: https://github.com/s3ththompson 21 | [#15]: https://github.com/stackcss/css-extract/pull/15 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Yoshua Wuyts 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # css-extract [![stability][0]][1] 2 | [![npm version][2]][3] [![build status][4]][5] [![test coverage][6]][7] 3 | [![downloads][8]][9] [![js-standard-style][10]][11] 4 | 5 | Looks up `require('insert-css')` calls to extract CSS from a browserify bundle 6 | to a file. Useful with `sheetify` or any other package / transform that uses 7 | `insert-css`. 8 | 9 | ## Command line 10 | ```sh 11 | $ browserify -t sheetify/transform -p [ css-extract -o bundle.css ] index.js \ 12 | -o bundle.js 13 | ``` 14 | 15 | ## JS api 16 | ```js 17 | const browserify = require('browserify') 18 | 19 | browserify() 20 | .transform('sheetify/transform') 21 | .plugin('css-extract', { out: 'bundle.css' }) 22 | .bundle() 23 | ``` 24 | 25 | ```js 26 | const browserify = require('browserify') 27 | 28 | browserify() 29 | .transform('sheetify/transform') 30 | .plugin('css-extract', { out: createWriteStream }) 31 | .bundle() 32 | 33 | function createWriteStream () { 34 | return process.stdout 35 | } 36 | ``` 37 | 38 | ## Options 39 | - `-o` / `--out`: specify an outfile, defaults to `bundle.css`. Can also be a 40 | function that returns a writable stream from the JavaScript API. 41 | 42 | ## Installation 43 | ```sh 44 | $ npm install css-extract 45 | ``` 46 | 47 | ## See Also 48 | - [sheetify](https://github.com/stackcss/sheetify) 49 | - [insert-css](https://github.com/substack/insert-css) 50 | 51 | ## License 52 | [MIT](https://tldrlegal.com/license/mit-license) 53 | 54 | [0]: https://img.shields.io/badge/stability-stable-green.svg?style=flat-square 55 | [1]: https://nodejs.org/api/documentation.html#documentation_stability_index 56 | [2]: https://img.shields.io/npm/v/css-extract.svg?style=flat-square 57 | [3]: https://npmjs.org/package/css-extract 58 | [4]: https://img.shields.io/travis/stackcss/css-extract/master.svg?style=flat-square 59 | [5]: https://travis-ci.org/stackcss/css-extract 60 | [6]: https://img.shields.io/codecov/c/github/stackcss/css-extract/master.svg?style=flat-square 61 | [7]: https://codecov.io/github/stackcss/css-extract 62 | [8]: http://img.shields.io/npm/dm/css-extract.svg?style=flat-square 63 | [9]: https://npmjs.org/package/css-extract 64 | [10]: https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square 65 | [11]: https://github.com/feross/standard 66 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const staticModule = require('static-module') 2 | const from2 = require('from2-string') 3 | const through = require('through2') 4 | const assert = require('assert') 5 | const d = require('defined') 6 | const bl = require('bl') 7 | const fs = require('fs') 8 | 9 | module.exports = cssExtract 10 | 11 | // Extract CSS from a browserify bundle 12 | // obj -> null 13 | function cssExtract (bundle, opts) { 14 | opts = opts || {} 15 | 16 | var outFile = opts.out || opts.o || 'bundle.css' 17 | var sourceMap = d(opts.sourceMap, bundle && bundle._options && bundle._options.debug, false) 18 | 19 | assert.strictEqual(typeof bundle, 'object', 'bundle should be an object') 20 | assert.strictEqual(typeof opts, 'object', 'opts should be an object') 21 | 22 | // every time .bundle is called, attach hook 23 | bundle.on('reset', addHooks) 24 | addHooks() 25 | 26 | function addHooks () { 27 | const extractStream = through.obj(write, flush) 28 | const writeStream = (typeof outFile === 'function') 29 | ? outFile() 30 | : bl(writeComplete) 31 | 32 | // run before the "label" step in browserify pipeline 33 | bundle.pipeline.get('label').unshift(extractStream) 34 | 35 | function write (chunk, enc, cb) { 36 | // Performance boost: don't do ast parsing unless we know it's needed 37 | if (!/(insert-css|sheetify\/insert)/.test(chunk.source)) { 38 | return cb(null, chunk) 39 | } 40 | 41 | var source = from2(chunk.source) 42 | var sm = staticModule({ 43 | 'insert-css': function (src) { 44 | writeStream.write(String(src) + '\n') 45 | return from2('null') 46 | }, 47 | 'sheetify/insert': function (src) { 48 | writeStream.write(String(src) + '\n') 49 | return from2('null') 50 | } 51 | }, { sourceMap: sourceMap }) 52 | 53 | source.pipe(sm).pipe(bl(complete)) 54 | 55 | function complete (err, source) { 56 | if (err) return extractStream.emit('error', err) 57 | chunk.source = String(source) 58 | cb(null, chunk) 59 | } 60 | } 61 | 62 | // close stream and signal end 63 | function flush (cb) { 64 | writeStream.end() 65 | cb() 66 | } 67 | 68 | function writeComplete (err, buffer) { 69 | if (err) return extractStream.emit('error', err) 70 | fs.writeFileSync(outFile, buffer) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "css-extract", 3 | "version": "2.0.0", 4 | "description": "Extract CSS from a browserify bundle", 5 | "main": "index.js", 6 | "scripts": { 7 | "deps": "dependency-check . && dependency-check . --extra --no-dev", 8 | "test": "standard && npm run deps && NODE_ENV=test node test", 9 | "test:cov": "standard && npm run deps && NODE_ENV=test nyc node test" 10 | }, 11 | "repository": "stackcss/css-extract", 12 | "keywords": [ 13 | "css", 14 | "extract", 15 | "browserify", 16 | "plugin", 17 | "transform", 18 | "sheetify", 19 | "css-modules" 20 | ], 21 | "license": "MIT", 22 | "dependencies": { 23 | "bl": "^4.0.2", 24 | "defined": "^1.0.0", 25 | "from2-string": "^1.1.0", 26 | "static-module": "^3.0.0", 27 | "through2": "^4.0.2" 28 | }, 29 | "devDependencies": { 30 | "browserify": "^16.5.2", 31 | "dependency-check": "^2.10.1", 32 | "insert-css": "^2.0.0", 33 | "nyc": "^15.1.0", 34 | "sheetify": "^8.0.0", 35 | "standard": "^14.3.4", 36 | "tape": "^5.0.1", 37 | "tmp": "^0.2.1" 38 | }, 39 | "files": [ 40 | "index.js", 41 | "bin/*" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /test/expected-sf-static.css: -------------------------------------------------------------------------------- 1 | .foo {background: green} 2 | -------------------------------------------------------------------------------- /test/expected-static.css: -------------------------------------------------------------------------------- 1 | .foo {background: green} -------------------------------------------------------------------------------- /test/expected.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | font-family: sans-serif; 3 | } 4 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const browserify = require('browserify') 2 | const tmpDir = require('tmp').dir 3 | const path = require('path') 4 | const test = require('tape') 5 | const bl = require('bl') 6 | const fs = require('fs') 7 | 8 | const cssExtract = require('../') 9 | 10 | test('css-extract', function (t) { 11 | t.test('should assert input types', function (t) { 12 | t.plan(2) 13 | t.throws(cssExtract, /object/) 14 | t.throws(cssExtract.bind(null, {}), 123, /object/) 15 | }) 16 | 17 | t.test('should extract sheetify css to given stream', function (t) { 18 | t.plan(2) 19 | browserify(path.join(__dirname, 'source.js')) 20 | .transform('sheetify/transform') 21 | .plugin(cssExtract, { out: createWs }) 22 | .bundle() 23 | 24 | function createWs () { 25 | return bl(function (err, data) { 26 | t.ifError(err, 'no error') 27 | const exPath = path.join(__dirname, './expected.css') 28 | const expected = fs.readFileSync(exPath, 'utf8').trim() + '\n' 29 | t.equal(String(data), expected, 'extracted all the CSS') 30 | }) 31 | } 32 | }) 33 | 34 | t.test('should extract sheetify css to file', function (t) { 35 | t.plan(3) 36 | tmpDir({ unsafeCleanup: true }, onDir) 37 | 38 | function onDir (err, dir, cleanup) { 39 | t.ifError(err, 'no error') 40 | const outFile = path.join(dir, 'out.css') 41 | 42 | browserify(path.join(__dirname, 'source.js')) 43 | .transform('sheetify/transform') 44 | .plugin(cssExtract, { out: outFile }) 45 | .bundle(function (err) { 46 | t.ifError(err, 'no bundle error') 47 | 48 | const exPath = path.join(__dirname, './expected.css') 49 | const expected = fs.readFileSync(exPath, 'utf8').trim() 50 | const actual = fs.readFileSync(outFile, 'utf8').trim() 51 | t.equal(expected, actual, 'all css written to file') 52 | 53 | cleanup() 54 | }) 55 | } 56 | }) 57 | 58 | t.test('should extract static insert-css statements', function (t) { 59 | t.plan(2) 60 | browserify(path.join(__dirname, 'source-static.js')) 61 | .plugin(cssExtract, { out: createWs }) 62 | .bundle() 63 | 64 | function createWs () { 65 | return bl(function (err, data) { 66 | t.ifError(err, 'no error') 67 | const exPath = path.join(__dirname, './expected-static.css') 68 | const expected = fs.readFileSync(exPath, 'utf8').trim() + '\n' 69 | t.equal(String(data), expected, 'extracted all the CSS') 70 | }) 71 | } 72 | }) 73 | 74 | t.test('should extract static sheetify/insert statements', function (t) { 75 | t.plan(2) 76 | browserify(path.join(__dirname, 'source-sf-static.js')) 77 | .plugin(cssExtract, { out: createWs }) 78 | .bundle() 79 | 80 | function createWs () { 81 | return bl(function (err, data) { 82 | t.ifError(err, 'no error') 83 | const exPath = path.join(__dirname, './expected-sf-static.css') 84 | const expected = fs.readFileSync(exPath, 'utf8').trim() + '\n' 85 | t.equal(String(data), expected, 'extracted all the CSS') 86 | }) 87 | } 88 | }) 89 | 90 | t.test('should not extract dynamic insert-css statements', function (t) { 91 | t.plan(4) 92 | const sourcePath = path.join(__dirname, 'source-dynamic.js') 93 | 94 | browserify(sourcePath) 95 | .plugin(cssExtract, { out: readCss }) 96 | .bundle(readJs) 97 | 98 | function readCss () { 99 | return bl(function (err, data) { 100 | t.ifError(err, 'no error') 101 | t.equal(String(data), '', 'no css extracted') 102 | }) 103 | } 104 | 105 | function readJs (err, data) { 106 | t.ifError(err, 'no error') 107 | const source = fs.readFileSync(sourcePath, 'utf8') 108 | t.ok(String(data).indexOf(String(source)) !== -1, 'source is still in built bundle') 109 | } 110 | }) 111 | 112 | t.test('should not extract dynamic insert-css statements, again', function (t) { 113 | t.plan(4) 114 | const sourcePath = path.join(__dirname, 'source-dynamic-2.js') 115 | 116 | browserify(sourcePath) 117 | .plugin(cssExtract, { out: readCss }) 118 | .bundle(readJs) 119 | 120 | function readCss () { 121 | return bl(function (err, data) { 122 | t.ifError(err, 'no error') 123 | t.equal(String(data), '', 'no css extracted') 124 | }) 125 | } 126 | 127 | function readJs (err, data) { 128 | t.ifError(err, 'no error') 129 | const source = fs.readFileSync(sourcePath, 'utf8') 130 | t.ok(String(data).indexOf(String(source)) !== -1, 'source is still in built bundle') 131 | } 132 | }) 133 | }) 134 | -------------------------------------------------------------------------------- /test/source-dynamic-2.js: -------------------------------------------------------------------------------- 1 | var insertCss = require('insert-css') 2 | 3 | insert('.foo {}') 4 | 5 | function insert (foo) { 6 | insertCss(foo) 7 | } 8 | -------------------------------------------------------------------------------- /test/source-dynamic.js: -------------------------------------------------------------------------------- 1 | insert('.foo {}') 2 | 3 | function insert (foo) { 4 | require('insert-css')(foo) 5 | } 6 | -------------------------------------------------------------------------------- /test/source-sf-static.js: -------------------------------------------------------------------------------- 1 | var insertCss = require('sheetify/insert') 2 | 3 | insertCss('.foo {background: green}') 4 | -------------------------------------------------------------------------------- /test/source-static.js: -------------------------------------------------------------------------------- 1 | var insertCss = require('insert-css') 2 | 3 | insertCss('.foo {background: green}') 4 | -------------------------------------------------------------------------------- /test/source.js: -------------------------------------------------------------------------------- 1 | const sf = require('sheetify') 2 | 3 | sf` 4 | h1 { 5 | font-family: sans-serif; 6 | } 7 | ` 8 | --------------------------------------------------------------------------------