├── .eslintignore ├── tests ├── cases │ ├── simple │ │ ├── styles.css │ │ ├── expected.css │ │ └── main.js │ ├── import-and-compose │ │ ├── main.js │ │ ├── styles-2.css │ │ ├── styles-1.css │ │ └── expected.css │ ├── custom-ext │ │ ├── styles.cssmodule │ │ ├── expected.css │ │ ├── custom.js │ │ └── main.js │ ├── compose-from-shared │ │ ├── shared.css │ │ ├── main.js │ │ ├── styles-1.css │ │ ├── styles-2.css │ │ └── expected.css │ ├── import-node-module │ │ ├── custom.js │ │ ├── expected.css │ │ └── main.js │ ├── multiple-js-files │ │ ├── main.js │ │ └── expected.css │ ├── compose-node-module │ │ ├── custom.js │ │ ├── main.js │ │ ├── styles.css │ │ └── expected.css │ └── compose-local-node-module │ │ ├── custom.js │ │ ├── main.js │ │ ├── node_modules │ │ └── cool-local-styles │ │ │ └── styles.css │ │ ├── styles.css │ │ └── expected.css ├── src │ └── main.js ├── cache.js ├── index.js └── stream-output.js ├── .gitignore ├── .travis.yml ├── LICENSE ├── package.json ├── cmify.js ├── .eslintrc ├── file-system-loader.js ├── README.md └── index.js /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /tests/cases/simple/styles.css: -------------------------------------------------------------------------------- 1 | .foo { 2 | color: #F00; 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | !node_modules/cool-styles 3 | npm-debug.log -------------------------------------------------------------------------------- /tests/cases/import-and-compose/main.js: -------------------------------------------------------------------------------- 1 | require('./styles-1.css'); 2 | -------------------------------------------------------------------------------- /tests/cases/custom-ext/styles.cssmodule: -------------------------------------------------------------------------------- 1 | .foo { 2 | color: #F00; 3 | } 4 | -------------------------------------------------------------------------------- /tests/cases/simple/expected.css: -------------------------------------------------------------------------------- 1 | ._styles__foo { 2 | color: #F00; 3 | } 4 | -------------------------------------------------------------------------------- /tests/cases/custom-ext/expected.css: -------------------------------------------------------------------------------- 1 | ._styles__foo { 2 | color: #F00; 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "4" 5 | - "6" 6 | -------------------------------------------------------------------------------- /tests/cases/compose-from-shared/shared.css: -------------------------------------------------------------------------------- 1 | .shared { 2 | background: #000; 3 | } 4 | -------------------------------------------------------------------------------- /tests/cases/import-and-compose/styles-2.css: -------------------------------------------------------------------------------- 1 | .bar { 2 | background: #BAA; 3 | } 4 | -------------------------------------------------------------------------------- /tests/cases/import-node-module/custom.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | global: true 3 | } 4 | -------------------------------------------------------------------------------- /tests/cases/multiple-js-files/main.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../simple/main.js'); 2 | -------------------------------------------------------------------------------- /tests/src/main.js: -------------------------------------------------------------------------------- 1 | var styles = require('./styles.css'); 2 | module.exports = styles; 3 | -------------------------------------------------------------------------------- /tests/cases/compose-node-module/custom.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | global: true 3 | } 4 | -------------------------------------------------------------------------------- /tests/cases/compose-local-node-module/custom.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | global: true 3 | } 4 | -------------------------------------------------------------------------------- /tests/cases/custom-ext/custom.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | filePattern: /\.cssmodule$/ 3 | } 4 | -------------------------------------------------------------------------------- /tests/cases/multiple-js-files/expected.css: -------------------------------------------------------------------------------- 1 | ._simple_styles__foo { 2 | color: #F00; 3 | } 4 | -------------------------------------------------------------------------------- /tests/cases/simple/main.js: -------------------------------------------------------------------------------- 1 | var styles = require('./styles.css'); 2 | module.exports = styles; 3 | -------------------------------------------------------------------------------- /tests/cases/compose-from-shared/main.js: -------------------------------------------------------------------------------- 1 | require('./styles-1.css'); 2 | require('./styles-2.css'); 3 | -------------------------------------------------------------------------------- /tests/cases/compose-node-module/main.js: -------------------------------------------------------------------------------- 1 | var styles = require('./styles.css'); 2 | module.exports = styles; 3 | -------------------------------------------------------------------------------- /tests/cases/import-node-module/expected.css: -------------------------------------------------------------------------------- 1 | ._node_modules_cool_styles_styles__foo { 2 | color: #F00; 3 | } 4 | -------------------------------------------------------------------------------- /tests/cases/compose-local-node-module/main.js: -------------------------------------------------------------------------------- 1 | var styles = require('./styles.css'); 2 | module.exports = styles; 3 | -------------------------------------------------------------------------------- /tests/cases/compose-local-node-module/node_modules/cool-local-styles/styles.css: -------------------------------------------------------------------------------- 1 | .foo { 2 | color: #F00; 3 | } 4 | -------------------------------------------------------------------------------- /tests/cases/import-and-compose/styles-1.css: -------------------------------------------------------------------------------- 1 | .foo { 2 | composes: bar from "./styles-2.css"; 3 | color: #F00; 4 | } 5 | -------------------------------------------------------------------------------- /tests/cases/import-node-module/main.js: -------------------------------------------------------------------------------- 1 | var styles = require('cool-styles/styles.css'); 2 | module.exports = styles; 3 | -------------------------------------------------------------------------------- /tests/cases/compose-from-shared/styles-1.css: -------------------------------------------------------------------------------- 1 | .foo { 2 | composes: shared from "./shared.css"; 3 | color: #F00; 4 | } 5 | -------------------------------------------------------------------------------- /tests/cases/compose-from-shared/styles-2.css: -------------------------------------------------------------------------------- 1 | .bar { 2 | composes: shared from "./shared.css"; 3 | background: #BAA; 4 | } 5 | -------------------------------------------------------------------------------- /tests/cases/compose-node-module/styles.css: -------------------------------------------------------------------------------- 1 | .foo { 2 | composes: foo from "cool-styles/styles.css"; 3 | background: black; 4 | } 5 | -------------------------------------------------------------------------------- /tests/cases/import-and-compose/expected.css: -------------------------------------------------------------------------------- 1 | ._styles_2__bar { 2 | background: #BAA; 3 | } 4 | ._styles_1__foo { 5 | color: #F00; 6 | } 7 | -------------------------------------------------------------------------------- /tests/cases/compose-local-node-module/styles.css: -------------------------------------------------------------------------------- 1 | .foo { 2 | composes: foo from "cool-local-styles/styles.css"; 3 | background: black; 4 | } 5 | -------------------------------------------------------------------------------- /tests/cases/custom-ext/main.js: -------------------------------------------------------------------------------- 1 | // test using a custom `filePattern` option 2 | var styles = require('./styles.cssmodule'); 3 | module.exports = styles; 4 | -------------------------------------------------------------------------------- /tests/cases/compose-node-module/expected.css: -------------------------------------------------------------------------------- 1 | ._node_modules_cool_styles_styles__foo { 2 | color: #F00; 3 | } 4 | ._styles__foo { 5 | background: black; 6 | } 7 | -------------------------------------------------------------------------------- /tests/cases/compose-local-node-module/expected.css: -------------------------------------------------------------------------------- 1 | ._node_modules_cool_local_styles_styles__foo { 2 | color: #F00; 3 | } 4 | ._styles__foo { 5 | background: black; 6 | } 7 | -------------------------------------------------------------------------------- /tests/cases/compose-from-shared/expected.css: -------------------------------------------------------------------------------- 1 | ._shared__shared { 2 | background: #000; 3 | } 4 | ._styles_1__foo { 5 | color: #F00; 6 | } 7 | ._styles_2__bar { 8 | background: #BAA; 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Josh Johnston 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 | 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "css-modulesify", 3 | "version": "0.28.0", 4 | "description": "A browserify transform to load CSS Modules", 5 | "main": "index.js", 6 | "dependencies": { 7 | "css-modules-loader-core": "^1.1.0", 8 | "dependency-graph": "^0.4.1", 9 | "object-assign": "^3.0.0", 10 | "promise-polyfill": "^2.1.0", 11 | "resolve": "^1.1.7", 12 | "string-hash": "^1.1.0", 13 | "through2": "^2.0.1" 14 | }, 15 | "devDependencies": { 16 | "browserify": "^11.0.1", 17 | "eslint": "^1.4.0", 18 | "proxyquire": "^1.6.0", 19 | "rebundler": "^0.2.0", 20 | "tape": "^4.0.1" 21 | }, 22 | "scripts": { 23 | "test": "tape tests/*.js", 24 | "lint": "eslint index.js tests/" 25 | }, 26 | "author": "joshwnj", 27 | "license": "MIT", 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/css-modules/css-modulesify.git" 31 | }, 32 | "keywords": [ 33 | "browserify-transform", 34 | "browserify-plugin", 35 | "css-modules", 36 | "browserify", 37 | "css" 38 | ], 39 | "bugs": { 40 | "url": "https://github.com/css-modules/css-modulesify/issues" 41 | }, 42 | "homepage": "https://github.com/css-modules/css-modulesify" 43 | } 44 | -------------------------------------------------------------------------------- /tests/cache.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape'); 2 | 3 | var browserify = require('browserify'); 4 | var proxyquire = require('proxyquire'); 5 | var fs = require('fs'); 6 | var path = require('path'); 7 | var rebundler = require('rebundler'); 8 | 9 | var casesDir = path.join(__dirname, 'cases'); 10 | var simpleCaseDir = path.join(casesDir, 'simple'); 11 | var cssOutFilename = 'out.css'; 12 | 13 | tape('multiple builds', function (t) { 14 | var fakeFs = { 15 | writeFile: function (filename, content, cb) { 16 | var expected = fs.readFileSync(path.join(simpleCaseDir, 'expected.css'), 'utf8'); 17 | 18 | t.equal(filename, cssOutFilename, 'correct output filename'); 19 | t.equal(content, expected, 'output matches expected'); 20 | cb(); 21 | } 22 | }; 23 | 24 | var cssModulesify = proxyquire('../', { 25 | fs: fakeFs 26 | }); 27 | 28 | var cssModulesifyCache = {}; 29 | var getBundler = rebundler(function (cache, packageCache) { 30 | return browserify(path.join(simpleCaseDir, 'main.js'), { 31 | cache: cache 32 | , packageCache: packageCache 33 | , fullPaths: true 34 | }) 35 | .plugin(cssModulesify, { 36 | rootDir: path.join(simpleCaseDir) 37 | , output: cssOutFilename 38 | , cache: cssModulesifyCache 39 | }); 40 | }); 41 | 42 | getBundler().bundle(function (err) { 43 | t.error(err, 'initial bundle without a cache does not error'); 44 | 45 | getBundler().bundle(function (err2) { 46 | t.error(err2, 'second pass bundle with a cache does not error'); 47 | 48 | t.end(); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape'); 2 | 3 | var browserify = require('browserify'); 4 | var proxyquire = require('proxyquire'); 5 | var fs = require('fs'); 6 | var path = require('path'); 7 | 8 | var casesDir = path.join(__dirname, 'cases'); 9 | var cssOutFilename = 'out.css'; 10 | 11 | function runTestCase (dir) { 12 | tape('case: ' + dir, function (t) { 13 | // load (optional) custom setup for this testcase 14 | var customPath = path.join(casesDir, dir, 'custom.js'); 15 | var customOpts; 16 | try { 17 | fs.accessSync(customPath); 18 | customOpts = require(customPath); 19 | } catch (e) { 20 | customOpts = {}; 21 | } 22 | 23 | var fakeFs = { 24 | writeFile: function (filename, content, cb) { 25 | var expected = fs.readFileSync(path.join(casesDir, dir, 'expected.css'), 'utf8'); 26 | 27 | t.equal(filename, cssOutFilename, 'correct output filename'); 28 | t.equal(content, expected, 'output matches expected'); 29 | cb(); 30 | } 31 | }; 32 | 33 | var cssModulesify = proxyquire('../', { 34 | fs: fakeFs 35 | }); 36 | 37 | var b = browserify(); 38 | 39 | b.add(path.join(casesDir, dir, 'main.js')); 40 | b.plugin(cssModulesify, Object.assign({}, { 41 | rootDir: path.join(casesDir, dir) 42 | , output: cssOutFilename 43 | , generateScopedName: cssModulesify.generateLongName 44 | }, customOpts)); 45 | 46 | b.bundle(function (err) { 47 | if (err) { 48 | t.error(err, 'should not error'); 49 | } 50 | 51 | t.end(); 52 | }); 53 | }); 54 | } 55 | 56 | // test cases are expected to have: 57 | // - main.js (entry point) 58 | // - expected.css (what to expect from css-modulesify output) 59 | // optional: 60 | // - custom.js (module that exports an object of custom settings) 61 | 62 | fs.readdirSync(path.join(__dirname, 'cases')).forEach(runTestCase); 63 | -------------------------------------------------------------------------------- /tests/stream-output.js: -------------------------------------------------------------------------------- 1 | var tape = require('tape'); 2 | 3 | var browserify = require('browserify'); 4 | var proxyquire = require('proxyquire'); 5 | var fs = require('fs'); 6 | var path = require('path'); 7 | 8 | var casesDir = path.join(__dirname, 'cases'); 9 | var simpleCaseDir = path.join(casesDir, 'simple'); 10 | var cssFilesTotal = 1; 11 | var cssOutFilename = 'out.css'; 12 | 13 | tape('stream output', function (t) { 14 | var fakeFs = { 15 | writeFile: function (filename, content, cb) { 16 | var expected = fs.readFileSync(path.join(simpleCaseDir, 'expected.css'), 'utf8'); 17 | 18 | t.equal(filename, cssOutFilename, 'correct output filename'); 19 | t.equal(content, expected, 'output matches expected'); 20 | cb(); 21 | } 22 | }; 23 | 24 | var cssModulesify = proxyquire('../', { 25 | fs: fakeFs 26 | }); 27 | 28 | t.plan(cssFilesTotal * 2 + 2); 29 | 30 | var cssFilesCount = 0; 31 | var b = browserify(path.join(simpleCaseDir, 'main.js')); 32 | 33 | b 34 | .plugin(cssModulesify, { 35 | rootDir: path.join(simpleCaseDir) 36 | }) 37 | .on('error', t.error) 38 | .bundle(function noop () {}); 39 | 40 | b 41 | .once('css stream', function (stream) { 42 | stream 43 | .on('data', function onData (css) { 44 | var cssString = css.toString(); 45 | // just get the first class name, use that as an id 46 | var cssId = cssString.split('\n')[0].split(' ')[0]; 47 | 48 | t.ok( 49 | ++cssFilesCount <= cssFilesTotal 50 | , 'emits data for ' + cssId 51 | ); 52 | 53 | t.ok( 54 | cssString.indexOf('._styles') === 0 55 | , 'emits compiled css for ' + cssId 56 | ); 57 | }) 58 | .on('end', function onEnd () { 59 | t.pass('ends the stream'); 60 | 61 | b.bundle(function noop () {}); 62 | 63 | b.once('css stream', function (stream2) { 64 | t.ok(stream2, 'registers a second event for a CSS stream'); 65 | }); 66 | }) 67 | .on('error', t.error); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /cmify.js: -------------------------------------------------------------------------------- 1 | var stream = require('stream'); 2 | var util = require('util'); 3 | var assign = require('object-assign'); 4 | var path = require('path'); 5 | 6 | util.inherits(Cmify, stream.Transform); 7 | function Cmify (filename, opts) { 8 | if (!(this instanceof Cmify)) { 9 | return new Cmify(filename, opts); 10 | } 11 | 12 | stream.Transform.call(this); 13 | 14 | this.cssFilePattern = new RegExp(opts.cssFilePattern || '\.css$'); 15 | this._data = ''; 16 | this._filename = filename; 17 | this._cssOutFilename = opts.cssOutFilename; 18 | this._loader = opts.loader; 19 | this._tokensByFile = opts.tokensByFile; 20 | this._rootDir = opts.rootDir; 21 | opts.cssFiles.push(filename); 22 | } 23 | 24 | Cmify.prototype.isCssFile = function (filename) { 25 | return this.cssFilePattern.test(filename); 26 | }; 27 | 28 | Cmify.prototype._transform = function (buf, enc, callback) { 29 | // only handle .css files 30 | if (!this.isCssFile(this._filename)) { 31 | this.push(buf); 32 | return callback(); 33 | } 34 | 35 | this._data += buf; 36 | callback(); 37 | }; 38 | 39 | Cmify.prototype._flush = function (callback) { 40 | var self = this; 41 | var filename = this._filename; 42 | 43 | // only handle .css files 44 | if (!this.isCssFile(filename)) { return callback(); } 45 | 46 | // grab the correct loader 47 | var loader = this._loader; 48 | var tokensByFile = this._tokensByFile; 49 | 50 | // convert css to js before pushing 51 | // reset the `tokensByFile` state 52 | var relFilename = path.relative(this._rootDir, filename); 53 | tokensByFile[filename] = loader.tokensByFile[filename] = null; 54 | 55 | loader.fetch(relFilename, '/').then(function (tokens) { 56 | var deps = loader.deps.dependenciesOf(filename); 57 | var output = deps.map(function (f) { 58 | return 'require("' + f + '")'; 59 | }); 60 | output.push('module.exports = ' + JSON.stringify(tokens)); 61 | 62 | var isValid = true; 63 | var isUndefined = /\bundefined\b/; 64 | Object.keys(tokens).forEach(function (k) { 65 | if (isUndefined.test(tokens[k])) { 66 | isValid = false; 67 | } 68 | }); 69 | 70 | if (!isValid) { 71 | var err = 'Composition in ' + filename + ' contains an undefined reference'; 72 | console.error(err); 73 | output.push('console.error("' + err + '");'); 74 | } 75 | 76 | assign(tokensByFile, loader.tokensByFile); 77 | 78 | self.push(output.join('\n')); 79 | return callback(); 80 | }).catch(function (err) { 81 | self.push('console.error("' + err + '");'); 82 | self.emit('error', err); 83 | return callback(); 84 | }); 85 | }; 86 | 87 | module.exports = Cmify; 88 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "ecmaFeatures": { 3 | }, 4 | "env": { 5 | "node": true 6 | }, 7 | "rules": { 8 | "comma-dangle": [2, "never"], 9 | "no-cond-assign": 2, 10 | "no-constant-condition": 2, 11 | "no-control-regex": 2, 12 | "no-debugger": 2, 13 | "no-dupe-keys": 2, 14 | "no-empty": 2, 15 | "no-empty-character-class": 2, 16 | "no-ex-assign": 2, 17 | "no-extra-boolean-cast": 2, 18 | "no-extra-parens": 0, 19 | "no-extra-semi": 2, 20 | "no-func-assign": 2, 21 | "no-inner-declarations": 2, 22 | "no-invalid-regexp": 2, 23 | "no-irregular-whitespace": 2, 24 | "no-negated-in-lhs": 2, 25 | "no-obj-calls": 2, 26 | "no-regex-spaces": 2, 27 | "no-sparse-arrays": 2, 28 | "no-unreachable": 2, 29 | "use-isnan": 2, 30 | "valid-typeof": 2, 31 | "block-scoped-var": 0, 32 | "consistent-return": 2, 33 | "curly": [ 34 | 1, 35 | "multi-line" 36 | ], 37 | "default-case": 2, 38 | "dot-notation": 2, 39 | "eqeqeq": 2, 40 | "guard-for-in": 2, 41 | "no-alert": 1, 42 | "no-caller": 2, 43 | "no-div-regex": 2, 44 | "no-eq-null": 2, 45 | "no-eval": 2, 46 | "no-extend-native": 2, 47 | "no-extra-bind": 2, 48 | "no-fallthrough": 2, 49 | "no-floating-decimal": 2, 50 | "no-implied-eval": 2, 51 | "no-iterator": 2, 52 | "no-labels": 2, 53 | "no-lone-blocks": 2, 54 | "no-loop-func": 2, 55 | "no-multi-spaces": 1, 56 | "no-multi-str": 2, 57 | "no-native-reassign": 2, 58 | "no-new": 2, 59 | "no-new-func": 2, 60 | "no-new-wrappers": 2, 61 | "no-octal": 2, 62 | "no-octal-escape": 2, 63 | "no-proto": 2, 64 | "no-redeclare": 2, 65 | "no-return-assign": [2, "except-parens"], 66 | "no-script-url": 2, 67 | "no-self-compare": 2, 68 | "no-sequences": 2, 69 | "no-throw-literal": 2, 70 | "no-unused-expressions": 0, 71 | "no-with": 2, 72 | "radix": 2, 73 | "vars-on-top": 0, 74 | "wrap-iife": 2, 75 | "yoda": [ 76 | 1, 77 | "never" 78 | ], 79 | "strict": [ 80 | 0, 81 | "never" 82 | ], 83 | "no-delete-var": 2, 84 | "no-label-var": 2, 85 | "no-shadow": 2, 86 | "no-shadow-restricted-names": 2, 87 | "no-undef": 2, 88 | "no-undef-init": 2, 89 | "no-undefined": 2, 90 | "no-unused-vars": [ 91 | 2, 92 | "all" 93 | ], 94 | "no-use-before-define": 0, 95 | "handle-callback-err": 2, 96 | "no-mixed-requires": 0, 97 | "no-new-require": 2, 98 | "no-path-concat": 2, 99 | "indent": [ 100 | 1, 101 | 2, 102 | {"SwitchCase": 1} 103 | ], 104 | "brace-style": [ 105 | 1, 106 | "stroustrup", 107 | { 108 | "allowSingleLine": true 109 | } 110 | ], 111 | "comma-spacing": [ 112 | 1, 113 | { 114 | "before": false, 115 | "after": true 116 | } 117 | ], 118 | "comma-style": [ 119 | 2, 120 | "first" 121 | ], 122 | "consistent-this": [ 123 | 1, 124 | "self" 125 | ], 126 | "eol-last": 2, 127 | "func-names": 0, 128 | "key-spacing": [ 129 | 1, 130 | { 131 | "beforeColon": false, 132 | "afterColon": true 133 | } 134 | ], 135 | "max-nested-callbacks": [ 136 | 2, 137 | 3 138 | ], 139 | "new-cap": 2, 140 | "new-parens": 2, 141 | "no-array-constructor": 0, 142 | "no-inline-comments": 1, 143 | "no-lonely-if": 0, 144 | "no-mixed-spaces-and-tabs": 2, 145 | "no-multiple-empty-lines": 2, 146 | "no-nested-ternary": 0, 147 | "no-new-object": 2, 148 | "semi-spacing": [2, {"before": false, "after": true}], 149 | "no-spaced-func": 1, 150 | "no-ternary": 0, 151 | "no-trailing-spaces": 2, 152 | "no-underscore-dangle": 0, 153 | "one-var": [ 154 | 1, 155 | { 156 | "var": "never", 157 | "let": "never", 158 | "const": "never" 159 | } 160 | ], 161 | "operator-assignment": [ 162 | 2, 163 | "always" 164 | ], 165 | "padded-blocks": [ 166 | 1, 167 | "never" 168 | ], 169 | "quote-props": [ 170 | 1, 171 | "as-needed" 172 | ], 173 | "quotes": [ 174 | 2, 175 | "single" 176 | ], 177 | "semi": [ 178 | 2, 179 | "always" 180 | ], 181 | "sort-vars": 0, 182 | "space-after-keywords": [ 183 | 1, 184 | "always" 185 | ], 186 | "space-before-blocks": 0, 187 | "space-before-function-paren": [ 188 | 1, 189 | "always" 190 | ], 191 | "object-curly-spacing": [ 192 | 1, 193 | "never" 194 | ], 195 | "array-bracket-spacing": [ 196 | 1, 197 | "never" 198 | ], 199 | "space-in-parens": [ 200 | 1, 201 | "never" 202 | ], 203 | "space-infix-ops": 2, 204 | "space-return-throw-case": 2, 205 | "space-unary-ops": [ 206 | 1, 207 | { 208 | "words": true, 209 | "nonwords": false 210 | } 211 | ], 212 | "spaced-comment": [ 213 | 1, 214 | "always", 215 | { 216 | "exceptions": [ 217 | "-" 218 | ] 219 | } 220 | ], 221 | "wrap-regex": 0, 222 | "constructor-super": 2, 223 | "no-this-before-super": 2, 224 | "require-yield": 2, 225 | "prefer-spread": 1, 226 | "no-useless-call": 1, 227 | "no-invalid-this": 0, 228 | "no-implicit-coercion": 0, 229 | "no-const-assign": 2, 230 | "no-class-assign": 2, 231 | "init-declarations": 0, 232 | "callback-return": [0, ["callback", "cb", "done", "next"]], 233 | "arrow-spacing": [1, {"before": true, "after": true}], 234 | "arrow-parens": 1 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /file-system-loader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var DepGraph = require('dependency-graph').DepGraph; 4 | var nodeResolve = require('resolve'); 5 | 6 | Object.defineProperty(exports, '__esModule', { 7 | value: true 8 | }); 9 | 10 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 11 | 12 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 13 | 14 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } 15 | 16 | var _indexJs = require('css-modules-loader-core/lib/index.js'); 17 | 18 | var _indexJs2 = _interopRequireDefault(_indexJs); 19 | 20 | var _fs = require('fs'); 21 | 22 | var _fs2 = _interopRequireDefault(_fs); 23 | 24 | var _path = require('path'); 25 | 26 | var _path2 = _interopRequireDefault(_path); 27 | 28 | // Sorts dependencies in the following way: 29 | // AAA comes before AA and A 30 | // AB comes after AA and before A 31 | // All Bs come after all As 32 | // This ensures that the files are always returned in the following order: 33 | // - In the order they were required, except 34 | // - After all their dependencies 35 | var traceKeySorter = function traceKeySorter(a, b) { 36 | if (a.length < b.length) { 37 | return a < b.substring(0, a.length) ? -1 : 1; 38 | } else if (a.length > b.length) { 39 | return a.substring(0, b.length) <= b ? -1 : 1; 40 | } else { 41 | return a < b ? -1 : 1; 42 | } 43 | }; 44 | 45 | var FileSystemLoader = (function () { 46 | function FileSystemLoader(root, plugins) { 47 | _classCallCheck(this, FileSystemLoader); 48 | 49 | this.root = root; 50 | this.sources = {}; 51 | this.importNr = 0; 52 | this.core = new _indexJs2['default'](plugins); 53 | this.tokensByFile = {}; 54 | this.deps = new DepGraph(); 55 | } 56 | 57 | _createClass(FileSystemLoader, [{ 58 | key: 'fetch', 59 | value: function fetch(_newPath, relativeTo, _trace) { 60 | var _this = this; 61 | 62 | var newPath = _newPath.replace(/^["']|["']$/g, ''), 63 | trace = _trace || String.fromCharCode(this.importNr++); 64 | return new Promise(function (resolve, reject) { 65 | var relativeDir = _path2['default'].dirname(relativeTo), 66 | rootRelativePath = _path2['default'].resolve(relativeDir, newPath), 67 | rootRelativeDir = _path2['default'].join(_this.root, relativeDir), 68 | fileRelativePath = _path2['default'].resolve(rootRelativeDir, newPath); 69 | 70 | // if the path is not relative or absolute, try to resolve it in node_modules 71 | if (newPath[0] !== '.' && newPath[0] !== '/') { 72 | var paths; 73 | if (process.env.NODE_PATH) { 74 | paths = process.env.NODE_PATH.split(_path2['default'].delimiter); 75 | } 76 | try { 77 | fileRelativePath = nodeResolve.sync(newPath, { 78 | basedir: rootRelativeDir, 79 | paths: paths 80 | }); 81 | // in this case we need to actualize rootRelativePath too 82 | rootRelativePath = _path2['default'].relative(_this.root, fileRelativePath); 83 | } catch (e) {} 84 | } 85 | 86 | // first time? add a node 87 | if (_trace === undefined) { 88 | if (!_this.deps.hasNode(fileRelativePath)) { 89 | _this.deps.addNode(fileRelativePath); 90 | } 91 | } 92 | // otherwise add a dependency 93 | else { 94 | var parentFilePath = _path2['default'].join(_this.root, relativeTo); 95 | if (!_this.deps.hasNode(parentFilePath)) { 96 | console.error('NO NODE', parentFilePath, fileRelativePath) 97 | } 98 | if (!_this.deps.hasNode(fileRelativePath)) { 99 | _this.deps.addNode(fileRelativePath); 100 | } 101 | _this.deps.addDependency(parentFilePath, fileRelativePath); 102 | } 103 | 104 | var tokens = _this.tokensByFile[fileRelativePath]; 105 | if (tokens) { 106 | return resolve(tokens); 107 | } 108 | 109 | _fs2['default'].readFile(fileRelativePath, 'utf-8', function (err, source) { 110 | if (err) reject(err); 111 | _this.core.load(source, rootRelativePath, trace, _this.fetch.bind(_this)).then(function (_ref) { 112 | var injectableSource = _ref.injectableSource; 113 | var exportTokens = _ref.exportTokens; 114 | 115 | _this.sources[fileRelativePath] = injectableSource; 116 | _this.tokensByFile[fileRelativePath] = exportTokens; 117 | resolve(exportTokens); 118 | }, reject); 119 | }); 120 | }); 121 | } 122 | }, { 123 | key: 'finalSource', 124 | get: function () { 125 | var sources = this.sources; 126 | var written = {}; 127 | 128 | return this.deps.overallOrder().map(function (filename) { 129 | if (written[filename] === true) { 130 | return null; 131 | } 132 | written[filename] = true; 133 | 134 | return sources[filename]; 135 | }).join(''); 136 | } 137 | }]); 138 | 139 | return FileSystemLoader; 140 | })(); 141 | 142 | exports['default'] = FileSystemLoader; 143 | module.exports = exports['default']; 144 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # css-modulesify 2 | 3 | [![Build Status](https://travis-ci.org/css-modules/css-modulesify.svg?branch=master)](https://travis-ci.org/css-modules/css-modulesify) 4 | 5 | A browserify plugin to load [CSS Modules]. 6 | 7 | [CSS Modules]: https://github.com/css-modules/css-modules 8 | 9 | **Please note that this is still highly experimental.** 10 | 11 | ## Why CSS Modules? 12 | 13 | Normally you need to use a strict naming convention like BEM to ensure that one component's CSS doesn't collide with another's. CSS Modules are locally scoped, which allows you to use names that are meaningful within the context of the component, without any danger of name collision. 14 | 15 | Read Mark Dalgleish's excellent ["End of Global CSS"](https://medium.com/seek-ui-engineering/the-end-of-global-css-90d2a4a06284) and check out [css-modules](https://github.com/css-modules/css-modules) for more context. 16 | 17 | ## Getting started 18 | 19 | First install the package: `npm install --save css-modulesify` 20 | 21 | Then you can use it as a browserify plugin, eg: `browserify -p [ css-modulesify -o dist/main.css ] example/index.js` 22 | 23 | Inside `example/index.js` you can now load css into your scripts. When you do `var box1 = require('./box1.css')`, `box1` will be an object to lookup the localized classname for one of the selectors in that file. 24 | 25 | So to apply a class to an element you can do something like: 26 | 27 | ```js 28 | var styles = require('./styles.css'); 29 | var div = `
...
`; 30 | ``` 31 | 32 | The generated css will contain locally-scoped versions of any css you have `require`'d, and will be written out to the file you specify in the `--output` or `-o` option. 33 | 34 | ## API Usage 35 | 36 | ```js 37 | var b = require('browserify')(); 38 | 39 | b.add('./main.js'); 40 | b.plugin(require('css-modulesify'), { 41 | rootDir: __dirname, 42 | output: './path/to/my.css' 43 | }); 44 | 45 | b.bundle(); 46 | ``` 47 | 48 | ```js 49 | // or, get the output as a stream 50 | var b = require('browserify')(); 51 | var fs = require('fs'); 52 | 53 | b.add('./main.js'); 54 | b.plugin(require('css-modulesify'), { 55 | rootDir: __dirname 56 | }); 57 | 58 | var bundle = b.bundle() 59 | b.on('css stream', function (css) { 60 | css.pipe(fs.createWriteStream('mycss.css')); 61 | }); 62 | ``` 63 | 64 | ### Options: 65 | 66 | - `rootDir`: absolute path to your project's root directory. This is optional but providing it will result in better generated classnames. css-modulesify will try to use the browserify `basedir` if `rootDir` is not specified, if both are not specified it will use the location from which the command was executed. 67 | - `output`: path to write the generated css. If not provided, you'll need to listen to the `'css stream'` event on the bundle to get the output. 68 | - `jsonOutput`: optional path to write a json manifest of classnames. 69 | - `use`: optional array of postcss plugins (by default we use the css-modules core plugins). NOTE: it's safer to use `after` 70 | - `before`: optional array of postcss plugins to run before the required css-modules core plugins are run. 71 | - `after`: optional array of postcss plugins to run after the required css-modules core plugins are run. 72 | - `generateScopedName`: (API only) a function to override the default behaviour of creating locally scoped classnames. 73 | - `global`: optional boolean. Set to `true` if you want `css-modulesify` to apply to `node_modules` as well as local files. You can read more about it in the [browserify docs](https://github.com/substack/node-browserify/#btransformtr-opts). 74 | - `filePattern`: optional regular expression string to specify css file names. (default: `\.css$`) 75 | - `cache`: optional object to persist cache between runs. 76 | 77 | ### Events 78 | - `b.on('css stream', callback)` The callback is called with a readable stream containing the compiled CSS. You can write this to a file. 79 | 80 | ## Using CSS Modules on the backend 81 | 82 | If you want to use CSS Modules in server-generated templates there are a couple of options: 83 | 84 | - Option A (nodejs only): register the [require-hook](https://github.com/css-modules/css-modules-require-hook) so that `var styles = require('./foo.css')` operates the same way as on the frontend. Make sure that the `rootDir` option matches to guarantee that the classnames are the same. 85 | 86 | - Option B: configure the `jsonOutput` option with a file path and `css-modulesify` will generate a JSON manifest of classnames. 87 | 88 | 89 | ## PostCSS Plugins 90 | 91 | The following PostCSS plugins are enabled by default: 92 | 93 | * [postcss-modules-local-by-default] 94 | * [postcss-modules-extract-imports] 95 | * [postcss-modules-scope] 96 | * [postcss-modules-values] 97 | 98 | (i.e. the [CSS Modules] specification). 99 | 100 | You can override the default PostCSS Plugins (and add your own) by passing `--use|-u` to `css-modulesify`. 101 | 102 | Or if you just want to add some extra plugins to run after the default, add them to the `postcssAfter` array option (API only at this time). In the same way, add extra plugins to `postcssBefore` to run the before the defaults. 103 | 104 | In addition you may also wish to configure defined PostCSS plugins by passing `--plugin.option true`. 105 | 106 | An example of this would be: 107 | 108 | ``` 109 | browserify -p [css-modulesify \ 110 | --after autoprefixer --autoprefixer.browsers '> 5%' \ 111 | -o dist/main.css] -o dist/index.js src/index.js 112 | ``` 113 | 114 | [postcss-modules-local-by-default]: https://github.com/css-modules/postcss-modules-local-by-default 115 | [postcss-modules-extract-imports]: https://github.com/css-modules/postcss-modules-extract-imports 116 | [postcss-modules-scope]: https://github.com/css-modules/postcss-modules-scope 117 | [postcss-modules-values]: https://github.com/css-modules/postcss-modules-values 118 | 119 | ## Building for production 120 | 121 | If you set `NODE_ENV=production` then `css-modulesify` will generate shorter (though less useful) classnames. 122 | 123 | You can also manually switch to short names by setting the `generateScopedName` option. Eg: 124 | 125 | ``` 126 | browserify.plugin(cssModulesify, { 127 | rootDir: __dirname, 128 | output: './dist/main.css', 129 | generateScopedName: cssModulesify.generateShortName 130 | }) 131 | ``` 132 | 133 | ## Example 134 | 135 | An example implementation can be found [here](https://github.com/css-modules/browserify-demo). 136 | 137 | ## Licence 138 | 139 | MIT 140 | 141 | ## With thanks 142 | 143 | - Tobias Koppers 144 | - Mark Dalgleish 145 | - Glen Maddern 146 | 147 | ---- 148 | Josh Johnston, 2015. 149 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Some css-modules-loader-code dependencies use Promise so we'll provide it for older node versions 2 | if (!global.Promise) { global.Promise = require('promise-polyfill'); } 3 | 4 | var fs = require('fs'); 5 | var path = require('path'); 6 | var Cmify = require('./cmify'); 7 | var Core = require('css-modules-loader-core'); 8 | var FileSystemLoader = require('./file-system-loader'); 9 | var stringHash = require('string-hash'); 10 | var ReadableStream = require('stream').Readable; 11 | var through = require('through2'); 12 | 13 | /* 14 | Custom `generateScopedName` function for `postcss-modules-scope`. 15 | Short names consisting of source hash and line number. 16 | */ 17 | function generateShortName (name, filename, css) { 18 | // first occurrence of the name 19 | // TODO: better match with regex 20 | var i = css.indexOf('.' + name); 21 | var numLines = css.substr(0, i).split(/[\r\n]/).length; 22 | 23 | var hash = stringHash(css).toString(36).substr(0, 5); 24 | return '_' + name + '_' + hash + '_' + numLines; 25 | } 26 | 27 | /* 28 | Custom `generateScopedName` function for `postcss-modules-scope`. 29 | Appends a hash of the css source. 30 | */ 31 | function generateLongName (name, filename) { 32 | var sanitisedPath = filename.replace(/\.[^\.\/\\]+$/, '') 33 | .replace(/[\W_]+/g, '_') 34 | .replace(/^_|_$/g, ''); 35 | 36 | return '_' + sanitisedPath + '__' + name; 37 | } 38 | 39 | /* 40 | Get the default plugins and apply options. 41 | */ 42 | function getDefaultPlugins (options) { 43 | var scope = Core.scope; 44 | var customNameFunc = options.generateScopedName; 45 | var defaultNameFunc = process.env.NODE_ENV === 'production' ? 46 | generateShortName : 47 | generateLongName; 48 | 49 | scope.generateScopedName = customNameFunc || defaultNameFunc; 50 | 51 | return [ 52 | Core.values 53 | , Core.localByDefault 54 | , Core.extractImports 55 | , scope 56 | ]; 57 | } 58 | 59 | /* 60 | 61 | Normalize the manifest paths so that they are always relative 62 | to the project root directory. 63 | 64 | */ 65 | function normalizeManifestPaths (tokensByFile, rootDir) { 66 | var output = {}; 67 | var rootDirLength = rootDir.length + 1; 68 | 69 | Object.keys(tokensByFile).forEach(function (filename) { 70 | var normalizedFilename = filename.substr(rootDirLength); 71 | output[normalizedFilename] = tokensByFile[filename]; 72 | }); 73 | 74 | return output; 75 | } 76 | 77 | // PostCSS plugins passed to FileSystemLoader 78 | function getPlugins (options) { 79 | var plugins = options.use || options.u; 80 | if (!plugins) { 81 | plugins = getDefaultPlugins(options); 82 | } 83 | else { 84 | if (typeof plugins === 'string') { 85 | plugins = [plugins]; 86 | } 87 | } 88 | 89 | var postcssBefore = options.postcssBefore || options.before || []; 90 | var postcssAfter = options.postcssAfter || options.after || []; 91 | plugins = (Array.isArray(postcssBefore) ? postcssBefore : [postcssBefore]).concat(plugins).concat(postcssAfter); 92 | 93 | // load plugins by name (if a string is used) 94 | return plugins.map(function requirePlugin (name) { 95 | // assume not strings are already required plugins 96 | if (typeof name !== 'string') { 97 | return name; 98 | } 99 | 100 | var plugin = module.parent.require(name); 101 | 102 | // custom scoped name generation 103 | if (name === 'postcss-modules-scope') { 104 | options[name] = options[name] || {}; 105 | if (!options[name].generateScopedName) { 106 | options[name].generateScopedName = generateLongName; 107 | } 108 | } 109 | 110 | if (name in options) { 111 | plugin = plugin(options[name]); 112 | } 113 | else { 114 | plugin = plugin.postcss || plugin(); 115 | } 116 | 117 | return plugin; 118 | }); 119 | } 120 | 121 | module.exports = function (browserify, options) { 122 | options = options || {}; 123 | 124 | // if no root directory is specified, assume the cwd 125 | var rootDir = options.rootDir || options.d || browserify._options.basedir; 126 | if (rootDir) { 127 | rootDir = path.resolve(rootDir); 128 | } 129 | if (!rootDir) { 130 | rootDir = process.cwd(); 131 | } 132 | 133 | var cssOutFilename = options.output || options.o; 134 | var jsonOutFilename = options.json || options.jsonOutput; 135 | var loader; 136 | // keep track of all tokens so we can avoid duplicates 137 | var tokensByFile; 138 | if (options.cache) { 139 | if (options.cache.loaders) { 140 | loader = options.cache.loaders[cssOutFilename]; 141 | } else { 142 | options.cache.loaders = {}; 143 | } 144 | if (options.cache.tokens) { 145 | tokensByFile = options.cache.tokens; 146 | } else { 147 | options.cache.tokens = {}; 148 | } 149 | } 150 | 151 | loader = loader || new FileSystemLoader(rootDir, getPlugins(options)); 152 | tokensByFile = tokensByFile || {}; 153 | 154 | if (options.cache) { 155 | options.cache.loaders[cssOutFilename] = loader; 156 | options.cache.tokens = tokensByFile; 157 | } 158 | 159 | var transformOpts = { 160 | cssFilePattern: options.filePattern 161 | , cssFiles: [] 162 | , cssOutFilename: cssOutFilename 163 | , global: options.global || options.g 164 | , loader: loader 165 | , rootDir: rootDir 166 | , tokensByFile: tokensByFile 167 | }; 168 | 169 | browserify.transform(Cmify, transformOpts); 170 | 171 | // ---- 172 | 173 | function addHooks () { 174 | browserify.pipeline.get('pack').push(through(function write (row, enc, next) { 175 | next(null, row); 176 | }, function end (cb) { 177 | // on each bundle, create a new stream b/c the old one might have ended 178 | var compiledCssStream = new ReadableStream(); 179 | compiledCssStream._read = function () {}; 180 | 181 | browserify.emit('css stream', compiledCssStream); 182 | 183 | // Combine the collected sources for a single bundle into a single CSS file 184 | var self = this; 185 | var css = loader.finalSource; 186 | 187 | // end the output stream 188 | compiledCssStream.push(css); 189 | compiledCssStream.push(null); 190 | 191 | var writes = []; 192 | 193 | // write the css file 194 | if (cssOutFilename) { 195 | writes.push(writeFile(cssOutFilename, css)); 196 | } 197 | 198 | // write the classname manifest 199 | if (jsonOutFilename) { 200 | writes.push(writeFile(jsonOutFilename, JSON.stringify(normalizeManifestPaths(tokensByFile, rootDir)))); 201 | } 202 | Promise.all(writes) 203 | .then(function () { cb(); }) 204 | .catch(function (err) { self.emit('error', err); cb(); }); 205 | })); 206 | } 207 | 208 | browserify.on('reset', addHooks); 209 | addHooks(); 210 | 211 | return browserify; 212 | }; 213 | 214 | function writeFile (filename, content) { 215 | return new Promise(function (resolve, reject) { 216 | fs.writeFile(filename, content, function (err) { 217 | if (err) reject(err); 218 | else resolve(); 219 | }); 220 | }); 221 | } 222 | 223 | module.exports.generateShortName = generateShortName; 224 | module.exports.generateLongName = generateLongName; 225 | --------------------------------------------------------------------------------