diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..c2658d7
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1 @@
+node_modules/
diff --git a/.eslintrc b/.eslintrc
new file mode 100755
index 0000000..ef37a41
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,236 @@
+{
+ "ecmaFeatures": {
+ },
+ "env": {
+ "node": true
+ },
+ "rules": {
+ "comma-dangle": [2, "never"],
+ "no-cond-assign": 2,
+ "no-constant-condition": 2,
+ "no-control-regex": 2,
+ "no-debugger": 2,
+ "no-dupe-keys": 2,
+ "no-empty": 2,
+ "no-empty-character-class": 2,
+ "no-ex-assign": 2,
+ "no-extra-boolean-cast": 2,
+ "no-extra-parens": 0,
+ "no-extra-semi": 2,
+ "no-func-assign": 2,
+ "no-inner-declarations": 2,
+ "no-invalid-regexp": 2,
+ "no-irregular-whitespace": 2,
+ "no-negated-in-lhs": 2,
+ "no-obj-calls": 2,
+ "no-regex-spaces": 2,
+ "no-sparse-arrays": 2,
+ "no-unreachable": 2,
+ "use-isnan": 2,
+ "valid-typeof": 2,
+ "block-scoped-var": 0,
+ "consistent-return": 2,
+ "curly": [
+ 1,
+ "multi-line"
+ ],
+ "default-case": 2,
+ "dot-notation": 2,
+ "eqeqeq": 2,
+ "guard-for-in": 2,
+ "no-alert": 1,
+ "no-caller": 2,
+ "no-div-regex": 2,
+ "no-eq-null": 2,
+ "no-eval": 2,
+ "no-extend-native": 2,
+ "no-extra-bind": 2,
+ "no-fallthrough": 2,
+ "no-floating-decimal": 2,
+ "no-implied-eval": 2,
+ "no-iterator": 2,
+ "no-labels": 2,
+ "no-lone-blocks": 2,
+ "no-loop-func": 2,
+ "no-multi-spaces": 1,
+ "no-multi-str": 2,
+ "no-native-reassign": 2,
+ "no-new": 2,
+ "no-new-func": 2,
+ "no-new-wrappers": 2,
+ "no-octal": 2,
+ "no-octal-escape": 2,
+ "no-proto": 2,
+ "no-redeclare": 2,
+ "no-return-assign": [2, "except-parens"],
+ "no-script-url": 2,
+ "no-self-compare": 2,
+ "no-sequences": 2,
+ "no-throw-literal": 2,
+ "no-unused-expressions": 0,
+ "no-with": 2,
+ "radix": 2,
+ "vars-on-top": 0,
+ "wrap-iife": 2,
+ "yoda": [
+ 1,
+ "never"
+ ],
+ "strict": [
+ 0,
+ "never"
+ ],
+ "no-delete-var": 2,
+ "no-label-var": 2,
+ "no-shadow": 2,
+ "no-shadow-restricted-names": 2,
+ "no-undef": 2,
+ "no-undef-init": 2,
+ "no-undefined": 2,
+ "no-unused-vars": [
+ 2,
+ "all"
+ ],
+ "no-use-before-define": 0,
+ "handle-callback-err": 2,
+ "no-mixed-requires": 0,
+ "no-new-require": 2,
+ "no-path-concat": 2,
+ "indent": [
+ 1,
+ 2,
+ {"SwitchCase": 1}
+ ],
+ "brace-style": [
+ 1,
+ "stroustrup",
+ {
+ "allowSingleLine": true
+ }
+ ],
+ "comma-spacing": [
+ 1,
+ {
+ "before": false,
+ "after": true
+ }
+ ],
+ "comma-style": [
+ 2,
+ "first"
+ ],
+ "consistent-this": [
+ 1,
+ "self"
+ ],
+ "eol-last": 2,
+ "func-names": 0,
+ "key-spacing": [
+ 1,
+ {
+ "beforeColon": false,
+ "afterColon": true
+ }
+ ],
+ "max-nested-callbacks": [
+ 2,
+ 3
+ ],
+ "new-cap": 2,
+ "new-parens": 2,
+ "no-array-constructor": 0,
+ "no-inline-comments": 1,
+ "no-lonely-if": 0,
+ "no-mixed-spaces-and-tabs": 2,
+ "no-multiple-empty-lines": 2,
+ "no-nested-ternary": 0,
+ "no-new-object": 2,
+ "semi-spacing": [2, {"before": false, "after": true}],
+ "no-spaced-func": 1,
+ "no-ternary": 0,
+ "no-trailing-spaces": 2,
+ "no-underscore-dangle": 0,
+ "one-var": [
+ 1,
+ {
+ "var": "never",
+ "let": "never",
+ "const": "never"
+ }
+ ],
+ "operator-assignment": [
+ 2,
+ "always"
+ ],
+ "padded-blocks": [
+ 1,
+ "never"
+ ],
+ "quote-props": [
+ 1,
+ "as-needed"
+ ],
+ "quotes": [
+ 2,
+ "single"
+ ],
+ "semi": [
+ 2,
+ "always"
+ ],
+ "sort-vars": 0,
+ "space-after-keywords": [
+ 1,
+ "always"
+ ],
+ "space-before-blocks": 0,
+ "space-before-function-paren": [
+ 1,
+ "always"
+ ],
+ "object-curly-spacing": [
+ 1,
+ "never"
+ ],
+ "array-bracket-spacing": [
+ 1,
+ "never"
+ ],
+ "space-in-parens": [
+ 1,
+ "never"
+ ],
+ "space-infix-ops": 2,
+ "space-return-throw-case": 2,
+ "space-unary-ops": [
+ 1,
+ {
+ "words": true,
+ "nonwords": false
+ }
+ ],
+ "spaced-comment": [
+ 1,
+ "always",
+ {
+ "exceptions": [
+ "-"
+ ]
+ }
+ ],
+ "wrap-regex": 0,
+ "constructor-super": 2,
+ "no-this-before-super": 2,
+ "require-yield": 2,
+ "prefer-spread": 1,
+ "no-useless-call": 1,
+ "no-invalid-this": 0,
+ "no-implicit-coercion": 0,
+ "no-const-assign": 2,
+ "no-class-assign": 2,
+ "init-declarations": 0,
+ "callback-return": [0, ["callback", "cb", "done", "next"]],
+ "arrow-spacing": [1, {"before": true, "after": true}],
+ "arrow-parens": 1
+ }
+}
diff --git a/.gitignore b/.gitignore
index 3c3629e..ce25fd0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,3 @@
-node_modules
+node_modules/*
+!node_modules/cool-styles
+npm-debug.log
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
index 9ccb608..1d0ad98 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,6 +1,5 @@
sudo: false
language: node_js
node_js:
- - "0.10"
- - "0.12"
- - "iojs"
+ - "4"
+ - "6"
diff --git a/README.md b/README.md
index 11ffd3c..f806a7a 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
[](https://travis-ci.org/css-modules/css-modulesify)
-A browserify transform to load [CSS Modules].
+A browserify plugin to load [CSS Modules].
[CSS Modules]: https://github.com/css-modules/css-modules
@@ -14,7 +14,7 @@ Normally you need to use a strict naming convention like BEM to ensure that one
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.
-## Usage
+## Getting started
First install the package: `npm install --save css-modulesify`
@@ -31,34 +31,104 @@ var div = `
...
`;
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.
-### PostCSS Plugins
+## API Usage
+
+```js
+var b = require('browserify')();
+
+b.add('./main.js');
+b.plugin(require('css-modulesify'), {
+ rootDir: __dirname,
+ output: './path/to/my.css'
+});
+
+b.bundle();
+```
+
+```js
+// or, get the output as a stream
+var b = require('browserify')();
+var fs = require('fs');
+
+b.add('./main.js');
+b.plugin(require('css-modulesify'), {
+ rootDir: __dirname
+});
+
+var bundle = b.bundle()
+b.on('css stream', function (css) {
+ css.pipe(fs.createWriteStream('mycss.css'));
+});
+```
+
+### Options:
+
+- `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.
+- `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.
+- `jsonOutput`: optional path to write a json manifest of classnames.
+- `use`: optional array of postcss plugins (by default we use the css-modules core plugins). NOTE: it's safer to use `after`
+- `before`: optional array of postcss plugins to run before the required css-modules core plugins are run.
+- `after`: optional array of postcss plugins to run after the required css-modules core plugins are run.
+- `generateScopedName`: (API only) a function to override the default behaviour of creating locally scoped classnames.
+- `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).
+- `filePattern`: optional regular expression string to specify css file names. (default: `\.css$`)
+- `cache`: optional object to persist cache between runs.
+
+### Events
+- `b.on('css stream', callback)` The callback is called with a readable stream containing the compiled CSS. You can write this to a file.
+
+## Using CSS Modules on the backend
+
+If you want to use CSS Modules in server-generated templates there are a couple of options:
+
+- 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.
+
+- Option B: configure the `jsonOutput` option with a file path and `css-modulesify` will generate a JSON manifest of classnames.
+
+
+## PostCSS Plugins
The following PostCSS plugins are enabled by default:
* [postcss-modules-local-by-default]
* [postcss-modules-extract-imports]
* [postcss-modules-scope]
+ * [postcss-modules-values]
(i.e. the [CSS Modules] specification).
-You can supply your own additional PostCSS Plugins by passing `--use|-u` to `css-modulesify`.
+You can override the default PostCSS Plugins (and add your own) by passing `--use|-u` to `css-modulesify`.
+
+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.
-In addion you may also wish to configure defined PostCSS plugins by passing `--plugin.option true`.
+In addition you may also wish to configure defined PostCSS plugins by passing `--plugin.option true`.
An example of this would be:
```
-browserify -p [css-modulesify -u postcss-modules-local-by-default \
- -u postcss-modules-extract-imports \
- -u postcss-modules-scope \
- -u postcss-color-rebeccapurple \
- -u autoprefixer --autoprefixer.browsers '> 5%' \
+browserify -p [css-modulesify \
+ --after autoprefixer --autoprefixer.browsers '> 5%' \
-o dist/main.css] -o dist/index.js src/index.js
```
[postcss-modules-local-by-default]: https://github.com/css-modules/postcss-modules-local-by-default
[postcss-modules-extract-imports]: https://github.com/css-modules/postcss-modules-extract-imports
[postcss-modules-scope]: https://github.com/css-modules/postcss-modules-scope
+[postcss-modules-values]: https://github.com/css-modules/postcss-modules-values
+
+## Building for production
+
+If you set `NODE_ENV=production` then `css-modulesify` will generate shorter (though less useful) classnames.
+
+You can also manually switch to short names by setting the `generateScopedName` option. Eg:
+
+```
+browserify.plugin(cssModulesify, {
+ rootDir: __dirname,
+ output: './dist/main.css',
+ generateScopedName: cssModulesify.generateShortName
+})
+```
## Example
diff --git a/cmify.js b/cmify.js
new file mode 100644
index 0000000..a8dec5a
--- /dev/null
+++ b/cmify.js
@@ -0,0 +1,87 @@
+var stream = require('stream');
+var util = require('util');
+var assign = require('object-assign');
+var path = require('path');
+
+util.inherits(Cmify, stream.Transform);
+function Cmify (filename, opts) {
+ if (!(this instanceof Cmify)) {
+ return new Cmify(filename, opts);
+ }
+
+ stream.Transform.call(this);
+
+ this.cssFilePattern = new RegExp(opts.cssFilePattern || '\.css$');
+ this._data = '';
+ this._filename = filename;
+ this._cssOutFilename = opts.cssOutFilename;
+ this._loader = opts.loader;
+ this._tokensByFile = opts.tokensByFile;
+ this._rootDir = opts.rootDir;
+ opts.cssFiles.push(filename);
+}
+
+Cmify.prototype.isCssFile = function (filename) {
+ return this.cssFilePattern.test(filename);
+};
+
+Cmify.prototype._transform = function (buf, enc, callback) {
+ // only handle .css files
+ if (!this.isCssFile(this._filename)) {
+ this.push(buf);
+ return callback();
+ }
+
+ this._data += buf;
+ callback();
+};
+
+Cmify.prototype._flush = function (callback) {
+ var self = this;
+ var filename = this._filename;
+
+ // only handle .css files
+ if (!this.isCssFile(filename)) { return callback(); }
+
+ // grab the correct loader
+ var loader = this._loader;
+ var tokensByFile = this._tokensByFile;
+
+ // convert css to js before pushing
+ // reset the `tokensByFile` state
+ var relFilename = path.relative(this._rootDir, filename);
+ tokensByFile[filename] = loader.tokensByFile[filename] = null;
+
+ loader.fetch(relFilename, '/').then(function (tokens) {
+ var deps = loader.deps.dependenciesOf(filename);
+ var output = deps.map(function (f) {
+ return 'require("' + f + '")';
+ });
+ output.push('module.exports = ' + JSON.stringify(tokens));
+
+ var isValid = true;
+ var isUndefined = /\bundefined\b/;
+ Object.keys(tokens).forEach(function (k) {
+ if (isUndefined.test(tokens[k])) {
+ isValid = false;
+ }
+ });
+
+ if (!isValid) {
+ var err = 'Composition in ' + filename + ' contains an undefined reference';
+ console.error(err);
+ output.push('console.error("' + err + '");');
+ }
+
+ assign(tokensByFile, loader.tokensByFile);
+
+ self.push(output.join('\n'));
+ return callback();
+ }).catch(function (err) {
+ self.push('console.error("' + err + '");');
+ self.emit('error', err);
+ return callback();
+ });
+};
+
+module.exports = Cmify;
diff --git a/file-system-loader.js b/file-system-loader.js
new file mode 100644
index 0000000..4e033fc
--- /dev/null
+++ b/file-system-loader.js
@@ -0,0 +1,143 @@
+'use strict';
+
+var DepGraph = require('dependency-graph').DepGraph;
+var nodeResolve = require('resolve');
+
+Object.defineProperty(exports, '__esModule', {
+ value: true
+});
+
+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; }; })();
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
+
+var _indexJs = require('css-modules-loader-core/lib/index.js');
+
+var _indexJs2 = _interopRequireDefault(_indexJs);
+
+var _fs = require('fs');
+
+var _fs2 = _interopRequireDefault(_fs);
+
+var _path = require('path');
+
+var _path2 = _interopRequireDefault(_path);
+
+// Sorts dependencies in the following way:
+// AAA comes before AA and A
+// AB comes after AA and before A
+// All Bs come after all As
+// This ensures that the files are always returned in the following order:
+// - In the order they were required, except
+// - After all their dependencies
+var traceKeySorter = function traceKeySorter(a, b) {
+ if (a.length < b.length) {
+ return a < b.substring(0, a.length) ? -1 : 1;
+ } else if (a.length > b.length) {
+ return a.substring(0, b.length) <= b ? -1 : 1;
+ } else {
+ return a < b ? -1 : 1;
+ }
+};
+
+var FileSystemLoader = (function () {
+ function FileSystemLoader(root, plugins) {
+ _classCallCheck(this, FileSystemLoader);
+
+ this.root = root;
+ this.sources = {};
+ this.importNr = 0;
+ this.core = new _indexJs2['default'](plugins);
+ this.tokensByFile = {};
+ this.deps = new DepGraph();
+ }
+
+ _createClass(FileSystemLoader, [{
+ key: 'fetch',
+ value: function fetch(_newPath, relativeTo, _trace) {
+ var _this = this;
+
+ var newPath = _newPath.replace(/^["']|["']$/g, ''),
+ trace = _trace || String.fromCharCode(this.importNr++);
+ return new Promise(function (resolve, reject) {
+ var relativeDir = _path2['default'].dirname(relativeTo),
+ rootRelativePath = _path2['default'].resolve(relativeDir, newPath),
+ rootRelativeDir = _path2['default'].join(_this.root, relativeDir),
+ fileRelativePath = _path2['default'].resolve(rootRelativeDir, newPath);
+
+ // if the path is not relative or absolute, try to resolve it in node_modules
+ if (newPath[0] !== '.' && newPath[0] !== '/') {
+ var paths;
+ if (process.env.NODE_PATH) {
+ paths = process.env.NODE_PATH.split(_path2['default'].delimiter);
+ }
+ try {
+ fileRelativePath = nodeResolve.sync(newPath, {
+ basedir: rootRelativeDir,
+ paths: paths
+ });
+ // in this case we need to actualize rootRelativePath too
+ rootRelativePath = _path2['default'].relative(_this.root, fileRelativePath);
+ } catch (e) {}
+ }
+
+ // first time? add a node
+ if (_trace === undefined) {
+ if (!_this.deps.hasNode(fileRelativePath)) {
+ _this.deps.addNode(fileRelativePath);
+ }
+ }
+ // otherwise add a dependency
+ else {
+ var parentFilePath = _path2['default'].join(_this.root, relativeTo);
+ if (!_this.deps.hasNode(parentFilePath)) {
+ console.error('NO NODE', parentFilePath, fileRelativePath)
+ }
+ if (!_this.deps.hasNode(fileRelativePath)) {
+ _this.deps.addNode(fileRelativePath);
+ }
+ _this.deps.addDependency(parentFilePath, fileRelativePath);
+ }
+
+ var tokens = _this.tokensByFile[fileRelativePath];
+ if (tokens) {
+ return resolve(tokens);
+ }
+
+ _fs2['default'].readFile(fileRelativePath, 'utf-8', function (err, source) {
+ if (err) reject(err);
+ _this.core.load(source, rootRelativePath, trace, _this.fetch.bind(_this)).then(function (_ref) {
+ var injectableSource = _ref.injectableSource;
+ var exportTokens = _ref.exportTokens;
+
+ _this.sources[fileRelativePath] = injectableSource;
+ _this.tokensByFile[fileRelativePath] = exportTokens;
+ resolve(exportTokens);
+ }, reject);
+ });
+ });
+ }
+ }, {
+ key: 'finalSource',
+ get: function () {
+ var sources = this.sources;
+ var written = {};
+
+ return this.deps.overallOrder().map(function (filename) {
+ if (written[filename] === true) {
+ return null;
+ }
+ written[filename] = true;
+
+ return sources[filename];
+ }).join('');
+ }
+ }]);
+
+ return FileSystemLoader;
+})();
+
+exports['default'] = FileSystemLoader;
+module.exports = exports['default'];
diff --git a/index.js b/index.js
index 61c17fa..d71525e 100644
--- a/index.js
+++ b/index.js
@@ -1,136 +1,224 @@
+// Some css-modules-loader-code dependencies use Promise so we'll provide it for older node versions
+if (!global.Promise) { global.Promise = require('promise-polyfill'); }
+
var fs = require('fs');
var path = require('path');
-var through = require('through');
+var Cmify = require('./cmify');
var Core = require('css-modules-loader-core');
-var FileSystemLoader = require('css-modules-loader-core/lib/file-system-loader');
-var assign = require('object-assign');
+var FileSystemLoader = require('./file-system-loader');
var stringHash = require('string-hash');
+var ReadableStream = require('stream').Readable;
+var through = require('through2');
+
+/*
+ Custom `generateScopedName` function for `postcss-modules-scope`.
+ Short names consisting of source hash and line number.
+*/
+function generateShortName (name, filename, css) {
+ // first occurrence of the name
+ // TODO: better match with regex
+ var i = css.indexOf('.' + name);
+ var numLines = css.substr(0, i).split(/[\r\n]/).length;
+
+ var hash = stringHash(css).toString(36).substr(0, 5);
+ return '_' + name + '_' + hash + '_' + numLines;
+}
/*
Custom `generateScopedName` function for `postcss-modules-scope`.
Appends a hash of the css source.
*/
-function createScopedNameFunc (plugin) {
- var orig = plugin.generateScopedName;
- return function (name, path, css) {
- var hash = stringHash(css).toString(36).substr(0, 5);
- return orig.apply(plugin, arguments) + '___' + hash;
- }
-};
+function generateLongName (name, filename) {
+ var sanitisedPath = filename.replace(/\.[^\.\/\\]+$/, '')
+ .replace(/[\W_]+/g, '_')
+ .replace(/^_|_$/g, '');
-var cssExt = /\.css$/;
-module.exports = function (browserify, options) {
- options = options || {};
+ return '_' + sanitisedPath + '__' + name;
+}
- var cssOutFilename = options.output || options.o;
- if (!cssOutFilename) {
- throw new Error('css-modulesify needs the --output / -o option (path to output css file)');
- }
+/*
+ Get the default plugins and apply options.
+*/
+function getDefaultPlugins (options) {
+ var scope = Core.scope;
+ var customNameFunc = options.generateScopedName;
+ var defaultNameFunc = process.env.NODE_ENV === 'production' ?
+ generateShortName :
+ generateLongName;
+
+ scope.generateScopedName = customNameFunc || defaultNameFunc;
+
+ return [
+ Core.values
+ , Core.localByDefault
+ , Core.extractImports
+ , scope
+ ];
+}
+
+/*
+
+ Normalize the manifest paths so that they are always relative
+ to the project root directory.
+
+*/
+function normalizeManifestPaths (tokensByFile, rootDir) {
+ var output = {};
+ var rootDirLength = rootDir.length + 1;
- // PostCSS plugins passed to FileSystemLoader
+ Object.keys(tokensByFile).forEach(function (filename) {
+ var normalizedFilename = filename.substr(rootDirLength);
+ output[normalizedFilename] = tokensByFile[filename];
+ });
+
+ return output;
+}
+
+// PostCSS plugins passed to FileSystemLoader
+function getPlugins (options) {
var plugins = options.use || options.u;
if (!plugins) {
- plugins = Core.defaultPlugins;
- } else {
+ plugins = getDefaultPlugins(options);
+ }
+ else {
if (typeof plugins === 'string') {
- plugins = [ plugins ];
+ plugins = [plugins];
}
+ }
- plugins = plugins.map(function requirePlugin (name) {
- // assume functions are already required plugins
- if (typeof name === 'function') {
- return name;
- }
+ var postcssBefore = options.postcssBefore || options.before || [];
+ var postcssAfter = options.postcssAfter || options.after || [];
+ plugins = (Array.isArray(postcssBefore) ? postcssBefore : [postcssBefore]).concat(plugins).concat(postcssAfter);
- var plugin = require(require.resolve(name));
+ // load plugins by name (if a string is used)
+ return plugins.map(function requirePlugin (name) {
+ // assume not strings are already required plugins
+ if (typeof name !== 'string') {
+ return name;
+ }
- // custom scoped name generation
- if (name === 'postcss-modules-scope') {
- options[name] = options[name] || {};
- if (!options[name].generateScopedName) {
- options[name].generateScopedName = createScopedNameFunc(plugin);
- }
- }
+ var plugin = module.parent.require(name);
- if (name in options) {
- plugin = plugin(options[name]);
- } else {
- plugin = plugin.postcss || plugin();
+ // custom scoped name generation
+ if (name === 'postcss-modules-scope') {
+ options[name] = options[name] || {};
+ if (!options[name].generateScopedName) {
+ options[name].generateScopedName = generateLongName;
}
+ }
- return plugin;
- });
- }
+ if (name in options) {
+ plugin = plugin(options[name]);
+ }
+ else {
+ plugin = plugin.postcss || plugin();
+ }
- // keep track of css files visited
- var filenames = [];
+ return plugin;
+ });
+}
- // keep track of all tokens so we can avoid duplicates
- var tokensByFile = {};
+module.exports = function (browserify, options) {
+ options = options || {};
- // keep track of all source files for later builds: when
- // using watchify, not all files will be caught on subsequent
- // bundles
- var sourceByFile = {};
+ // if no root directory is specified, assume the cwd
+ var rootDir = options.rootDir || options.d || browserify._options.basedir;
+ if (rootDir) {
+ rootDir = path.resolve(rootDir);
+ }
+ if (!rootDir) {
+ rootDir = process.cwd();
+ }
- function transform (filename) {
- // only handle .css files
- if (!cssExt.test(filename)) {
- return through();
+ var cssOutFilename = options.output || options.o;
+ var jsonOutFilename = options.json || options.jsonOutput;
+ var loader;
+ // keep track of all tokens so we can avoid duplicates
+ var tokensByFile;
+ if (options.cache) {
+ if (options.cache.loaders) {
+ loader = options.cache.loaders[cssOutFilename];
+ } else {
+ options.cache.loaders = {};
}
+ if (options.cache.tokens) {
+ tokensByFile = options.cache.tokens;
+ } else {
+ options.cache.tokens = {};
+ }
+ }
- // collect visited filenames
- filenames.push(filename);
+ loader = loader || new FileSystemLoader(rootDir, getPlugins(options));
+ tokensByFile = tokensByFile || {};
- return through(function noop () {}, function end () {
- var self = this;
+ if (options.cache) {
+ options.cache.loaders[cssOutFilename] = loader;
+ options.cache.tokens = tokensByFile;
+ }
+
+ var transformOpts = {
+ cssFilePattern: options.filePattern
+ , cssFiles: []
+ , cssOutFilename: cssOutFilename
+ , global: options.global || options.g
+ , loader: loader
+ , rootDir: rootDir
+ , tokensByFile: tokensByFile
+ };
- var loader = new FileSystemLoader(path.dirname(filename), plugins);
+ browserify.transform(Cmify, transformOpts);
- // pre-populate the loader's tokensByFile
- loader.tokensByFile = tokensByFile;
+ // ----
- loader.fetch(path.basename(filename), '/').then(function (tokens) {
- var output = "module.exports = " + JSON.stringify(tokens);
+ function addHooks () {
+ browserify.pipeline.get('pack').push(through(function write (row, enc, next) {
+ next(null, row);
+ }, function end (cb) {
+ // on each bundle, create a new stream b/c the old one might have ended
+ var compiledCssStream = new ReadableStream();
+ compiledCssStream._read = function () {};
- assign(tokensByFile, loader.tokensByFile);
+ browserify.emit('css stream', compiledCssStream);
- // store this file's source to be written out to disk later
- sourceByFile[filename] = loader.finalSource;
+ // Combine the collected sources for a single bundle into a single CSS file
+ var self = this;
+ var css = loader.finalSource;
- self.queue(output);
- self.queue(null);
- }, function (err) {
- console.error(err);
- });
- });
- }
+ // end the output stream
+ compiledCssStream.push(css);
+ compiledCssStream.push(null);
- browserify.transform(transform, {
- global: true
- });
+ var writes = [];
- // wrap the `bundle` function
- var bundle = browserify.bundle;
- browserify.bundle = function (cb) {
- // reset the `tokensByFile` cache
- tokensByFile = {};
-
- // call the original
- var stream = bundle.call(browserify, function () {
- // Combine the collected sources into a single CSS file
- var css = Object.keys(sourceByFile).map(function(file) {
- return sourceByFile[file];
- }).join('\n');
- var args = arguments;
-
- fs.writeFile(cssOutFilename, css, function () {
- if (typeof cb === 'function') cb.apply(null, args);
- });
- });
+ // write the css file
+ if (cssOutFilename) {
+ writes.push(writeFile(cssOutFilename, css));
+ }
- return stream;
- };
+ // write the classname manifest
+ if (jsonOutFilename) {
+ writes.push(writeFile(jsonOutFilename, JSON.stringify(normalizeManifestPaths(tokensByFile, rootDir))));
+ }
+ Promise.all(writes)
+ .then(function () { cb(); })
+ .catch(function (err) { self.emit('error', err); cb(); });
+ }));
+ }
+
+ browserify.on('reset', addHooks);
+ addHooks();
return browserify;
};
+
+function writeFile (filename, content) {
+ return new Promise(function (resolve, reject) {
+ fs.writeFile(filename, content, function (err) {
+ if (err) reject(err);
+ else resolve();
+ });
+ });
+}
+
+module.exports.generateShortName = generateShortName;
+module.exports.generateLongName = generateLongName;
diff --git a/tests/cases/import-node-module/node_modules/cool-styles/styles.css b/node_modules/cool-styles/styles.css
similarity index 100%
rename from tests/cases/import-node-module/node_modules/cool-styles/styles.css
rename to node_modules/cool-styles/styles.css
diff --git a/package.json b/package.json
index f5300f7..bba0ee2 100644
--- a/package.json
+++ b/package.json
@@ -1,21 +1,27 @@
{
"name": "css-modulesify",
- "version": "0.3.5",
+ "version": "0.28.0",
"description": "A browserify transform to load CSS Modules",
"main": "index.js",
"dependencies": {
- "css-modules-loader-core": "0.0.11",
+ "css-modules-loader-core": "^1.1.0",
+ "dependency-graph": "^0.4.1",
"object-assign": "^3.0.0",
+ "promise-polyfill": "^2.1.0",
+ "resolve": "^1.1.7",
"string-hash": "^1.1.0",
- "through": "^2.3.7"
+ "through2": "^2.0.1"
},
"devDependencies": {
"browserify": "^11.0.1",
+ "eslint": "^1.4.0",
"proxyquire": "^1.6.0",
+ "rebundler": "^0.2.0",
"tape": "^4.0.1"
},
"scripts": {
- "test": "tape tests/*.js"
+ "test": "tape tests/*.js",
+ "lint": "eslint index.js tests/"
},
"author": "joshwnj",
"license": "MIT",
diff --git a/tests/.gitignore b/tests/.gitignore
deleted file mode 100644
index cf4bab9..0000000
--- a/tests/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-!node_modules
diff --git a/tests/cache.js b/tests/cache.js
new file mode 100644
index 0000000..3f59240
--- /dev/null
+++ b/tests/cache.js
@@ -0,0 +1,51 @@
+var tape = require('tape');
+
+var browserify = require('browserify');
+var proxyquire = require('proxyquire');
+var fs = require('fs');
+var path = require('path');
+var rebundler = require('rebundler');
+
+var casesDir = path.join(__dirname, 'cases');
+var simpleCaseDir = path.join(casesDir, 'simple');
+var cssOutFilename = 'out.css';
+
+tape('multiple builds', function (t) {
+ var fakeFs = {
+ writeFile: function (filename, content, cb) {
+ var expected = fs.readFileSync(path.join(simpleCaseDir, 'expected.css'), 'utf8');
+
+ t.equal(filename, cssOutFilename, 'correct output filename');
+ t.equal(content, expected, 'output matches expected');
+ cb();
+ }
+ };
+
+ var cssModulesify = proxyquire('../', {
+ fs: fakeFs
+ });
+
+ var cssModulesifyCache = {};
+ var getBundler = rebundler(function (cache, packageCache) {
+ return browserify(path.join(simpleCaseDir, 'main.js'), {
+ cache: cache
+ , packageCache: packageCache
+ , fullPaths: true
+ })
+ .plugin(cssModulesify, {
+ rootDir: path.join(simpleCaseDir)
+ , output: cssOutFilename
+ , cache: cssModulesifyCache
+ });
+ });
+
+ getBundler().bundle(function (err) {
+ t.error(err, 'initial bundle without a cache does not error');
+
+ getBundler().bundle(function (err2) {
+ t.error(err2, 'second pass bundle with a cache does not error');
+
+ t.end();
+ });
+ });
+});
diff --git a/tests/cases/compose-from-shared/expected.css b/tests/cases/compose-from-shared/expected.css
new file mode 100644
index 0000000..9c004b1
--- /dev/null
+++ b/tests/cases/compose-from-shared/expected.css
@@ -0,0 +1,9 @@
+._shared__shared {
+ background: #000;
+}
+._styles_1__foo {
+ color: #F00;
+}
+._styles_2__bar {
+ background: #BAA;
+}
diff --git a/tests/cases/compose-from-shared/main.js b/tests/cases/compose-from-shared/main.js
new file mode 100644
index 0000000..14cc992
--- /dev/null
+++ b/tests/cases/compose-from-shared/main.js
@@ -0,0 +1,2 @@
+require('./styles-1.css');
+require('./styles-2.css');
diff --git a/tests/cases/compose-from-shared/shared.css b/tests/cases/compose-from-shared/shared.css
new file mode 100644
index 0000000..d6fc168
--- /dev/null
+++ b/tests/cases/compose-from-shared/shared.css
@@ -0,0 +1,3 @@
+.shared {
+ background: #000;
+}
diff --git a/tests/cases/compose-from-shared/styles-1.css b/tests/cases/compose-from-shared/styles-1.css
new file mode 100644
index 0000000..2ffd802
--- /dev/null
+++ b/tests/cases/compose-from-shared/styles-1.css
@@ -0,0 +1,4 @@
+.foo {
+ composes: shared from "./shared.css";
+ color: #F00;
+}
diff --git a/tests/cases/compose-from-shared/styles-2.css b/tests/cases/compose-from-shared/styles-2.css
new file mode 100644
index 0000000..f9d32d8
--- /dev/null
+++ b/tests/cases/compose-from-shared/styles-2.css
@@ -0,0 +1,4 @@
+.bar {
+ composes: shared from "./shared.css";
+ background: #BAA;
+}
diff --git a/tests/cases/compose-local-node-module/custom.js b/tests/cases/compose-local-node-module/custom.js
new file mode 100644
index 0000000..7e10afd
--- /dev/null
+++ b/tests/cases/compose-local-node-module/custom.js
@@ -0,0 +1,3 @@
+module.exports = {
+ global: true
+}
diff --git a/tests/cases/compose-local-node-module/expected.css b/tests/cases/compose-local-node-module/expected.css
new file mode 100644
index 0000000..fa249e5
--- /dev/null
+++ b/tests/cases/compose-local-node-module/expected.css
@@ -0,0 +1,6 @@
+._node_modules_cool_local_styles_styles__foo {
+ color: #F00;
+}
+._styles__foo {
+ background: black;
+}
diff --git a/tests/cases/compose-local-node-module/main.js b/tests/cases/compose-local-node-module/main.js
new file mode 100644
index 0000000..922a6aa
--- /dev/null
+++ b/tests/cases/compose-local-node-module/main.js
@@ -0,0 +1,2 @@
+var styles = require('./styles.css');
+module.exports = styles;
diff --git a/tests/cases/compose-local-node-module/node_modules/cool-local-styles/styles.css b/tests/cases/compose-local-node-module/node_modules/cool-local-styles/styles.css
new file mode 100644
index 0000000..092b8ad
--- /dev/null
+++ b/tests/cases/compose-local-node-module/node_modules/cool-local-styles/styles.css
@@ -0,0 +1,3 @@
+.foo {
+ color: #F00;
+}
diff --git a/tests/cases/compose-local-node-module/styles.css b/tests/cases/compose-local-node-module/styles.css
new file mode 100644
index 0000000..2f17694
--- /dev/null
+++ b/tests/cases/compose-local-node-module/styles.css
@@ -0,0 +1,4 @@
+.foo {
+ composes: foo from "cool-local-styles/styles.css";
+ background: black;
+}
diff --git a/tests/cases/compose-node-module/custom.js b/tests/cases/compose-node-module/custom.js
new file mode 100644
index 0000000..7e10afd
--- /dev/null
+++ b/tests/cases/compose-node-module/custom.js
@@ -0,0 +1,3 @@
+module.exports = {
+ global: true
+}
diff --git a/tests/cases/compose-node-module/expected.css b/tests/cases/compose-node-module/expected.css
new file mode 100644
index 0000000..492da06
--- /dev/null
+++ b/tests/cases/compose-node-module/expected.css
@@ -0,0 +1,6 @@
+._node_modules_cool_styles_styles__foo {
+ color: #F00;
+}
+._styles__foo {
+ background: black;
+}
diff --git a/tests/cases/compose-node-module/main.js b/tests/cases/compose-node-module/main.js
new file mode 100644
index 0000000..922a6aa
--- /dev/null
+++ b/tests/cases/compose-node-module/main.js
@@ -0,0 +1,2 @@
+var styles = require('./styles.css');
+module.exports = styles;
diff --git a/tests/cases/compose-node-module/styles.css b/tests/cases/compose-node-module/styles.css
new file mode 100644
index 0000000..ffb6962
--- /dev/null
+++ b/tests/cases/compose-node-module/styles.css
@@ -0,0 +1,4 @@
+.foo {
+ composes: foo from "cool-styles/styles.css";
+ background: black;
+}
diff --git a/tests/cases/custom-ext/custom.js b/tests/cases/custom-ext/custom.js
new file mode 100644
index 0000000..548c51d
--- /dev/null
+++ b/tests/cases/custom-ext/custom.js
@@ -0,0 +1,3 @@
+module.exports = {
+ filePattern: /\.cssmodule$/
+}
diff --git a/tests/cases/custom-ext/expected.css b/tests/cases/custom-ext/expected.css
new file mode 100644
index 0000000..f0a88f9
--- /dev/null
+++ b/tests/cases/custom-ext/expected.css
@@ -0,0 +1,3 @@
+._styles__foo {
+ color: #F00;
+}
diff --git a/tests/cases/custom-ext/main.js b/tests/cases/custom-ext/main.js
new file mode 100644
index 0000000..96280a2
--- /dev/null
+++ b/tests/cases/custom-ext/main.js
@@ -0,0 +1,3 @@
+// test using a custom `filePattern` option
+var styles = require('./styles.cssmodule');
+module.exports = styles;
diff --git a/tests/cases/custom-ext/styles.cssmodule b/tests/cases/custom-ext/styles.cssmodule
new file mode 100644
index 0000000..092b8ad
--- /dev/null
+++ b/tests/cases/custom-ext/styles.cssmodule
@@ -0,0 +1,3 @@
+.foo {
+ color: #F00;
+}
diff --git a/tests/cases/import-node-module/custom.js b/tests/cases/import-node-module/custom.js
new file mode 100644
index 0000000..7e10afd
--- /dev/null
+++ b/tests/cases/import-node-module/custom.js
@@ -0,0 +1,3 @@
+module.exports = {
+ global: true
+}
diff --git a/tests/cases/import-node-module/expected.css b/tests/cases/import-node-module/expected.css
index f0a88f9..93515fc 100644
--- a/tests/cases/import-node-module/expected.css
+++ b/tests/cases/import-node-module/expected.css
@@ -1,3 +1,3 @@
-._styles__foo {
+._node_modules_cool_styles_styles__foo {
color: #F00;
}
diff --git a/tests/cases/import-node-module/main.js b/tests/cases/import-node-module/main.js
index 59f8d0d..729af44 100644
--- a/tests/cases/import-node-module/main.js
+++ b/tests/cases/import-node-module/main.js
@@ -1 +1,2 @@
var styles = require('cool-styles/styles.css');
+module.exports = styles;
diff --git a/tests/cases/multiple-js-files/expected.css b/tests/cases/multiple-js-files/expected.css
new file mode 100644
index 0000000..00c95c8
--- /dev/null
+++ b/tests/cases/multiple-js-files/expected.css
@@ -0,0 +1,3 @@
+._simple_styles__foo {
+ color: #F00;
+}
diff --git a/tests/cases/multiple-js-files/main.js b/tests/cases/multiple-js-files/main.js
new file mode 100644
index 0000000..a095317
--- /dev/null
+++ b/tests/cases/multiple-js-files/main.js
@@ -0,0 +1 @@
+module.exports = require('../simple/main.js');
diff --git a/tests/index.js b/tests/index.js
index 1f06e6d..0270550 100644
--- a/tests/index.js
+++ b/tests/index.js
@@ -8,13 +8,18 @@ var path = require('path');
var casesDir = path.join(__dirname, 'cases');
var cssOutFilename = 'out.css';
-// test cases are expected to have:
-// - main.js (entry point)
-// - expected.css (what to expect from css-modulesify output)
-fs.readdirSync(path.join(__dirname, 'cases')).forEach(runTestCase);
-
function runTestCase (dir) {
tape('case: ' + dir, function (t) {
+ // load (optional) custom setup for this testcase
+ var customPath = path.join(casesDir, dir, 'custom.js');
+ var customOpts;
+ try {
+ fs.accessSync(customPath);
+ customOpts = require(customPath);
+ } catch (e) {
+ customOpts = {};
+ }
+
var fakeFs = {
writeFile: function (filename, content, cb) {
var expected = fs.readFileSync(path.join(casesDir, dir, 'expected.css'), 'utf8');
@@ -30,18 +35,28 @@ function runTestCase (dir) {
});
var b = browserify();
+
b.add(path.join(casesDir, dir, 'main.js'));
- b.plugin(cssModulesify, {
- output: cssOutFilename
- });
+ b.plugin(cssModulesify, Object.assign({}, {
+ rootDir: path.join(casesDir, dir)
+ , output: cssOutFilename
+ , generateScopedName: cssModulesify.generateLongName
+ }, customOpts));
- b.bundle(function (err, buf) {
+ b.bundle(function (err) {
if (err) {
- console.error(err);
- return t.fail('Unexpected error');
+ t.error(err, 'should not error');
}
t.end();
});
});
}
+
+// test cases are expected to have:
+// - main.js (entry point)
+// - expected.css (what to expect from css-modulesify output)
+// optional:
+// - custom.js (module that exports an object of custom settings)
+
+fs.readdirSync(path.join(__dirname, 'cases')).forEach(runTestCase);
diff --git a/tests/stream-output.js b/tests/stream-output.js
new file mode 100644
index 0000000..48fc861
--- /dev/null
+++ b/tests/stream-output.js
@@ -0,0 +1,69 @@
+var tape = require('tape');
+
+var browserify = require('browserify');
+var proxyquire = require('proxyquire');
+var fs = require('fs');
+var path = require('path');
+
+var casesDir = path.join(__dirname, 'cases');
+var simpleCaseDir = path.join(casesDir, 'simple');
+var cssFilesTotal = 1;
+var cssOutFilename = 'out.css';
+
+tape('stream output', function (t) {
+ var fakeFs = {
+ writeFile: function (filename, content, cb) {
+ var expected = fs.readFileSync(path.join(simpleCaseDir, 'expected.css'), 'utf8');
+
+ t.equal(filename, cssOutFilename, 'correct output filename');
+ t.equal(content, expected, 'output matches expected');
+ cb();
+ }
+ };
+
+ var cssModulesify = proxyquire('../', {
+ fs: fakeFs
+ });
+
+ t.plan(cssFilesTotal * 2 + 2);
+
+ var cssFilesCount = 0;
+ var b = browserify(path.join(simpleCaseDir, 'main.js'));
+
+ b
+ .plugin(cssModulesify, {
+ rootDir: path.join(simpleCaseDir)
+ })
+ .on('error', t.error)
+ .bundle(function noop () {});
+
+ b
+ .once('css stream', function (stream) {
+ stream
+ .on('data', function onData (css) {
+ var cssString = css.toString();
+ // just get the first class name, use that as an id
+ var cssId = cssString.split('\n')[0].split(' ')[0];
+
+ t.ok(
+ ++cssFilesCount <= cssFilesTotal
+ , 'emits data for ' + cssId
+ );
+
+ t.ok(
+ cssString.indexOf('._styles') === 0
+ , 'emits compiled css for ' + cssId
+ );
+ })
+ .on('end', function onEnd () {
+ t.pass('ends the stream');
+
+ b.bundle(function noop () {});
+
+ b.once('css stream', function (stream2) {
+ t.ok(stream2, 'registers a second event for a CSS stream');
+ });
+ })
+ .on('error', t.error);
+ });
+});