diff --git a/cmify.js b/cmify.js new file mode 100644 index 0000000..cd34282 --- /dev/null +++ b/cmify.js @@ -0,0 +1,33 @@ +var stream = require("stream"); +var path = require("path"); +var util = require("util"); + +util.inherits(Cmify, stream.Transform); +function Cmify(filename, opts) { + if (!(this instanceof Cmify)) { + return new Cmify(filename, opts); + } + + stream.Transform.call(this); + + this.cssExt = /\.css$/; + this._data = ""; + this._filename = filename; +} + +Cmify.prototype.isCssFile = function (filename) { + return this.cssExt.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() +}; + +module.exports = Cmify diff --git a/file-system-loader.js b/file-system-loader.js new file mode 100644 index 0000000..0a3d06a --- /dev/null +++ b/file-system-loader.js @@ -0,0 +1,136 @@ +'use strict'; + +var DepGraph = require('dependency-graph').DepGraph; + +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.traces = {}; + 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), + fileRelativePath = _path2['default'].resolve(_path2['default'].join(_this.root, relativeDir), newPath); + + // if the path is not relative or absolute, try to resolve it in node_modules + if (newPath[0] !== '.' && newPath[0] !== '/') { + try { + fileRelativePath = require.resolve(newPath); + } 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.traces[trace] = fileRelativePath; + _this.tokensByFile[fileRelativePath] = exportTokens; + resolve(exportTokens); + }, reject); + }); + }); + } + }, { + key: 'finalSource', + get: function () { + var traces = this.traces; + var sources = this.sources; + var written = {}; + + return Object.keys(traces).sort(traceKeySorter).map(function (key) { + var filename = traces[key]; + 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 5bcbc91..7505996 100644 --- a/index.js +++ b/index.js @@ -3,9 +3,9 @@ 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 FileSystemLoader = require('./file-system-loader'); var assign = require('object-assign'); var stringHash = require('string-hash'); var ReadableStream = require('stream').Readable; @@ -16,7 +16,7 @@ var ReadableStream = require('stream').Readable; */ function generateShortName (name, filename, css) { // first occurrence of the name - // TOOD: better match with regex + // TODO: better match with regex var i = css.indexOf('.' + name); var numLines = css.substr(0, i).split(/[\r\n]/).length; @@ -74,21 +74,6 @@ function normalizeManifestPaths (tokensByFile, rootDir) { return output; } -function dedupeSources (sources) { - var foundHashes = {} - Object.keys(sources).forEach(function (key) { - var hash = stringHash(sources[key]); - if (foundHashes[hash]) { - delete sources[key]; - } - else { - foundHashes[hash] = true; - } - }) -} - -var cssExt = /\.css$/; - // caches // // persist these for as long as the process is running. #32 @@ -107,6 +92,11 @@ module.exports = function (browserify, options) { if (rootDir) { rootDir = path.resolve(rootDir); } if (!rootDir) { rootDir = process.cwd(); } + var transformOpts = {}; + if (options.global) { + transformOpts.global = true; + } + var cssOutFilename = options.output || options.o; var jsonOutFilename = options.json || options.jsonOutput; var sourceKey = cssOutFilename; @@ -161,31 +151,56 @@ module.exports = function (browserify, options) { // but re-created on each bundle call. var compiledCssStream; - function transform (filename) { - // only handle .css files - if (!cssExt.test(filename)) { - return through(); - } + // TODO: clean this up so there's less scope crossing + Cmify.prototype._flush = function (callback) { + var self = this; + var filename = this._filename; - return through(function noop () {}, function end () { - var self = this; + // only handle .css files + if (!this.isCssFile(filename)) { return callback(); } + + // convert css to js before pushing + // reset the `tokensByFile` cache + var relFilename = path.relative(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 + "')" + }).join('\n'), + 'module.exports = ' + JSON.stringify(tokens) + ].join('\n'); + + var isValid = true; + var isUndefined = /\bundefined\b/; + Object.keys(tokens).forEach(function (k) { + if (isUndefined.test(tokens[k])) { + isValid = false; + } + }); - loader.fetch(path.relative(rootDir, filename), '/').then(function (tokens) { - var output = 'module.exports = ' + JSON.stringify(tokens); + if (!isValid) { + var err = 'Composition in ' + filename + ' contains an undefined reference'; + console.error(err) + output += '\nconsole.error("' + err + '");'; + } - assign(tokensByFile, loader.tokensByFile); + assign(tokensByFile, loader.tokensByFile); - self.queue(output); - self.queue(null); - }, function (err) { - self.emit('error', err); - }); + self.push(output); + return callback() + }).catch(function (err) { + self.push('console.error("' + err + '");'); + browserify.emit('error', err); + return callback() }); - } + }; - browserify.transform(transform, { - global: true - }); + browserify.transform(Cmify, transformOpts); + + // ---- browserify.on('bundle', function (bundle) { // on each bundle, create a new stream b/c the old one might have ended @@ -195,10 +210,6 @@ module.exports = function (browserify, options) { bundle.emit('css stream', compiledCssStream); bundle.on('end', function () { - // under certain conditions (eg. with shared libraries) we can end up with - // multiple occurrences of the same rule, so we need to remove duplicates - dedupeSources(loader.sources) - // Combine the collected sources for a single bundle into a single CSS file var css = loader.finalSource; @@ -223,9 +234,6 @@ module.exports = function (browserify, options) { } }); } - - // reset the `tokensByFile` cache - tokensByFile = {}; }); }); diff --git a/package.json b/package.json index d09d392..88d8634 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "main": "index.js", "dependencies": { "css-modules-loader-core": "^1.0.0", + "dependency-graph": "^0.4.1", "object-assign": "^3.0.0", "promise-polyfill": "^2.1.0", "string-hash": "^1.1.0", diff --git a/tests/cases/compose-node-module/expected.css b/tests/cases/compose-node-module/expected.css index ddd63b1..492da06 100644 --- a/tests/cases/compose-node-module/expected.css +++ b/tests/cases/compose-node-module/expected.css @@ -1,4 +1,4 @@ -._cool_styles_styles__foo { +._node_modules_cool_styles_styles__foo { color: #F00; } ._styles__foo { diff --git a/tests/index.js b/tests/index.js index f651e55..af446aa 100644 --- a/tests/index.js +++ b/tests/index.js @@ -8,6 +8,8 @@ var path = require('path'); var casesDir = path.join(__dirname, 'cases'); var cssOutFilename = 'out.css'; +var globalCases = ['compose-node-module', 'import-node-module']; + function runTestCase (dir) { tape('case: ' + dir, function (t) { var fakeFs = { @@ -30,6 +32,9 @@ function runTestCase (dir) { rootDir: path.join(casesDir, dir) , output: cssOutFilename , generateScopedName: cssModulesify.generateLongName + + // only certain cases will use a global transform + , global: globalCases.indexOf(dir) !== -1 }); b.bundle(function (err) {