├── .gitignore ├── .istanbul.yml ├── README.md ├── lib ├── browser.js ├── index.js ├── options.js └── process-css.js ├── package.json └── test ├── browser ├── basic │ ├── index.js │ └── style.css ├── external-url │ └── index.js └── index.js ├── extract-coverage.js └── node ├── index.spec.js ├── options.spec.js └── process-css.spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | node_modules 3 | -------------------------------------------------------------------------------- /.istanbul.yml: -------------------------------------------------------------------------------- 1 | instrumentation: 2 | root: lib 3 | include-all-sources: true 4 | 5 | reporting: 6 | reports: 7 | - html 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cssify # 2 | 3 | A simple Browserify v2 transform for adding required styles to the browser. 4 | 5 | # Example 6 | 7 | If you have a file `entry.js` that you want to require some css from `style.css`: 8 | 9 | style.css: 10 | ``` css 11 | body { 12 | background: pink; 13 | } 14 | ``` 15 | 16 | entry.js: 17 | ``` js 18 | var styleNode = require('./style.css'); 19 | 20 | console.log('The background is pink!') 21 | ``` 22 | 23 | Install cssify into your app: 24 | 25 | ``` 26 | $ npm install cssify 27 | ``` 28 | 29 | When you compile your app, just pass `-t cssify` to browserify: 30 | 31 | ``` 32 | $ browserify -t cssify entry.js > bundle.js 33 | ``` 34 | 35 | 36 | # Install 37 | 38 | With [npm](https://npmjs.org): 39 | 40 | ``` 41 | npm install cssify 42 | ``` 43 | 44 | # Bonus 45 | 46 | To add a stylesheet from a url: 47 | 48 | ``` js 49 | 50 | var cssify = require('cssify') 51 | 52 | cssify.byUrl('//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css') 53 | 54 | // Bootstrap styles! 55 | 56 | ``` 57 | 58 | # License 59 | 60 | BSD 61 | 62 | # Misc 63 | 64 | Thanks to substack's insert-css and domenic's simple-jadeify for helping me figure out how to actually test this thing. -------------------------------------------------------------------------------- /lib/browser.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | function injectStyleTag (document, fileName, cb) { 4 | var style = document.getElementById(fileName) 5 | 6 | if (style) { 7 | cb(style) 8 | } else { 9 | var head = document.getElementsByTagName('head')[0] 10 | 11 | style = document.createElement('style') 12 | if (fileName != null) style.id = fileName 13 | cb(style) 14 | head.appendChild(style) 15 | } 16 | 17 | return style 18 | } 19 | 20 | module.exports = function (css, customDocument, fileName) { 21 | var doc = customDocument || document 22 | /* istanbul ignore if: not supported by Electron */ 23 | if (doc.createStyleSheet) { 24 | var sheet = doc.createStyleSheet() 25 | sheet.cssText = css 26 | return sheet.ownerNode 27 | } else { 28 | return injectStyleTag(doc, fileName, function (style) { 29 | /* istanbul ignore if: not supported by Electron */ 30 | if (style.styleSheet) { 31 | style.styleSheet.cssText = css 32 | } else { 33 | style.innerHTML = css 34 | } 35 | }) 36 | } 37 | } 38 | 39 | module.exports.byUrl = function (url) { 40 | /* istanbul ignore if: not supported by Electron */ 41 | if (document.createStyleSheet) { 42 | return document.createStyleSheet(url).ownerNode 43 | } else { 44 | var head = document.getElementsByTagName('head')[0] 45 | var link = document.createElement('link') 46 | 47 | link.rel = 'stylesheet' 48 | link.href = url 49 | 50 | head.appendChild(link) 51 | return link 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var through = require('through2') 4 | var processCss = require('./process-css') 5 | var options = require('./options') 6 | 7 | module.exports = function (fileName, opts) { 8 | opts = options.normalize(opts) 9 | 10 | if (options.skipIt(fileName, opts)) return through() 11 | 12 | var chunks = [] 13 | 14 | return through( 15 | function (chunk, enc, next) { 16 | chunks.push(chunk) 17 | next() 18 | }, 19 | function (done) { 20 | var buffer = Buffer.concat(chunks) 21 | var source = buffer.toString('utf-8') 22 | 23 | processCss(fileName, source, opts).then(function (moduleSource) { 24 | this.push(moduleSource) 25 | done() 26 | }.bind(this)) 27 | .catch(done) 28 | } 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /lib/options.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var assign = require('lodash.assign') 4 | var cssRE = /\.css$/i 5 | var RegExpRE = /^\/(.*)\/(.*)$/ 6 | 7 | function normalize (opts) { 8 | opts = assign({}, opts) 9 | 10 | if (typeof opts['auto-inject'] === 'undefined') { 11 | opts['auto-inject'] = true 12 | } 13 | 14 | if (opts['no-auto-inject']) { 15 | opts['auto-inject'] = false 16 | delete opts['no-auto-inject'] 17 | } 18 | 19 | if (opts.test) { 20 | if (typeof opts.test === 'string') { 21 | opts.test = stringToRegExp(opts.test) 22 | } 23 | } else { 24 | opts.test = cssRE 25 | } 26 | 27 | return opts 28 | } 29 | 30 | function skipIt (fileName, opts) { 31 | if (typeof opts.test === 'function') { 32 | if (!opts.test(fileName)) { 33 | return true 34 | } 35 | } else if (opts.test instanceof RegExp) { 36 | if (!opts.test.test(fileName)) { 37 | return true 38 | } 39 | } 40 | 41 | return false 42 | } 43 | 44 | function stringToRegExp (str) { 45 | var match = RegExpRE.exec(str) 46 | if (!match) return 47 | 48 | var re = match[1] 49 | var flags = match[2] 50 | return new RegExp(re, flags) 51 | } 52 | 53 | exports.normalize = normalize 54 | exports.skipIt = skipIt 55 | exports.stringToRegExp = stringToRegExp 56 | -------------------------------------------------------------------------------- /lib/process-css.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var path = require('path') 4 | var Core = require('css-modules-loader-core') 5 | var stringHash = require('string-hash') 6 | var stringifyObject = require('stringify-object') 7 | 8 | function escapeCss (css) { 9 | return JSON.stringify(css) 10 | } 11 | 12 | function hash (str) { 13 | return '_' + stringHash(str).toString(36) 14 | } 15 | 16 | function generateHashName (styleName, fileName) { 17 | return hash(fileName + ':' + styleName) 18 | } 19 | 20 | function generateDebugName (styleName, fileName) { 21 | var sanitisedPath = fileName 22 | .replace(/\.[^\.\/\\]+$/, '') 23 | .replace(/[\W_]+/g, '_') 24 | .replace(/^_|_$/g, '') 25 | 26 | return '_' + sanitisedPath + '__' + styleName 27 | } 28 | 29 | function wrapCss (fileName, css, options, map) { 30 | var escapedCss = escapeCss(css) 31 | var stringifiedMap = stringifyObject(map) 32 | var packagePath = path.join(__dirname, '..') 33 | var dirName = path.dirname(fileName) 34 | var requirePath = path.relative(dirName, packagePath) 35 | 36 | // On Windows, path.relative returns unescaped backslashes and 37 | // that causes cssify to not be findable. 38 | requirePath = requirePath.replace(/\\/g, '/') 39 | 40 | var moduleSource = options['auto-inject'] 41 | ? [ 42 | 'var inject = require(\'./' + requirePath + '\');', 43 | 'var css = ' + escapedCss + ';', 44 | 'inject(css, undefined, \'' + hash(fileName) + '\');', 45 | options.modules 46 | ? 'module.exports = ' + stringifiedMap + ';' 47 | : 'module.exports = css;' 48 | ].join('\n') + '\n' 49 | : options.modules 50 | ? 'module.exports = { css: ' + escapedCss + ', map: ' + stringifiedMap + ' };\n' 51 | : 'module.exports = ' + escapedCss + ';\n' 52 | 53 | return moduleSource 54 | } 55 | 56 | function processCss (fileName, source, options) { 57 | if (options.modules) { 58 | Core.scope.generateScopedName = options.debug 59 | ? generateDebugName 60 | : generateHashName 61 | 62 | var core = new Core() 63 | 64 | return core.load(source, path.relative(process.cwd(), fileName)) 65 | .then(function (result) { 66 | return wrapCss( 67 | fileName, 68 | result.injectableSource, 69 | options, 70 | result.exportTokens 71 | ) 72 | }) 73 | } 74 | 75 | return Promise.resolve(wrapCss(fileName, source, options)) 76 | } 77 | 78 | module.exports = processCss 79 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cssify", 3 | "version": "1.0.3", 4 | "description": "A simple Browserify transform for adding required styles to the browser.", 5 | "main": "./lib/index.js", 6 | "scripts": { 7 | "pretest": "standard", 8 | "test:browser": "browserify -t [ . --no-auto-inject ] -t browserify-istanbul test/browser | browser-run | node test/extract-coverage.js | tap-difflet", 9 | "test:node": "istanbul cover --report json --print none ./node_modules/.bin/tape 'test/**/*.spec.js' | tap-difflet", 10 | "test": "mkdir -p coverage && npm run test:node && npm run test:browser && istanbul report" 11 | }, 12 | "repository": "https://github.com/davidguttman/cssify", 13 | "keywords": [ 14 | "browserify", 15 | "css", 16 | "transform", 17 | "browserify-transform", 18 | "dom", 19 | "browser" 20 | ], 21 | "browserify": "./lib/browser.js", 22 | "author": "David Guttman", 23 | "license": "BSD", 24 | "engines": { 25 | "node": ">= 0.12.0" 26 | }, 27 | "dependencies": { 28 | "css-modules-loader-core": "^1.0.0", 29 | "lodash.assign": "^3.2.0", 30 | "resolve": "^1.1.6", 31 | "string-hash": "^1.1.0", 32 | "stringify-object": "^2.3.1", 33 | "through2": "^2.0.0" 34 | }, 35 | "devDependencies": { 36 | "browser-run": "^3.0.3", 37 | "browserify": "^12.0.1", 38 | "browserify-istanbul": "^0.2.1", 39 | "concat-stream": "^1.5.1", 40 | "istanbul": "^0.4.0", 41 | "standard": "^5.3.1", 42 | "tap-difflet": "^0.4.0", 43 | "tape": "^4.2.2", 44 | "tape-catch": "^1.0.4" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /test/browser/basic/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var tapeCatch = require('tape-catch') 4 | var css = require('./style.css') 5 | var injectCss = require('../../..') 6 | 7 | var styleId = 'injection-testing' 8 | 9 | function test (desc, fn) { 10 | tapeCatch(desc, function (t) { 11 | try { 12 | setTimeout(function () { fn(t) }, 0) 13 | } catch (err) { 14 | t.fail(err) 15 | } 16 | }) 17 | } 18 | 19 | test('browser: basic usage', function (t) { 20 | t.equal(css, 'body { position: absolute; }\n', 'styles exported') 21 | 22 | injectCss(css, undefined, styleId) 23 | t.equal( 24 | window.getComputedStyle(document.body).position, 25 | 'absolute', 26 | 'styles injected' 27 | ) 28 | 29 | injectCss('body { width: 300px; }') 30 | injectCss('body { font-size: 5px; }') 31 | 32 | const computedStyle = window.getComputedStyle(document.body) 33 | 34 | const actualStyles = { 35 | position: computedStyle.position, 36 | width: computedStyle.width, 37 | fontSize: computedStyle.fontSize 38 | } 39 | 40 | const expectedStyles = { 41 | position: 'absolute', 42 | width: '300px', 43 | fontSize: '5px' 44 | } 45 | 46 | t.deepEqual(actualStyles, expectedStyles, 'inject styles without an id') 47 | t.end() 48 | }) 49 | 50 | test('browser: hot module replacement', function (t) { 51 | injectCss('body { position: absolute; }\n', undefined, styleId) 52 | 53 | t.equal( 54 | window.getComputedStyle(document.body).position, 55 | 'absolute', 56 | 'first injection' 57 | ) 58 | 59 | injectCss('', undefined, styleId) 60 | 61 | t.equal( 62 | window.getComputedStyle(document.body).position, 63 | 'static', 64 | 'second injection' 65 | ) 66 | 67 | t.end() 68 | }) 69 | -------------------------------------------------------------------------------- /test/browser/basic/style.css: -------------------------------------------------------------------------------- 1 | body { position: absolute; } 2 | -------------------------------------------------------------------------------- /test/browser/external-url/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var test = require('tape-catch') 4 | var cssify = require('../../..') 5 | 6 | var externalUrl = 7 | 'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css' 8 | var link = cssify.byUrl(externalUrl) 9 | 10 | test('injecting external url', function (t) { 11 | t.equal(link.parentElement, document.head, 'link is inserted') 12 | t.equal(link.rel, 'stylesheet', 'rel is set') 13 | t.equal(link.href, externalUrl, 'href is set') 14 | t.end() 15 | }) 16 | -------------------------------------------------------------------------------- /test/browser/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // FIXME: probably not good to use "private" interface 4 | require('tape-catch').getHarness()._results.once('done', function () { 5 | console.log('# coverage:', JSON.stringify(global.__coverage__)) 6 | window.close() 7 | }) 8 | 9 | require('./basic') 10 | require('./external-url') 11 | -------------------------------------------------------------------------------- /test/extract-coverage.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var fs = require('fs') 4 | var path = require('path') 5 | var concatStream = require('concat-stream') 6 | 7 | var covPath = path.join(__dirname, '..', 'coverage', 'coverage-browser.json') 8 | 9 | process.stdin.pipe(concatStream(function (input) { 10 | input = input.toString('utf-8') 11 | var sp = input.split('# coverage: ') 12 | var output = sp[0] 13 | var coverage = sp[1] 14 | console.log(output) 15 | 16 | fs.writeFile(covPath, coverage, function (err) { 17 | if (err) console.error(err) 18 | }) 19 | })) 20 | -------------------------------------------------------------------------------- /test/node/index.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var test = require('tape-catch') 4 | var browserify = require('browserify') 5 | var path = require('path') 6 | var stream = require('stream') 7 | var cssify = require('../../lib') 8 | 9 | // writable stream that just ignores everything written to it 10 | // similar to /dev/null in unix-like systems 11 | var devNull = new stream.Writable({ 12 | write: function (chunk, enc, next) { 13 | next() 14 | } 15 | }) 16 | 17 | test('main module', function (t) { 18 | browserify(path.join(__dirname, '..', 'browser', 'index.js')) 19 | .transform(cssify) 20 | .bundle() 21 | .pipe(devNull) 22 | .on('finish', t.end) 23 | .on('error', t.fail) 24 | }) 25 | -------------------------------------------------------------------------------- /test/node/options.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var test = require('tape-catch') 4 | var options = require('../../lib/options') 5 | var normalize = options.normalize 6 | var skipIt = options.skipIt 7 | var stringToRegExp = options.stringToRegExp 8 | 9 | test('options.normalize', function (t) { 10 | t.deepEqual( 11 | normalize(), 12 | { 'auto-inject': true, test: /\.css$/i }, 13 | 'falls back to defaults' 14 | ) 15 | 16 | t.equal( 17 | normalize({ 'auto-inject': null })['auto-inject'], 18 | null, 19 | 'falsy value for auto-inject is returned as is' 20 | ) 21 | 22 | t.equal( 23 | normalize({ test: '/str/' }).test.toString(), 24 | '/str/', 25 | 'regular expression string is parsed for test option' 26 | ) 27 | 28 | var testObj = {} 29 | 30 | t.equal( 31 | normalize({ test: testObj }).test, 32 | testObj, 33 | 'non-string value for test option is passed as is' 34 | ) 35 | 36 | t.deepEqual( 37 | normalize({ 'no-auto-inject': true }), 38 | { 'auto-inject': false, test: /\.css$/i }, 39 | 'no-auto-inject option' 40 | ) 41 | 42 | t.end() 43 | }) 44 | 45 | test('options.skipIt', function (t) { 46 | t.equal(skipIt('', {}), false, 'nothing to match') 47 | 48 | t.equal( 49 | skipIt('./style\.css', { test: function () { return false } }), 50 | true, 51 | 'function returning false' 52 | ) 53 | 54 | t.equal( 55 | skipIt('./style\.css', { test: function () { return true } }), 56 | false, 57 | 'function returning true' 58 | ) 59 | 60 | t.equal( 61 | skipIt('./style\.styl', { test: /\.styl$/ }), 62 | false, 63 | 'matching RegExp' 64 | ) 65 | 66 | t.equal( 67 | skipIt('./style\.styl', { test: /\.css$/ }), 68 | true, 69 | 'not-matching RegExp' 70 | ) 71 | 72 | t.end() 73 | }) 74 | 75 | test('options.stringToRegExp', function (t) { 76 | var reSource = '/str/' 77 | var re = stringToRegExp(reSource) 78 | 79 | t.equal(re instanceof RegExp, true, 'RegExp is instantiated') 80 | 81 | t.equal( 82 | re.toString(), reSource, 83 | 'regular expression converted to string matches source' 84 | ) 85 | 86 | t.equal( 87 | stringToRegExp(), undefined, 88 | 'returns undefined if arg is falsy' 89 | ) 90 | 91 | t.end() 92 | }) 93 | -------------------------------------------------------------------------------- /test/node/process-css.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var test = require('tape-catch') 4 | var assign = require('lodash.assign') 5 | var processCss = require('../../lib/process-css') 6 | 7 | var defaultOptions = { _flags: {} } 8 | 9 | var fileName = 'test.css' 10 | var hashedFilename = '_nu4uke' 11 | 12 | var css = '.test-class { font-family: "Times New Roman", sans-serif; }' 13 | var escapedCss = '.test-class { font-family: \\"Times New Roman\\", sans-serif; }' 14 | 15 | // var devMap = '{\n\t\'test-class\': \'_test___test-class\'\n}' 16 | // var devCss = '._test___test-class { font-family: \\"Times New Roman\\", sans-serif; }' 17 | 18 | var prodMap = '{\n\t\'test-class\': \'_1j9q0wu\'\n}' 19 | var prodCss = '._1j9q0wu { font-family: \\"Times New Roman\\", sans-serif; }' 20 | 21 | test('processCss', function (t) { 22 | processCss(fileName, css, assign({}, defaultOptions)) 23 | .then(function (moduleSource) { 24 | t.equal( 25 | moduleSource, 26 | 'module.exports = "' + escapedCss + '";\n', 27 | 'without injection' 28 | ) 29 | 30 | return processCss(fileName, css, assign({}, defaultOptions, { 31 | 'auto-inject': true 32 | })) 33 | }) 34 | .then(function (moduleSource) { 35 | t.equal( 36 | moduleSource, [ 37 | 'var inject = require(\'./\');', 38 | 'var css = "' + escapedCss + '";', 39 | 'inject(css, undefined, \'' + hashedFilename + '\');', 40 | 'module.exports = css;' 41 | ].join('\n') + '\n', 42 | 'with injection' 43 | ) 44 | 45 | return processCss(fileName, css, assign({}, defaultOptions, { 46 | modules: true 47 | })) 48 | }) 49 | .then(function (moduleSource) { 50 | t.equal( 51 | moduleSource, 52 | 'module.exports = { css: "' + prodCss + '", map: ' + prodMap + ' };\n', 53 | 'with modules' 54 | ) 55 | 56 | return processCss(fileName, css, assign({}, defaultOptions, { 57 | 'auto-inject': true, 58 | modules: true 59 | })) 60 | }) 61 | .then(function (moduleSource) { 62 | t.equal( 63 | moduleSource, [ 64 | 'var inject = require(\'./\');', 65 | 'var css = "' + prodCss + '";', 66 | 'inject(css, undefined, \'' + hashedFilename + '\');', 67 | 'module.exports = ' + prodMap + ';' 68 | ].join('\n') + '\n', 69 | 'with modules and injection' 70 | ) 71 | 72 | return processCss(fileName, css, assign({}, defaultOptions, { 73 | modules: true, 74 | _flags: assign({}, defaultOptions._flags, { debug: true }) 75 | })) 76 | }) 77 | .then(function (moduleSource) { 78 | // t.equal( 79 | // moduleSource, 80 | // 'module.exports = { css: "' + devCss + '", map: ' + devMap + ' };\n', 81 | // 'with debug' 82 | // ) 83 | }) 84 | .then(t.end) 85 | .catch(t.fail) 86 | }) 87 | --------------------------------------------------------------------------------