UNPKG

7.38 kBJavaScriptView Raw
1// Some css-modules-loader-code dependencies use Promise so we'll provide it for older node versions
2if (!global.Promise) { global.Promise = require('promise-polyfill'); }
3
4var fs = require('fs');
5var path = require('path');
6var Cmify = require('./cmify');
7var Core = require('css-modules-loader-core');
8var FileSystemLoader = require('./file-system-loader');
9var assign = require('object-assign');
10var stringHash = require('string-hash');
11var ReadableStream = require('stream').Readable;
12var through = require('through2');
13
14/*
15 Custom `generateScopedName` function for `postcss-modules-scope`.
16 Short names consisting of source hash and line number.
17*/
18function generateShortName (name, filename, css) {
19 // first occurrence of the name
20 // TODO: better match with regex
21 var i = css.indexOf('.' + name);
22 var numLines = css.substr(0, i).split(/[\r\n]/).length;
23
24 var hash = stringHash(css).toString(36).substr(0, 5);
25 return '_' + name + '_' + hash + '_' + numLines;
26}
27
28/*
29 Custom `generateScopedName` function for `postcss-modules-scope`.
30 Appends a hash of the css source.
31*/
32function generateLongName (name, filename) {
33 var sanitisedPath = filename.replace(/\.[^\.\/\\]+$/, '')
34 .replace(/[\W_]+/g, '_')
35 .replace(/^_|_$/g, '');
36
37 return '_' + sanitisedPath + '__' + name;
38}
39
40/*
41 Get the default plugins and apply options.
42*/
43function getDefaultPlugins (options) {
44 var scope = Core.scope;
45 var customNameFunc = options.generateScopedName;
46 var defaultNameFunc = process.env.NODE_ENV === 'production' ?
47 generateShortName :
48 generateLongName;
49
50 scope.generateScopedName = customNameFunc || defaultNameFunc;
51
52 return [
53 Core.values
54 , Core.localByDefault
55 , Core.extractImports
56 , scope
57 ];
58}
59
60/*
61
62 Normalize the manifest paths so that they are always relative
63 to the project root directory.
64
65*/
66function normalizeManifestPaths (tokensByFile, rootDir) {
67 var output = {};
68 var rootDirLength = rootDir.length + 1;
69
70 Object.keys(tokensByFile).forEach(function (filename) {
71 var normalizedFilename = filename.substr(rootDirLength);
72 output[normalizedFilename] = tokensByFile[filename];
73 });
74
75 return output;
76}
77
78// caches
79//
80// persist these for as long as the process is running. #32
81
82// keep track of all tokens so we can avoid duplicates
83var tokensByFile = {};
84
85// we need a separate loader for each entry point
86var loadersByFile = {};
87
88module.exports = function (browserify, options) {
89 options = options || {};
90
91 // if no root directory is specified, assume the cwd
92 var rootDir = options.rootDir || options.d || browserify._options.basedir;
93 if (rootDir) { rootDir = path.resolve(rootDir); }
94 if (!rootDir) { rootDir = process.cwd(); }
95
96 var transformOpts = {};
97 if (options.global) {
98 transformOpts.global = true;
99 }
100
101 var cssOutFilename = options.output || options.o;
102 var jsonOutFilename = options.json || options.jsonOutput;
103 transformOpts.cssOutFilename = cssOutFilename;
104
105 // PostCSS plugins passed to FileSystemLoader
106 var plugins = options.use || options.u;
107 if (!plugins) {
108 plugins = getDefaultPlugins(options);
109 }
110 else {
111 if (typeof plugins === 'string') {
112 plugins = [plugins];
113 }
114 }
115
116 var postcssBefore = options.postcssBefore || options.before || [];
117 var postcssAfter = options.postcssAfter || options.after || [];
118 plugins = (Array.isArray(postcssBefore) ? postcssBefore : [postcssBefore]).concat(plugins).concat(postcssAfter);
119
120 // load plugins by name (if a string is used)
121 plugins = plugins.map(function requirePlugin (name) {
122 // assume not strings are already required plugins
123 if (typeof name !== 'string') {
124 return name;
125 }
126
127 var plugin = module.parent.require(name);
128
129 // custom scoped name generation
130 if (name === 'postcss-modules-scope') {
131 options[name] = options[name] || {};
132 if (!options[name].generateScopedName) {
133 options[name].generateScopedName = generateLongName;
134 }
135 }
136
137 if (name in options) {
138 plugin = plugin(options[name]);
139 }
140 else {
141 plugin = plugin.postcss || plugin();
142 }
143
144 return plugin;
145 });
146
147 // create a loader for this entry file
148 if (!loadersByFile[cssOutFilename]) {
149 loadersByFile[cssOutFilename] = new FileSystemLoader(rootDir, plugins);
150 }
151
152 // TODO: clean this up so there's less scope crossing
153 Cmify.prototype._flush = function (callback) {
154 var self = this;
155 var filename = this._filename;
156
157 // only handle .css files
158 if (!this.isCssFile(filename)) { return callback(); }
159
160 // grab the correct loader
161 var loader = loadersByFile[this._cssOutFilename];
162
163 // convert css to js before pushing
164 // reset the `tokensByFile` cache
165 var relFilename = path.relative(rootDir, filename);
166 tokensByFile[filename] = loader.tokensByFile[filename] = null;
167
168 loader.fetch(relFilename, '/').then(function (tokens) {
169 var deps = loader.deps.dependenciesOf(filename);
170 var output = deps.map(function (f) {
171 return 'require("' + f + '")';
172 });
173 output.push('module.exports = ' + JSON.stringify(tokens));
174
175 var isValid = true;
176 var isUndefined = /\bundefined\b/;
177 Object.keys(tokens).forEach(function (k) {
178 if (isUndefined.test(tokens[k])) {
179 isValid = false;
180 }
181 });
182
183 if (!isValid) {
184 var err = 'Composition in ' + filename + ' contains an undefined reference';
185 console.error(err);
186 output.push('console.error("' + err + '");');
187 }
188
189 assign(tokensByFile, loader.tokensByFile);
190
191 self.push(output.join('\n'));
192 return callback();
193 }).catch(function (err) {
194 self.push('console.error("' + err + '");');
195 self.emit('error', err);
196 return callback();
197 });
198 };
199
200 browserify.transform(Cmify, transformOpts);
201
202 // ----
203
204 function addHooks () {
205 browserify.pipeline.get('pack').push(through(function write (row, enc, next) {
206 next(null, row);
207 }, function end (cb) {
208 // on each bundle, create a new stream b/c the old one might have ended
209 var compiledCssStream = new ReadableStream();
210 compiledCssStream._read = function () {};
211
212 browserify.emit('css stream', compiledCssStream);
213
214 // Combine the collected sources for a single bundle into a single CSS file
215 var self = this;
216 var loader = loadersByFile[cssOutFilename];
217 var css = loader.finalSource;
218
219 // end the output stream
220 compiledCssStream.push(css);
221 compiledCssStream.push(null);
222
223 var writes = [];
224
225 // write the css file
226 if (cssOutFilename) {
227 writes.push(writeFile(cssOutFilename, css));
228 }
229
230 // write the classname manifest
231 if (jsonOutFilename) {
232 writes.push(writeFile(jsonOutFilename, JSON.stringify(normalizeManifestPaths(tokensByFile, rootDir))));
233 }
234 Promise.all(writes)
235 .then(function () { cb(); })
236 .catch(function (err) { self.emit('error', err); cb(); });
237 }));
238 }
239
240 browserify.on('reset', addHooks);
241 addHooks();
242
243 return browserify;
244};
245
246function writeFile (filename, content) {
247 return new Promise(function (resolve, reject) {
248 fs.writeFile(filename, content, function (err) {
249 if (err) reject(err);
250 else resolve();
251 });
252 });
253}
254
255module.exports.generateShortName = generateShortName;
256module.exports.generateLongName = generateLongName;